diff --git a/governance/chat/program/tests/program_test/mod.rs b/governance/chat/program/tests/program_test/mod.rs index ff177d0b8cd..4eeebc7c22c 100644 --- a/governance/chat/program/tests/program_test/mod.rs +++ b/governance/chat/program/tests/program_test/mod.rs @@ -186,17 +186,18 @@ impl GovernanceChatProgramTest { let governed_account_address = Pubkey::new_unique(); let governance_config = GovernanceConfig { + community_vote_threshold: VoteThreshold::YesVotePercentage(60), min_community_weight_to_create_proposal: 5, - min_council_weight_to_create_proposal: 2, min_transaction_hold_up_time: 10, - max_voting_time: 10, - community_vote_threshold: VoteThreshold::YesVotePercentage(60), + voting_base_time: 10, community_vote_tipping: spl_governance::state::enums::VoteTipping::Strict, council_vote_threshold: VoteThreshold::YesVotePercentage(10), council_veto_vote_threshold: VoteThreshold::YesVotePercentage(50), + min_council_weight_to_create_proposal: 2, council_vote_tipping: spl_governance::state::enums::VoteTipping::Strict, community_veto_vote_threshold: VoteThreshold::YesVotePercentage(55), - reserved: [0; 5], + voting_cool_off_time: 1, + reserved: 0, }; let token_owner_record_address = get_token_owner_record_address( diff --git a/governance/program/src/error.rs b/governance/program/src/error.rs index 7bd1445852d..f2adea1f542 100644 --- a/governance/program/src/error.rs +++ b/governance/program/src/error.rs @@ -431,13 +431,17 @@ pub enum GovernanceError { #[error("Invalid GoverningToken source")] InvalidGoverningTokenSource, // 603 - /// Cannot change community TokenType to Memebership - #[error("Cannot change community TokenType to Memebership")] - CannotChangeCommunityTokenTypeToMemebership, // 604 + /// Cannot change community TokenType to Membership + #[error("Cannot change community TokenType to Membership")] + CannotChangeCommunityTokenTypeToMembership, // 604 /// Voter weight threshold disabled #[error("Voter weight threshold disabled")] VoterWeightThresholdDisabled, // 605 + + /// Vote not allowed in cool off time + #[error("Vote not allowed in cool off time")] + VoteNotAllowedInCoolOffTime, // 606 } impl PrintProgramError for GovernanceError { diff --git a/governance/program/src/processor/process_cast_vote.rs b/governance/program/src/processor/process_cast_vote.rs index 043efa4f736..2dfcdaecfdb 100644 --- a/governance/program/src/processor/process_cast_vote.rs +++ b/governance/program/src/processor/process_cast_vote.rs @@ -82,7 +82,7 @@ pub fn process_cast_vote( governance_info.key, &proposal_governing_token_mint, )?; - proposal_data.assert_can_cast_vote(&governance_data.config, clock.unix_timestamp)?; + proposal_data.assert_can_cast_vote(&governance_data.config, &vote, clock.unix_timestamp)?; let mut voter_token_owner_record_data = get_token_owner_record_data_for_realm_and_governing_mint( diff --git a/governance/program/src/processor/process_relinquish_vote.rs b/governance/program/src/processor/process_relinquish_vote.rs index 49c22d0a5d5..724003ab46e 100644 --- a/governance/program/src/processor/process_relinquish_vote.rs +++ b/governance/program/src/processor/process_relinquish_vote.rs @@ -65,10 +65,10 @@ pub fn process_relinquish_vote(program_id: &Pubkey, accounts: &[AccountInfo]) -> let clock = Clock::get()?; // If the Proposal is still being voted on then the token owner vote will be withdrawn and it won't count towards the vote outcome - // Note: If there is no tipping point the proposal can be still in Voting state but already past the configured max_voting_time + // Note: If there is no tipping point the proposal can be still in Voting state but already past the configured max voting time (base + cool off voting time) // It means it awaits manual finalization (FinalizeVote) and it should no longer be possible to withdraw the vote if proposal_data.state == ProposalState::Voting - && !proposal_data.has_vote_time_ended(&governance_data.config, clock.unix_timestamp) + && !proposal_data.has_voting_max_time_ended(&governance_data.config, clock.unix_timestamp) { let governance_authority_info = next_account_info(account_info_iter)?; // 5 let beneficiary_info = next_account_info(account_info_iter)?; // 6 diff --git a/governance/program/src/state/governance.rs b/governance/program/src/state/governance.rs index ef7e5c5a519..4417b5d4552 100644 --- a/governance/program/src/state/governance.rs +++ b/governance/program/src/state/governance.rs @@ -33,8 +33,10 @@ pub struct GovernanceConfig { /// Minimum waiting time in seconds for a transaction to be executed after proposal is voted on pub min_transaction_hold_up_time: u32, - /// Time limit in seconds for proposal to be open for voting - pub max_voting_time: u32, + /// The base voting time in seconds for proposal to be open for voting + /// Voting is unrestricted during the base voting time and any vote types can be cast + /// The base voting time can be extend by optional cool off time when only negative votes (Veto and Deny) are allowed + pub voting_base_time: u32, /// Conditions under which a Community vote will complete early pub community_vote_tipping: VoteTipping, @@ -55,8 +57,11 @@ pub struct GovernanceConfig { /// The threshold for Community Veto votes pub community_veto_vote_threshold: VoteThreshold, + /// Voting cool of time + pub voting_cool_off_time: u32, + /// Reserved space for future versions - pub reserved: [u8; 5], + pub reserved: u8, } /// Governance Account @@ -178,7 +183,7 @@ impl GovernanceV2 { } else if is_governance_v1_account_type(&self.account_type) { // V1 account can't be resized and we have to translate it back to the original format - // If reserved_v2 is used it must be individually assesed for v1 backward compatibility impact + // If reserved_v2 is used it must be individually assessed for v1 backward compatibility impact if self.reserved_v2 != [0; 128] { panic!("Extended data not supported by GovernanceV1") } @@ -288,13 +293,14 @@ pub fn get_governance_data( // In previous versions of spl-gov (< 3) we had config.proposal_cool_off_time:u32 which was unused and always 0 // In version 3.0.0 proposal_cool_off_time was replaced with council_vote_threshold:VoteThreshold and council_veto_vote_threshold:VoteThreshold - // // If we read a legacy account then council_vote_threshold == VoteThreshold::YesVotePercentage(0) - // and we coerce it to be equal to community_vote_threshold which was used for both council and community thresholds before // // Note: assert_is_valid_governance_config() prevents setting council_vote_threshold to VoteThreshold::YesVotePercentage(0) // which gives as guarantee that it is a legacy account layout set with proposal_cool_off_time = 0 + // + // Note: All the settings below are one time config migration from V1 & V2 account data to V3 if governance_data.config.council_vote_threshold == VoteThreshold::YesVotePercentage(0) { + // Set council_vote_threshold to community_vote_threshold which was used for both council and community thresholds before governance_data.config.council_vote_threshold = governance_data.config.community_vote_threshold.clone(); @@ -309,8 +315,9 @@ pub fn get_governance_data( // For legacy accounts set the community Veto threshold to Disabled governance_data.config.community_veto_vote_threshold = VoteThreshold::Disabled; - // Reset reserved space previously used for voting_proposal_count - governance_data.config.reserved = [0; 5]; + // Reset voting_cool_off_time and reserved space previously used for voting_proposal_count + governance_data.config.voting_cool_off_time = 0; + governance_data.config.reserved = 0; } Ok(governance_data) @@ -478,6 +485,10 @@ pub fn assert_is_valid_governance_config( return Err(GovernanceError::AtLeastOneVoteThresholdRequired.into()); } + if governance_config.reserved != 0 { + return Err(GovernanceError::ReservedBufferMustBeEmpty.into()); + } + Ok(()) } @@ -506,17 +517,18 @@ mod test { fn create_test_governance_config() -> GovernanceConfig { GovernanceConfig { + community_vote_threshold: VoteThreshold::YesVotePercentage(60), min_community_weight_to_create_proposal: 5, - min_council_weight_to_create_proposal: 1, min_transaction_hold_up_time: 10, - max_voting_time: 5, - community_vote_threshold: VoteThreshold::YesVotePercentage(60), + voting_base_time: 5, community_vote_tipping: VoteTipping::Strict, council_vote_threshold: VoteThreshold::YesVotePercentage(60), council_veto_vote_threshold: VoteThreshold::YesVotePercentage(50), + min_council_weight_to_create_proposal: 1, council_vote_tipping: VoteTipping::Strict, community_veto_vote_threshold: VoteThreshold::YesVotePercentage(40), - reserved: [0; 5], + voting_cool_off_time: 2, + reserved: 0, } } @@ -621,25 +633,15 @@ mod test { governance.config.community_vote_tipping ); - assert_eq!(governance.config.reserved, [0; 5]); + assert_eq!(governance.config.reserved, 0); + assert_eq!(governance.config.voting_cool_off_time, 0); } #[test] fn test_assert_config_invalid_with_council_zero_yes_vote_threshold() { // Arrange - let governance_config = GovernanceConfig { - community_vote_threshold: VoteThreshold::YesVotePercentage(1), - min_community_weight_to_create_proposal: 1, - min_transaction_hold_up_time: 1, - max_voting_time: 1, - community_vote_tipping: VoteTipping::Strict, - council_vote_threshold: VoteThreshold::YesVotePercentage(0), - council_veto_vote_threshold: VoteThreshold::YesVotePercentage(1), - min_council_weight_to_create_proposal: 1, - council_vote_tipping: VoteTipping::Strict, - community_veto_vote_threshold: VoteThreshold::YesVotePercentage(1), - reserved: [0; 5], - }; + let mut governance_config = create_test_governance_config(); + governance_config.council_vote_threshold = VoteThreshold::YesVotePercentage(0); // Act let err = assert_is_valid_governance_config(&governance_config) @@ -650,22 +652,77 @@ mod test { assert_eq!(err, GovernanceError::InvalidVoteThresholdPercentage.into()); } + #[test] + fn test_migrate_governance_config_from_legacy_data_to_v3() { + // Arrange + let mut governance_legacy_data = create_test_governance(); + + governance_legacy_data.config.community_vote_threshold = + VoteThreshold::YesVotePercentage(60); + + // council_vote_threshold == YesVotePercentage(0) indicates legacy account from V1 & V2 program versions + governance_legacy_data.config.council_vote_threshold = VoteThreshold::YesVotePercentage(0); + + governance_legacy_data.config.council_veto_vote_threshold = + VoteThreshold::YesVotePercentage(0); + governance_legacy_data.config.council_vote_tipping = VoteTipping::Disabled; + governance_legacy_data.config.community_veto_vote_threshold = + VoteThreshold::YesVotePercentage(0); + governance_legacy_data.config.voting_cool_off_time = 1; + governance_legacy_data.config.voting_base_time = 36000; + + let mut legacy_data = vec![]; + governance_legacy_data.serialize(&mut legacy_data).unwrap(); + + let program_id = Pubkey::new_unique(); + + let info_key = Pubkey::new_unique(); + let mut lamports = 10u64; + + let legacy_account_info = AccountInfo::new( + &info_key, + false, + false, + &mut lamports, + &mut legacy_data[..], + &program_id, + false, + Epoch::default(), + ); + // Act + let governance_v3 = get_governance_data(&program_id, &legacy_account_info).unwrap(); + + // Assert + assert_eq!( + governance_v3.config.council_vote_threshold, + VoteThreshold::YesVotePercentage(60) + ); + + assert_eq!( + governance_v3.config.council_veto_vote_threshold, + VoteThreshold::YesVotePercentage(60) + ); + + assert_eq!( + governance_v3.config.community_veto_vote_threshold, + VoteThreshold::Disabled + ); + + assert_eq!( + governance_v3.config.council_vote_tipping, + VoteTipping::Strict + ); + + assert_eq!(governance_v3.config.voting_cool_off_time, 0); + + assert_eq!(governance_v3.config.reserved, 0); + } + #[test] fn test_assert_config_invalid_with_community_zero_yes_vote_threshold() { // Arrange - let governance_config = GovernanceConfig { - community_vote_threshold: VoteThreshold::YesVotePercentage(0), - min_community_weight_to_create_proposal: 1, - min_transaction_hold_up_time: 1, - max_voting_time: 1, - community_vote_tipping: VoteTipping::Strict, - council_vote_threshold: VoteThreshold::YesVotePercentage(1), - council_veto_vote_threshold: VoteThreshold::YesVotePercentage(1), - min_council_weight_to_create_proposal: 1, - council_vote_tipping: VoteTipping::Strict, - community_veto_vote_threshold: VoteThreshold::YesVotePercentage(1), - reserved: [0; 5], - }; + let mut governance_config = create_test_governance_config(); + governance_config.community_vote_threshold = VoteThreshold::YesVotePercentage(0); // Act let err = assert_is_valid_governance_config(&governance_config) @@ -679,19 +736,9 @@ mod test { #[test] fn test_assert_config_invalid_with_all_vote_thresholds_disabled() { // Arrange - let governance_config = GovernanceConfig { - community_vote_threshold: VoteThreshold::Disabled, - min_community_weight_to_create_proposal: 1, - min_transaction_hold_up_time: 1, - max_voting_time: 1, - community_vote_tipping: VoteTipping::Strict, - council_vote_threshold: VoteThreshold::Disabled, - council_veto_vote_threshold: VoteThreshold::YesVotePercentage(1), - min_council_weight_to_create_proposal: 1, - council_vote_tipping: VoteTipping::Strict, - community_veto_vote_threshold: VoteThreshold::YesVotePercentage(1), - reserved: [0; 5], - }; + let mut governance_config = create_test_governance_config(); + governance_config.community_vote_threshold = VoteThreshold::Disabled; + governance_config.council_vote_threshold = VoteThreshold::Disabled; // Act let err = assert_is_valid_governance_config(&governance_config) @@ -705,19 +752,8 @@ mod test { #[test] fn test_assert_config_invalid_with_council_zero_yes_veto_vote_threshold() { // Arrange - let governance_config = GovernanceConfig { - community_vote_threshold: VoteThreshold::YesVotePercentage(1), - min_community_weight_to_create_proposal: 1, - min_transaction_hold_up_time: 1, - max_voting_time: 1, - community_vote_tipping: VoteTipping::Strict, - council_vote_threshold: VoteThreshold::YesVotePercentage(1), - council_veto_vote_threshold: VoteThreshold::YesVotePercentage(0), - min_council_weight_to_create_proposal: 1, - council_vote_tipping: VoteTipping::Strict, - community_veto_vote_threshold: VoteThreshold::YesVotePercentage(1), - reserved: [0; 5], - }; + let mut governance_config = create_test_governance_config(); + governance_config.council_veto_vote_threshold = VoteThreshold::YesVotePercentage(0); // Act let err = assert_is_valid_governance_config(&governance_config) @@ -731,19 +767,8 @@ mod test { #[test] fn test_assert_config_invalid_with_community_zero_yes_veto_vote_threshold() { // Arrange - let governance_config = GovernanceConfig { - community_vote_threshold: VoteThreshold::YesVotePercentage(1), - min_community_weight_to_create_proposal: 1, - min_transaction_hold_up_time: 1, - max_voting_time: 1, - council_vote_tipping: VoteTipping::Strict, - council_vote_threshold: VoteThreshold::YesVotePercentage(1), - council_veto_vote_threshold: VoteThreshold::YesVotePercentage(1), - min_council_weight_to_create_proposal: 1, - community_veto_vote_threshold: VoteThreshold::YesVotePercentage(0), - community_vote_tipping: VoteTipping::Strict, - reserved: [0; 5], - }; + let mut governance_config = create_test_governance_config(); + governance_config.community_veto_vote_threshold = VoteThreshold::YesVotePercentage(0); // Act let err = assert_is_valid_governance_config(&governance_config) diff --git a/governance/program/src/state/proposal.rs b/governance/program/src/state/proposal.rs index b0f15823651..6abccf68bd3 100644 --- a/governance/program/src/state/proposal.rs +++ b/governance/program/src/state/proposal.rs @@ -264,35 +264,65 @@ impl ProposalV2 { pub fn assert_can_cast_vote( &self, config: &GovernanceConfig, + vote: &Vote, current_unix_timestamp: UnixTimestamp, ) -> Result<(), ProgramError> { self.assert_is_voting_state() .map_err(|_| GovernanceError::InvalidStateCannotVote)?; - // Check if we are still within the configured max_voting_time period - if self.has_vote_time_ended(config, current_unix_timestamp) { + // Check if we are still within the configured max voting time period + if self.has_voting_max_time_ended(config, current_unix_timestamp) { return Err(GovernanceError::ProposalVotingTimeExpired.into()); } - Ok(()) + match vote { + Vote::Approve(_) | Vote::Abstain => { + // Once the base voting time passes and we are in the voting cool off time approving votes are no longer accepted + // Abstain is considered as positive vote because when attendance quorum is used it can tip the scales + if self.has_voting_base_time_ended(config, current_unix_timestamp) { + Err(GovernanceError::VoteNotAllowedInCoolOffTime.into()) + } else { + Ok(()) + } + } + // Within voting cool off time only counter votes are allowed + Vote::Deny | Vote::Veto => Ok(()), + } } - /// Vote end time determined by the configured max_voting_time period - pub fn vote_end_time(&self, config: &GovernanceConfig) -> UnixTimestamp { + /// Expected base vote end time determined by the configured base_voting_time and actual voting start time + pub fn voting_base_time_end(&self, config: &GovernanceConfig) -> UnixTimestamp { self.voting_at .unwrap() - .checked_add(config.max_voting_time as i64) + .checked_add(config.voting_base_time as i64) .unwrap() } - /// Checks whether the voting time has ended for the proposal - pub fn has_vote_time_ended( + /// Checks whether the base voting time has ended for the proposal + pub fn has_voting_base_time_ended( &self, config: &GovernanceConfig, current_unix_timestamp: UnixTimestamp, ) -> bool { - // Check if we passed vote_end_time - self.vote_end_time(config) < current_unix_timestamp + // Check if we passed the configured base vote end time + self.voting_base_time_end(config) < current_unix_timestamp + } + + /// Expected max vote end time determined by the configured base_voting_time, optional voting_cool_off_time and actual voting start time + pub fn voting_max_time_end(&self, config: &GovernanceConfig) -> UnixTimestamp { + self.voting_base_time_end(config) + .checked_add(config.voting_cool_off_time as i64) + .unwrap() + } + + /// Checks whether the max voting time has ended for the proposal + pub fn has_voting_max_time_ended( + &self, + config: &GovernanceConfig, + current_unix_timestamp: UnixTimestamp, + ) -> bool { + // Check if we passed the max vote end time + self.voting_max_time_end(config) < current_unix_timestamp } /// Checks if Proposal can be finalized @@ -305,7 +335,7 @@ impl ProposalV2 { .map_err(|_| GovernanceError::InvalidStateCannotFinalize)?; // We can only finalize the vote after the configured max_voting_time has expired and vote time ended - if !self.has_vote_time_ended(config, current_unix_timestamp) { + if !self.has_voting_max_time_ended(config, current_unix_timestamp) { return Err(GovernanceError::CannotFinalizeVotingInProgress.into()); } @@ -324,7 +354,7 @@ impl ProposalV2 { self.assert_can_finalize_vote(config, current_unix_timestamp)?; self.state = self.resolve_final_vote_state(max_voter_weight, vote_threshold)?; - self.voting_completed_at = Some(self.vote_end_time(config)); + self.voting_completed_at = Some(self.voting_max_time_end(config)); // Capture vote params to correctly display historical results self.max_vote_weight = Some(max_voter_weight); @@ -659,7 +689,7 @@ impl ProposalV2 { ProposalState::Voting => { // Note: If there is no tipping point the proposal can be still in Voting state but already past the configured max_voting_time // In that case we treat the proposal as finalized and it's no longer allowed to be canceled - if self.has_vote_time_ended(config, current_unix_timestamp) { + if self.has_voting_max_time_ended(config, current_unix_timestamp) { return Err(GovernanceError::ProposalVotingTimeExpired.into()); } Ok(()) @@ -1167,17 +1197,18 @@ mod test { fn create_test_governance_config() -> GovernanceConfig { GovernanceConfig { + community_vote_threshold: VoteThreshold::YesVotePercentage(60), min_community_weight_to_create_proposal: 5, - min_council_weight_to_create_proposal: 1, min_transaction_hold_up_time: 10, - max_voting_time: 5, - community_vote_threshold: VoteThreshold::YesVotePercentage(60), + voting_base_time: 5, community_vote_tipping: VoteTipping::Strict, council_vote_threshold: VoteThreshold::YesVotePercentage(60), council_veto_vote_threshold: VoteThreshold::YesVotePercentage(50), + min_council_weight_to_create_proposal: 1, council_vote_tipping: VoteTipping::Strict, community_veto_vote_threshold: VoteThreshold::YesVotePercentage(40), - reserved: [0; 5], + voting_cool_off_time: 0, + reserved: 0, } } @@ -1652,7 +1683,7 @@ mod test { // Assert assert_eq!(proposal.state,test_case.expected_finalized_state,"CASE: {:?}",test_case); assert_eq!( - Some(proposal.vote_end_time(&governance_config)), + Some(proposal.voting_max_time_end(&governance_config)), proposal.voting_completed_at ); @@ -2158,7 +2189,7 @@ mod test { let governance_config = create_test_governance_config(); let current_timestamp = - proposal.voting_at.unwrap() + governance_config.max_voting_time as i64; + proposal.voting_at.unwrap() + governance_config.voting_base_time as i64; let realm = create_test_realm(); let governing_token_mint = proposal.governing_token_mint; @@ -2193,7 +2224,7 @@ mod test { let governance_config = create_test_governance_config(); let current_timestamp = - proposal.voting_at.unwrap() + governance_config.max_voting_time as i64 + 1; + proposal.voting_at.unwrap() + governance_config.voting_base_time as i64 + 1; let realm = create_test_realm(); let governing_token_mint = proposal.governing_token_mint; @@ -2225,11 +2256,13 @@ mod test { let governance_config = create_test_governance_config(); let current_timestamp = - proposal.voting_at.unwrap() + governance_config.max_voting_time as i64 + 1; + proposal.voting_at.unwrap() + governance_config.voting_base_time as i64 + 1; + + let vote = Vote::Approve(vec![]); // Act let err = proposal - .assert_can_cast_vote(&governance_config, current_timestamp) + .assert_can_cast_vote(&governance_config, &vote, current_timestamp) .err() .unwrap(); @@ -2245,10 +2278,99 @@ mod test { let governance_config = create_test_governance_config(); let current_timestamp = - proposal.voting_at.unwrap() + governance_config.max_voting_time as i64; + proposal.voting_at.unwrap() + governance_config.voting_base_time as i64; + + let vote = Vote::Approve(vec![]); + + // Act + let result = proposal.assert_can_cast_vote(&governance_config, &vote, current_timestamp); + + // Assert + assert_eq!(result, Ok(())); + } + + #[test] + pub fn test_assert_can_vote_approve_before_voting_cool_off_time() { + // Arrange + let mut proposal = create_test_proposal(); + proposal.state = ProposalState::Voting; + + let mut governance_config = create_test_governance_config(); + governance_config.voting_cool_off_time = 2; + + let current_timestamp = + proposal.voting_at.unwrap() + governance_config.voting_base_time as i64 - 1; + + let vote = Vote::Approve(vec![]); + + // Act + let result = proposal.assert_can_cast_vote(&governance_config, &vote, current_timestamp); + + // Assert + assert_eq!(result, Ok(())); + } + + #[test] + pub fn test_assert_cannot_vote_approve_within_voting_cool_off_time() { + // Arrange + let mut proposal = create_test_proposal(); + proposal.state = ProposalState::Voting; + + let mut governance_config = create_test_governance_config(); + governance_config.voting_cool_off_time = 2; + + let current_timestamp = + proposal.voting_at.unwrap() + governance_config.voting_base_time as i64 + 1; + + let vote = Vote::Approve(vec![]); + + // Act + let err = proposal + .assert_can_cast_vote(&governance_config, &vote, current_timestamp) + .err() + .unwrap(); + + // Assert + assert_eq!(err, GovernanceError::VoteNotAllowedInCoolOffTime.into()); + } + + #[test] + pub fn test_assert_can_vote_veto_within_voting_cool_off_time() { + // Arrange + let mut proposal = create_test_proposal(); + proposal.state = ProposalState::Voting; + + let mut governance_config = create_test_governance_config(); + governance_config.voting_cool_off_time = 2; + + let current_timestamp = + proposal.voting_at.unwrap() + governance_config.voting_base_time as i64 + 1; + + let vote = Vote::Veto; + + // Act + let result = proposal.assert_can_cast_vote(&governance_config, &vote, current_timestamp); + + // Assert + assert_eq!(result, Ok(())); + } + + #[test] + pub fn test_assert_can_vote_deny_within_voting_cool_off_time() { + // Arrange + let mut proposal = create_test_proposal(); + proposal.state = ProposalState::Voting; + + let mut governance_config = create_test_governance_config(); + governance_config.voting_cool_off_time = 1; + + let current_timestamp = + proposal.voting_at.unwrap() + governance_config.voting_base_time as i64 + 1; + + let vote = Vote::Deny; // Act - let result = proposal.assert_can_cast_vote(&governance_config, current_timestamp); + let result = proposal.assert_can_cast_vote(&governance_config, &vote, current_timestamp); // Assert assert_eq!(result, Ok(())); diff --git a/governance/program/src/state/realm_config.rs b/governance/program/src/state/realm_config.rs index e8e7ab8baf5..af0bfbdbdf6 100644 --- a/governance/program/src/state/realm_config.rs +++ b/governance/program/src/state/realm_config.rs @@ -211,7 +211,7 @@ impl RealmConfigAccount { && realm_config_args.community_token_config_args.token_type == GoverningTokenType::Membership { - return Err(GovernanceError::CannotChangeCommunityTokenTypeToMemebership.into()); + return Err(GovernanceError::CannotChangeCommunityTokenTypeToMembership.into()); } Ok(()) diff --git a/governance/program/tests/process_cancel_proposal.rs b/governance/program/tests/process_cancel_proposal.rs index dbc3535130b..dcf31fc92b9 100644 --- a/governance/program/tests/process_cancel_proposal.rs +++ b/governance/program/tests/process_cancel_proposal.rs @@ -190,7 +190,63 @@ async fn test_cancel_proposal_with_vote_time_expired_error() { // Advance timestamp past max_voting_time governance_test .advance_clock_past_timestamp( - governance_cookie.account.config.max_voting_time as i64 + clock.unix_timestamp, + governance_cookie.account.config.voting_base_time as i64 + clock.unix_timestamp, + ) + .await; + + // Act + + let err = governance_test + .cancel_proposal(&proposal_cookie, &token_owner_record_cookie) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::ProposalVotingTimeExpired.into()); +} + +#[tokio::test] +async fn test_cancel_proposal_after_voting_cool_off_with_vote_time_expired_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Set none default voting cool off time + let mut governance_config = governance_test.get_default_governance_config(); + governance_config.voting_cool_off_time = 10; + + let mut governance_cookie = governance_test + .with_governance_using_config( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + &governance_config, + ) + .await + .unwrap(); + + let clock = governance_test.bench.get_clock().await; + + let proposal_cookie = governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + // Advance timestamp past max_voting_time + governance_test + .advance_clock_past_timestamp( + (governance_cookie.account.config.voting_base_time + + governance_cookie.account.config.voting_cool_off_time) as i64 + + clock.unix_timestamp, ) .await; diff --git a/governance/program/tests/process_cast_vote.rs b/governance/program/tests/process_cast_vote.rs index b16d111ed96..8ab42c30d73 100644 --- a/governance/program/tests/process_cast_vote.rs +++ b/governance/program/tests/process_cast_vote.rs @@ -1017,7 +1017,7 @@ async fn test_cast_vote_with_voting_time_expired_error() { .await; let vote_expired_at = proposal_account.voting_at.unwrap() - + governance_cookie.account.config.max_voting_time as i64; + + governance_cookie.account.config.voting_base_time as i64; governance_test .advance_clock_past_timestamp(vote_expired_at) @@ -1550,3 +1550,57 @@ async fn test_cast_vote_with_strict_tipping_and_inflated_max_vote_weight() { // max_vote_weight should be coerced from 60 to 100 assert_eq!(proposal_account.max_vote_weight, Some(100)) } + +#[tokio::test] +async fn test_cast_approve_vote_with_cannot_vote_in_cool_off_time_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Set none default voting cool off time + let mut governance_config = governance_test.get_default_governance_config(); + governance_config.voting_cool_off_time = 50; + + let mut governance_cookie = governance_test + .with_governance_using_config( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + &governance_config, + ) + .await + .unwrap(); + + let proposal_cookie = governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + // Advance timestamp into voting_cool_off_time + let clock = governance_test.bench.get_clock().await; + + governance_test + .advance_clock_past_timestamp( + clock.unix_timestamp + governance_cookie.account.config.voting_base_time as i64, + ) + .await; + + // Act + + let err = governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::VoteNotAllowedInCoolOffTime.into()); +} diff --git a/governance/program/tests/process_finalize_vote.rs b/governance/program/tests/process_finalize_vote.rs index 57889866e24..6f78f599b28 100644 --- a/governance/program/tests/process_finalize_vote.rs +++ b/governance/program/tests/process_finalize_vote.rs @@ -62,7 +62,7 @@ async fn test_finalize_vote_to_succeeded() { // Advance timestamp past max_voting_time governance_test .advance_clock_past_timestamp( - governance_cookie.account.config.max_voting_time as i64 + governance_cookie.account.config.voting_base_time as i64 + proposal_account.voting_at.unwrap(), ) .await; @@ -82,7 +82,7 @@ async fn test_finalize_vote_to_succeeded() { assert_eq!(proposal_account.state, ProposalState::Succeeded); assert_eq!( - Some(proposal_account.vote_end_time(&governance_cookie.account.config)), + Some(proposal_account.voting_max_time_end(&governance_cookie.account.config)), proposal_account.voting_completed_at ); @@ -153,7 +153,7 @@ async fn test_finalize_vote_to_defeated() { // Advance clock past max_voting_time governance_test .advance_clock_past_timestamp( - governance_cookie.account.config.max_voting_time as i64 + governance_cookie.account.config.voting_base_time as i64 + proposal_account.voting_at.unwrap(), ) .await; @@ -359,7 +359,7 @@ async fn test_finalize_council_vote() { // Advance timestamp past max_voting_time governance_test .advance_clock_past_timestamp( - governance_cookie.account.config.max_voting_time as i64 + governance_cookie.account.config.voting_base_time as i64 + proposal_account.voting_at.unwrap(), ) .await; @@ -379,7 +379,7 @@ async fn test_finalize_council_vote() { assert_eq!(proposal_account.state, ProposalState::Succeeded); assert_eq!( - Some(proposal_account.vote_end_time(&governance_cookie.account.config)), + Some(proposal_account.voting_max_time_end(&governance_cookie.account.config)), proposal_account.voting_completed_at ); @@ -390,3 +390,119 @@ async fn test_finalize_council_vote() { proposal_account.vote_threshold ); } + +#[tokio::test] +async fn test_finalize_vote_with_cannot_finalize_during_voting_time_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + // Total 210 tokens + governance_test + .mint_community_tokens(&realm_cookie, 110) + .await; + + let proposal_cookie = governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + governance_test.advance_clock().await; + + // Act + + let err = governance_test + .finalize_vote(&realm_cookie, &proposal_cookie, None) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::CannotFinalizeVotingInProgress.into()); +} + +#[tokio::test] +async fn test_finalize_vote_with_cannot_finalize_during_cool_off_time_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Set none default voting cool off time + let mut governance_config = governance_test.get_default_governance_config(); + governance_config.voting_cool_off_time = 50; + + let mut governance_cookie = governance_test + .with_governance_using_config( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + &governance_config, + ) + .await + .unwrap(); + + // Total 210 tokens + governance_test + .mint_community_tokens(&realm_cookie, 110) + .await; + + let proposal_cookie = governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // Advance timestamp into voting_cool_off_time + let clock = governance_test.bench.get_clock().await; + + governance_test + .advance_clock_past_timestamp( + clock.unix_timestamp + governance_cookie.account.config.voting_base_time as i64, + ) + .await; + + // Act + + let err = governance_test + .finalize_vote(&realm_cookie, &proposal_cookie, None) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::CannotFinalizeVotingInProgress.into()); +} diff --git a/governance/program/tests/process_relinquish_vote.rs b/governance/program/tests/process_relinquish_vote.rs index c4d1f72a92e..4303625e0ce 100644 --- a/governance/program/tests/process_relinquish_vote.rs +++ b/governance/program/tests/process_relinquish_vote.rs @@ -497,7 +497,7 @@ async fn test_relinquish_proposal_with_cannot_relinquish_in_finalizing_state_err // Advance timestamp past max_voting_time governance_test .advance_clock_past_timestamp( - governance_cookie.account.config.max_voting_time as i64 + clock.unix_timestamp, + governance_cookie.account.config.voting_base_time as i64 + clock.unix_timestamp, ) .await; @@ -598,3 +598,76 @@ async fn test_relinquish_and_cast_vote_in_single_transaction() { assert_eq!(vote_record_cookie.account, vote_record_account); } + +#[tokio::test] +async fn test_change_yes_vote_to_no_within_cool_off_time() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Set none default voting cool off time + let mut governance_config = governance_test.get_default_governance_config(); + governance_config.voting_cool_off_time = 50; + + let mut governance_cookie = governance_test + .with_governance_using_config( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + &governance_config, + ) + .await + .unwrap(); + + // Total 300 tokens + governance_test + .mint_community_tokens(&realm_cookie, 200) + .await; + + let proposal_cookie = governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // Advance timestamp into voting_cool_off_time + let clock = governance_test.bench.get_clock().await; + + governance_test + .advance_clock_past_timestamp( + clock.unix_timestamp + governance_cookie.account.config.voting_base_time as i64, + ) + .await; + + // Act + governance_test + .relinquish_vote(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) + .await + .unwrap(); + + // Assert + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(0, proposal_account.options[0].vote_weight); + assert_eq!(100, proposal_account.deny_vote_weight.unwrap()); + assert_eq!(ProposalState::Voting, proposal_account.state); +} diff --git a/governance/program/tests/process_set_realm_config.rs b/governance/program/tests/process_set_realm_config.rs index a906ab4af76..d87c19af46f 100644 --- a/governance/program/tests/process_set_realm_config.rs +++ b/governance/program/tests/process_set_realm_config.rs @@ -231,7 +231,7 @@ async fn test_set_realm_config_with_liquid_community_token_cannot_be_changed_to_ // Assert assert_eq!( err, - GovernanceError::CannotChangeCommunityTokenTypeToMemebership.into() + GovernanceError::CannotChangeCommunityTokenTypeToMembership.into() ); } diff --git a/governance/program/tests/program_test/mod.rs b/governance/program/tests/program_test/mod.rs index d5404b32cb9..2468f8655a7 100644 --- a/governance/program/tests/program_test/mod.rs +++ b/governance/program/tests/program_test/mod.rs @@ -1405,17 +1405,18 @@ impl GovernanceProgramTest { pub fn get_default_governance_config(&mut self) -> GovernanceConfig { GovernanceConfig { + community_vote_threshold: VoteThreshold::YesVotePercentage(60), min_community_weight_to_create_proposal: 5, - min_council_weight_to_create_proposal: 2, min_transaction_hold_up_time: 10, - max_voting_time: 10, - community_vote_threshold: VoteThreshold::YesVotePercentage(60), + voting_base_time: 10, community_vote_tipping: spl_governance::state::enums::VoteTipping::Strict, council_vote_threshold: VoteThreshold::YesVotePercentage(80), council_veto_vote_threshold: VoteThreshold::YesVotePercentage(55), + min_council_weight_to_create_proposal: 2, council_vote_tipping: spl_governance::state::enums::VoteTipping::Strict, community_veto_vote_threshold: VoteThreshold::YesVotePercentage(80), - reserved: [0; 5], + voting_cool_off_time: 0, + reserved: 0, } } @@ -2897,7 +2898,7 @@ impl GovernanceProgramTest { let clock = self.bench.get_clock().await; self.advance_clock_past_timestamp( - clock.unix_timestamp + governance_cookie.account.config.max_voting_time as i64, + clock.unix_timestamp + governance_cookie.account.config.voting_base_time as i64, ) .await; } diff --git a/governance/program/tests/use_proposals_with_multiple_options.rs b/governance/program/tests/use_proposals_with_multiple_options.rs index de05ebb4db8..30b5ea4b21b 100644 --- a/governance/program/tests/use_proposals_with_multiple_options.rs +++ b/governance/program/tests/use_proposals_with_multiple_options.rs @@ -298,7 +298,7 @@ async fn test_vote_on_none_executable_single_choice_proposal_with_multiple_optio // Advance timestamp past max_voting_time governance_test .advance_clock_past_timestamp( - governance_cookie.account.config.max_voting_time as i64 + clock.unix_timestamp, + governance_cookie.account.config.voting_base_time as i64 + clock.unix_timestamp, ) .await; @@ -403,7 +403,7 @@ async fn test_vote_on_none_executable_multi_choice_proposal_with_multiple_option // Advance timestamp past max_voting_time governance_test .advance_clock_past_timestamp( - governance_cookie.account.config.max_voting_time as i64 + clock.unix_timestamp, + governance_cookie.account.config.voting_base_time as i64 + clock.unix_timestamp, ) .await; @@ -563,7 +563,7 @@ async fn test_vote_on_executable_proposal_with_multiple_options_and_partial_succ // Advance timestamp past max_voting_time governance_test .advance_clock_past_timestamp( - governance_cookie.account.config.max_voting_time as i64 + clock.unix_timestamp, + governance_cookie.account.config.voting_base_time as i64 + clock.unix_timestamp, ) .await; @@ -755,7 +755,7 @@ async fn test_execute_proposal_with_multiple_options_and_partial_success() { // Advance timestamp past max_voting_time governance_test - .advance_clock_by_min_timespan(governance_cookie.account.config.max_voting_time as u64) + .advance_clock_by_min_timespan(governance_cookie.account.config.voting_base_time as u64) .await; governance_test @@ -921,7 +921,7 @@ async fn test_try_execute_proposal_with_multiple_options_and_full_deny() { // Advance timestamp past max_voting_time governance_test - .advance_clock_by_min_timespan(governance_cookie.account.config.max_voting_time as u64) + .advance_clock_by_min_timespan(governance_cookie.account.config.voting_base_time as u64) .await; governance_test @@ -1053,7 +1053,7 @@ async fn test_create_proposal_with_10_options_and_cast_vote() { governance_test .advance_clock_past_timestamp( - governance_cookie.account.config.max_voting_time as i64 + clock.unix_timestamp, + governance_cookie.account.config.voting_base_time as i64 + clock.unix_timestamp, ) .await; diff --git a/governance/program/tests/use_veto_vote.rs b/governance/program/tests/use_veto_vote.rs index 05257b77f58..6a242bd0ecf 100644 --- a/governance/program/tests/use_veto_vote.rs +++ b/governance/program/tests/use_veto_vote.rs @@ -962,3 +962,66 @@ async fn test_veto_vote_with_community_max_voter_weight_addin_and_veto_not_tippe assert_eq!(proposal_account.state, ProposalState::Voting); } + +#[tokio::test] +async fn test_cast_council_veto_vote_within_cool_off_time() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Mint extra council tokens for total supply of 120 + governance_test.mint_council_tokens(&realm_cookie, 20).await; + + // Set none default voting cool off time + let mut governance_config = governance_test.get_default_governance_config(); + governance_config.voting_cool_off_time = 50; + + let mut governance_cookie = governance_test + .with_governance_using_config( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + &governance_config, + ) + .await + .unwrap(); + + let proposal_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let proposal_cookie = governance_test + .with_signed_off_proposal(&proposal_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + // Advance timestamp into voting_cool_off_time + let clock = governance_test.bench.get_clock().await; + + governance_test + .advance_clock_past_timestamp( + clock.unix_timestamp + governance_cookie.account.config.voting_base_time as i64, + ) + .await; + + // Act + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Veto) + .await + .unwrap(); + + // Assert + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(proposal_account.state, ProposalState::Vetoed); +}