From 8afc7d10275b4f563781e948c0e7fd6a214bf0c1 Mon Sep 17 00:00:00 2001 From: Ondra Chaloupka Date: Mon, 26 Sep 2022 14:44:34 +0200 Subject: [PATCH] Governance: Adding weighted choice for the VoteType --- governance/program/src/processor/mod.rs | 26 + governance/program/src/state/proposal.rs | 314 +++++++- governance/program/src/state/vote_record.rs | 58 +- .../use_proposals_with_multiple_options.rs | 675 ++++++++++++++++++ 4 files changed, 1067 insertions(+), 6 deletions(-) diff --git a/governance/program/src/processor/mod.rs b/governance/program/src/processor/mod.rs index a4baf1f9520..0cf93f8f0fa 100644 --- a/governance/program/src/processor/mod.rs +++ b/governance/program/src/processor/mod.rs @@ -29,6 +29,7 @@ mod process_update_program_metadata; mod process_withdraw_governing_tokens; use crate::instruction::GovernanceInstruction; +use crate::state::vote_record::Vote; use process_add_signatory::*; use process_cancel_proposal::*; @@ -88,6 +89,31 @@ pub fn process_instruction( index, hold_up_time ); + } else if let GovernanceInstruction::CreateProposal { + name, + description_link, + vote_type, + options, + use_deny_option, + } = &instruction + { + // Do not iterate through options + msg!("GOVERNANCE-INSTRUCTION: CreateProposal {{name: {:?}, description_link: {:?}, vote_type: {:?}, use_deny_option: {:?}, number of options: {} }}", + name, + description_link, + vote_type, + use_deny_option, + options.len() + ); + } else if let GovernanceInstruction::CastVote { + vote: Vote::Approve(v), + } = &instruction + { + // Do not iterate through options + msg!( + "GOVERNANCE-INSTRUCTION: CastVote {{Vote::Approve (number of options: {:?}) }}", + v.len() + ); } else { msg!("GOVERNANCE-INSTRUCTION: {:?}", instruction); } diff --git a/governance/program/src/state/proposal.rs b/governance/program/src/state/proposal.rs index 0230a3a78c3..efe81b6df50 100644 --- a/governance/program/src/state/proposal.rs +++ b/governance/program/src/state/proposal.rs @@ -101,6 +101,18 @@ pub enum VoteType { #[allow(dead_code)] max_winning_options: u8, }, + + /// The multi weighted choice behaves the same way as the MultiChoice + /// while it considers the weight_percentage defined in the VoteChoice + MultiWeightedChoice { + /// The max number of options a voter can choose; see MultiChoice + #[allow(dead_code)] + max_voter_options: u8, + + /// The max number of wining options; see MultiChoice + #[allow(dead_code)] + max_winning_options: u8, + }, } /// Governance Proposal @@ -403,6 +415,10 @@ impl ProposalV2 { VoteType::MultiChoice { max_voter_options: _n, max_winning_options: _m, + } + | VoteType::MultiWeightedChoice { + max_voter_options: _n, + max_winning_options: _m, } => { // If any option succeeded for multi choice then the proposal as a whole succeeded as well ProposalState::Succeeded @@ -760,13 +776,24 @@ impl ProposalV2 { } let mut choice_count = 0u16; + let mut choice_weight_percentage_sum = 0u8; for choice in choices { if choice.rank > 0 { return Err(GovernanceError::InvalidVote.into()); } - if choice.weight_percentage == 100 { + if let VoteType::MultiWeightedChoice { + max_voter_options: _m, + max_winning_options: _n, + } = self.vote_type + { + // the sum of the percent numbers of all choices cannot be over 100% + choice_weight_percentage_sum = choice_weight_percentage_sum + .checked_add(choice.weight_percentage) + .unwrap(); + choice_count = choice_count.checked_add(1).unwrap(); + } else if choice.weight_percentage == 100 { choice_count = choice_count.checked_add(1).unwrap(); } else if choice.weight_percentage != 0 { return Err(GovernanceError::InvalidVote.into()); @@ -787,6 +814,17 @@ impl ProposalV2 { return Err(GovernanceError::InvalidVote.into()); } } + VoteType::MultiWeightedChoice { + max_voter_options: _n, + max_winning_options: _m, + } => { + if choice_count == 0 { + return Err(GovernanceError::InvalidVote.into()); + } + if choice_weight_percentage_sum != 100 { + return Err(GovernanceError::InvalidVote.into()); + } + } } } Vote::Deny => { @@ -1022,13 +1060,17 @@ pub fn assert_valid_proposal_options( options: &[String], vote_type: &VoteType, ) -> Result<(), ProgramError> { - if options.is_empty() || options.len() > 10 { + if options.is_empty() { return Err(GovernanceError::InvalidProposalOptions.into()); } if let VoteType::MultiChoice { max_voter_options, max_winning_options, + } + | VoteType::MultiWeightedChoice { + max_voter_options, + max_winning_options, } = *vote_type { if options.len() == 1 @@ -2478,6 +2520,274 @@ mod test { assert_eq!(result, Err(GovernanceError::InvalidProposalOptions.into())); } + #[test] + pub fn test_assert_valid_vote_for_multi_weighted_choice() { + // Multi weighted choice may be weighted but sum of choices has to be 100% + // Arrange + let mut proposal = create_test_multi_option_proposal(); + proposal.vote_type = VoteType::MultiWeightedChoice { + max_voter_options: 3, + max_winning_options: 3, + }; + + let choices = vec![ + VoteChoice { + rank: 0, + weight_percentage: 42, + }, + VoteChoice { + rank: 0, + weight_percentage: 42, + }, + VoteChoice { + rank: 0, + weight_percentage: 16, + }, + ]; + let vote = Vote::Approve(choices.clone()); + + // Ensure + assert_eq!(proposal.options.len(), choices.len()); + + // Act + let result = proposal.assert_valid_vote(&vote); + + // Assert + assert_eq!(result, Ok(())); + } + + #[test] + pub fn test_assert_valid_full_vote_for_multi_weighted_choice() { + // Multi weighted choice may be weighted to 100% and 0% rest + // Arrange + let mut proposal = create_test_multi_option_proposal(); + proposal.vote_type = VoteType::MultiWeightedChoice { + max_voter_options: 3, + max_winning_options: 3, + }; + + let choices = vec![ + VoteChoice { + rank: 0, + weight_percentage: 0, + }, + VoteChoice { + rank: 0, + weight_percentage: 100, + }, + VoteChoice { + rank: 0, + weight_percentage: 0, + }, + ]; + let vote = Vote::Approve(choices.clone()); + + // Ensure + assert_eq!(proposal.options.len(), choices.len()); + + // Act + let result = proposal.assert_valid_vote(&vote); + + // Assert + assert_eq!(result, Ok(())); + } + + #[test] + pub fn test_assert_valid_vote_with_no_choices_for_multi_weighted_choice_error() { + // Arrange + let mut proposal = create_test_multi_option_proposal(); + proposal.vote_type = VoteType::MultiWeightedChoice { + max_voter_options: 2, + max_winning_options: 2, + }; + + let choices = vec![ + VoteChoice { + rank: 0, + weight_percentage: 34, + }, + VoteChoice { + rank: 0, + weight_percentage: 34, + }, + VoteChoice { + rank: 0, + weight_percentage: 34, + }, + ]; + let vote = Vote::Approve(choices.clone()); + + // Ensure + assert_eq!(proposal.options.len(), choices.len()); + + // Act + let result = proposal.assert_valid_vote(&vote); + + // Assert + assert_eq!(result, Err(GovernanceError::InvalidVote.into())); + } + + #[test] + pub fn test_assert_valid_vote_with_over_percentage_for_multi_weighted_choice_error() { + // Multi weighted choice does not permit vote with sum weight over 100% + // Arrange + let mut proposal = create_test_multi_option_proposal(); + proposal.vote_type = VoteType::MultiWeightedChoice { + max_voter_options: 3, + max_winning_options: 3, + }; + + let choices = vec![ + VoteChoice { + rank: 0, + weight_percentage: 34, + }, + VoteChoice { + rank: 0, + weight_percentage: 34, + }, + VoteChoice { + rank: 0, + weight_percentage: 34, + }, + ]; + let vote = Vote::Approve(choices.clone()); + + // Ensure + assert_eq!(proposal.options.len(), choices.len()); + + // Act + let result = proposal.assert_valid_vote(&vote); + + // Assert + assert_eq!(result, Err(GovernanceError::InvalidVote.into())); + } + + #[test] + pub fn test_assert_valid_proposal_options_with_invalid_choice_number_for_multi_weighted_choice_vote_error( + ) { + // Arrange + let vote_type = VoteType::MultiWeightedChoice { + max_voter_options: 3, + max_winning_options: 3, + }; + + let options = vec!["option 1".to_string(), "option 2".to_string()]; + + // Act + let result = assert_valid_proposal_options(&options, &vote_type); + + // Assert + assert_eq!(result, Err(GovernanceError::InvalidProposalOptions.into())); + } + + #[test] + pub fn test_assert_valid_proposal_options_with_no_options_for_multi_weighted_choice_vote_error() + { + // Arrange + let vote_type = VoteType::MultiWeightedChoice { + max_voter_options: 3, + max_winning_options: 3, + }; + + let options = vec![]; + + // Act + let result = assert_valid_proposal_options(&options, &vote_type); + + // Assert + assert_eq!(result, Err(GovernanceError::InvalidProposalOptions.into())); + } + + #[test] + pub fn test_assert_valid_proposal_options_for_multi_weighted_choice_vote() { + // Arrange + let vote_type = VoteType::MultiWeightedChoice { + max_voter_options: 3, + max_winning_options: 3, + }; + + let options = vec![ + "option 1".to_string(), + "option 2".to_string(), + "option 3".to_string(), + ]; + + // Act + let result = assert_valid_proposal_options(&options, &vote_type); + + // Assert + assert_eq!(result, Ok(())); + } + + #[test] + pub fn test_assert_valid_proposal_options_for_multi_weighted_choice_vote_with_empty_option_error( + ) { + // Arrange + let vote_type = VoteType::MultiWeightedChoice { + max_voter_options: 3, + max_winning_options: 3, + }; + + let options = vec![ + "".to_string(), + "option 2".to_string(), + "option 3".to_string(), + ]; + + // Act + let result = assert_valid_proposal_options(&options, &vote_type); + + // Assert + assert_eq!(result, Err(GovernanceError::InvalidProposalOptions.into())); + } + + #[test] + pub fn test_assert_more_than_ten_proposal_options_for_multi_weighted_choice_error() { + // Arrange + let vote_type = VoteType::MultiWeightedChoice { + max_voter_options: 3, + max_winning_options: 3, + }; + + let options = vec![ + "option 1".to_string(), + "option 2".to_string(), + "option 3".to_string(), + "option 4".to_string(), + "option 5".to_string(), + "option 6".to_string(), + "option 7".to_string(), + "option 8".to_string(), + "option 9".to_string(), + "option 10".to_string(), + "option 11".to_string(), + ]; + + // Act + let result = assert_valid_proposal_options(&options, &vote_type); + + // Assert + assert_eq!(result, Err(GovernanceError::InvalidProposalOptions.into())); + } + + #[test] + pub fn test_assert_same_label_options_for_multi_weighted_choice_error() { + // Arrange + let vote_type = VoteType::MultiWeightedChoice { + max_voter_options: 1, + max_winning_options: 1, + }; + + let options = vec!["option 1".to_string(), "option 1".to_string()]; + + // Act + let result = assert_valid_proposal_options(&options, &vote_type); + + // Assert + assert_eq!(result, Err(GovernanceError::InvalidProposalOptions.into())); + } + #[test] fn test_proposal_v1_to_v2_serialisation_roundtrip() { // Arrange diff --git a/governance/program/src/state/vote_record.rs b/governance/program/src/state/vote_record.rs index 2bd0656791d..6a39dbc71bc 100644 --- a/governance/program/src/state/vote_record.rs +++ b/governance/program/src/state/vote_record.rs @@ -23,8 +23,8 @@ use crate::state::{ }; /// Voter choice for a proposal option -/// In the current version only 1) Single choice and 2) Multiple choices proposals are supported -/// In the future versions we can add support for 1) Quadratic voting, 2) Ranked choice voting and 3) Weighted voting +/// In the current version only 1) Single choice, 2) Multiple choices proposals are supported and 3) Weighted voting +/// In the future versions we can add support for 1) Quadratic voting and 2) Ranked choice voting #[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct VoteChoice { /// The rank given to the choice by voter @@ -39,8 +39,7 @@ impl VoteChoice { /// Returns the choice weight given the voter's weight pub fn get_choice_weight(&self, voter_weight: u64) -> Result { Ok(match self.weight_percentage { - 100 => voter_weight, - 0 => 0, + 0..=100 => (voter_weight as u128 * self.weight_percentage as u128 / 100_u128) as u64, _ => return Err(GovernanceError::InvalidVoteChoiceWeightPercentage.into()), }) } @@ -311,4 +310,55 @@ mod test { assert_eq!(vote_record_v1_source, vote_record_v1_target) } + + #[test] + fn test_vote_record_error() { + let vote_choice = VoteChoice { + rank: 0, + weight_percentage: 127, + }; + let result = vote_choice.get_choice_weight(100); + assert_eq!( + Err(GovernanceError::InvalidVoteChoiceWeightPercentage.into()), + result + ); + } + + #[test] + fn test_vote_record() { + let vote_choice = VoteChoice { + rank: 0, + weight_percentage: 100, + }; + let result = vote_choice.get_choice_weight(100); + assert_eq!(Ok(100_u64), result); + + let vote_choice = VoteChoice { + rank: 0, + weight_percentage: 0, + }; + let result = vote_choice.get_choice_weight(100); + assert_eq!(Ok(0_u64), result); + + let vote_choice = VoteChoice { + rank: 0, + weight_percentage: 33, + }; + let result = vote_choice.get_choice_weight(100); + assert_eq!(Ok(33_u64), result); + + let vote_choice = VoteChoice { + rank: 0, + weight_percentage: 34, + }; + let result = vote_choice.get_choice_weight(100); + assert_eq!(Ok(34_u64), result); + + let vote_choice = VoteChoice { + rank: 0, + weight_percentage: 50, + }; + let result = vote_choice.get_choice_weight(u64::MAX); + assert_eq!(Ok(u64::MAX / 2), result); + } } diff --git a/governance/program/tests/use_proposals_with_multiple_options.rs b/governance/program/tests/use_proposals_with_multiple_options.rs index de05ebb4db8..1b23df27ee2 100644 --- a/governance/program/tests/use_proposals_with_multiple_options.rs +++ b/governance/program/tests/use_proposals_with_multiple_options.rs @@ -1079,3 +1079,678 @@ async fn test_create_proposal_with_10_options_and_cast_vote() { assert_eq!(ProposalState::Completed, proposal_account.state); } + +#[tokio::test] +async fn test_vote_multi_weighted_choice_proposal_non_executable() { + // 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_config = governance_test.get_default_governance_config(); + governance_config.community_vote_threshold = VoteThreshold::YesVotePercentage(30); + + 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_multi_option_proposal( + &token_owner_record_cookie, + &mut governance_cookie, + vec![ + "option 1".to_string(), + "option 2".to_string(), + "option 3".to_string(), + "option 4".to_string(), + ], + false, + VoteType::MultiWeightedChoice { + max_winning_options: 4, + max_voter_options: 4, + }, + ) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + let clock = governance_test.bench.get_clock().await; + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + let vote = Vote::Approve(vec![ + VoteChoice { + rank: 0, + weight_percentage: 30, + }, + VoteChoice { + rank: 0, + weight_percentage: 29, + }, + VoteChoice { + rank: 0, + weight_percentage: 41, + }, + VoteChoice { + rank: 0, + weight_percentage: 0, + }, + ]); + + // Act + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, vote) + .await + .unwrap(); + + // Advance timestamp past max_voting_time + governance_test + .advance_clock_past_timestamp( + governance_cookie.account.config.max_voting_time as i64 + clock.unix_timestamp, + ) + .await; + + governance_test + .finalize_vote(&realm_cookie, &proposal_cookie, None) + .await + .unwrap(); + + // Assert + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!( + OptionVoteResult::Succeeded, + proposal_account.options[0].vote_result + ); + assert_eq!( + OptionVoteResult::Defeated, + proposal_account.options[1].vote_result + ); + assert_eq!( + OptionVoteResult::Succeeded, + proposal_account.options[2].vote_result + ); + assert_eq!( + OptionVoteResult::Defeated, + proposal_account.options[3].vote_result + ); + assert_eq!( + (token_owner_record_cookie.token_source_amount as f32 * 0.3) as u64, + proposal_account.options[0].vote_weight + ); + assert_eq!( + (token_owner_record_cookie.token_source_amount as f32 * 0.29) as u64, + proposal_account.options[1].vote_weight + ); + assert_eq!( + (token_owner_record_cookie.token_source_amount as f32 * 0.41) as u64, + proposal_account.options[2].vote_weight + ); + assert_eq!(0_u64, proposal_account.options[3].vote_weight); + + // None executable proposal transitions to Completed when vote is finalized + assert_eq!(ProposalState::Completed, proposal_account.state); +} + +#[tokio::test] +async fn test_vote_multi_weighted_choice_proposal_with_partial_success() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_mint_cookie = governance_test.with_governed_mint().await; + + // 100 tokens each, sum 300 tokens + let token_owner_record_cookie1 = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + let token_owner_record_cookie2 = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + let token_owner_record_cookie3 = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + // 60 tokes approval quorum as 20% of 300 is 60 + let mut governance_config = governance_test.get_default_governance_config(); + governance_config.community_vote_threshold = VoteThreshold::YesVotePercentage(20); + + let mut governance_cookie = governance_test + .with_mint_governance_using_config( + &realm_cookie, + &governed_mint_cookie, + &token_owner_record_cookie1, + &governance_config, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_multi_option_proposal( + &token_owner_record_cookie1, + &mut governance_cookie, + vec![ + "option 1".to_string(), + "option 2".to_string(), + "option 3".to_string(), + "option 4".to_string(), + ], + true, + VoteType::MultiWeightedChoice { + max_winning_options: 4, + max_voter_options: 4, + }, + ) + .await + .unwrap(); + + let proposal_transaction_cookie1 = governance_test + .with_mint_tokens_transaction( + &governed_mint_cookie, + &mut proposal_cookie, + &token_owner_record_cookie1, + 0, + Some(0), + None, + ) + .await + .unwrap(); + let proposal_transaction_cookie2 = governance_test + .with_mint_tokens_transaction( + &governed_mint_cookie, + &mut proposal_cookie, + &token_owner_record_cookie1, + 1, + Some(0), + None, + ) + .await + .unwrap(); + let proposal_transaction_cookie3 = governance_test + .with_mint_tokens_transaction( + &governed_mint_cookie, + &mut proposal_cookie, + &token_owner_record_cookie1, + 2, + Some(0), + None, + ) + .await + .unwrap(); + let proposal_transaction_cookie4 = governance_test + .with_mint_tokens_transaction( + &governed_mint_cookie, + &mut proposal_cookie, + &token_owner_record_cookie1, + 3, + Some(0), + None, + ) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie1) + .await + .unwrap(); + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + // vote1: + // deny: 100 + // vote2 + vote3: + // choice 1: 0 -> Defeated + // choice 2: 91 -> Defeated (91 is over 60, 20% from 300, but deny overrules) + // choice 3: 101 -> Success + // choice 4: 8 -> Defeated (below of 60) + + let vote1 = Vote::Approve(vec![ + VoteChoice { + rank: 0, + weight_percentage: 0, + }, + VoteChoice { + rank: 0, + weight_percentage: 30, + }, + VoteChoice { + rank: 0, + weight_percentage: 70, + }, + VoteChoice { + rank: 0, + weight_percentage: 0, + }, + ]); + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie1, vote1) + .await + .expect("Voting the vote 1 of owner 1 should succeed"); + + let vote2 = Vote::Approve(vec![ + VoteChoice { + rank: 0, + weight_percentage: 0, + }, + VoteChoice { + rank: 0, + weight_percentage: 61, + }, + VoteChoice { + rank: 0, + weight_percentage: 31, + }, + VoteChoice { + rank: 0, + weight_percentage: 8, + }, + ]); + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, vote2) + .await + .expect("Voting the vote 1 of owner 1 should succeed"); + + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie3, Vote::Deny) + .await + .expect("Casting deny vote of owner 3 should succeed"); + + // Advance timestamp past max_voting_time + governance_test + .advance_clock_by_min_timespan(governance_cookie.account.config.max_voting_time as u64) + .await; + governance_test + .finalize_vote(&realm_cookie, &proposal_cookie, None) + .await + .unwrap(); + // Advance timestamp past hold_up_time + governance_test + .advance_clock_by_min_timespan(proposal_transaction_cookie1.account.hold_up_time as u64) + .await; + + let mut proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(ProposalState::Succeeded, proposal_account.state); + + // Act + let transaction1_err = governance_test + .execute_proposal_transaction(&proposal_cookie, &proposal_transaction_cookie1) + .await + .expect_err("Choice 1 should fail to execute, it hasn't got enough votes"); + let transaction2_err = governance_test + .execute_proposal_transaction(&proposal_cookie, &proposal_transaction_cookie2) + .await + .expect_err("Choice 2 should fail to execute, it hasn't got enough votes"); + governance_test + .execute_proposal_transaction(&proposal_cookie, &proposal_transaction_cookie3) + .await + .expect("Choice 3 should be executed as it won the poll"); + let transaction4_err = governance_test + .execute_proposal_transaction(&proposal_cookie, &proposal_transaction_cookie4) + .await + .expect_err("Choice 4 should be executed as the winner has been executed already"); + + // Assert + proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(ProposalState::Completed, proposal_account.state); + + assert_eq!( + transaction1_err, + GovernanceError::CannotExecuteDefeatedOption.into() + ); + assert_eq!( + transaction2_err, + GovernanceError::CannotExecuteDefeatedOption.into() + ); + assert_eq!( + transaction4_err, + GovernanceError::InvalidStateCannotExecuteTransaction.into() + ); +} + +#[tokio::test] +async fn test_vote_multi_weighted_choice_proposal_with_multi_success() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_mint_cookie = governance_test.with_governed_mint().await; + + // 100 tokens each, sum 300 tokens + let token_owner_record_cookie1 = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + let token_owner_record_cookie2 = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + // 60 tokes approval quorum as 30% of 200 is 60 + let mut governance_config = governance_test.get_default_governance_config(); + governance_config.community_vote_threshold = VoteThreshold::YesVotePercentage(30); + + let mut governance_cookie = governance_test + .with_mint_governance_using_config( + &realm_cookie, + &governed_mint_cookie, + &token_owner_record_cookie1, + &governance_config, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_multi_option_proposal( + &token_owner_record_cookie1, + &mut governance_cookie, + vec![ + "option 1".to_string(), + "option 2".to_string(), + "option 3".to_string(), + ], + true, + VoteType::MultiWeightedChoice { + max_winning_options: 3, + max_voter_options: 3, + }, + ) + .await + .unwrap(); + + let proposal_transaction_cookie1 = governance_test + .with_mint_tokens_transaction( + &governed_mint_cookie, + &mut proposal_cookie, + &token_owner_record_cookie1, + 0, + Some(0), + None, + ) + .await + .unwrap(); + let proposal_transaction_cookie2 = governance_test + .with_mint_tokens_transaction( + &governed_mint_cookie, + &mut proposal_cookie, + &token_owner_record_cookie1, + 1, + Some(0), + None, + ) + .await + .unwrap(); + let proposal_transaction_cookie3 = governance_test + .with_mint_tokens_transaction( + &governed_mint_cookie, + &mut proposal_cookie, + &token_owner_record_cookie1, + 2, + Some(0), + None, + ) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie1) + .await + .unwrap(); + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + // vote1 + vote2: + // choice 1: 28 -> Defeated (below 60) + // choice 2: 105 -> Success + // choice 3: 61 -> Success + + let vote1 = Vote::Approve(vec![ + VoteChoice { + rank: 0, + weight_percentage: 14, + }, + VoteChoice { + rank: 0, + weight_percentage: 55, + }, + VoteChoice { + rank: 0, + weight_percentage: 31, + }, + ]); + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie1, vote1) + .await + .expect("Voting the vote 1 of owner 1 should succeed"); + + let vote2 = Vote::Approve(vec![ + VoteChoice { + rank: 0, + weight_percentage: 20, + }, + VoteChoice { + rank: 0, + weight_percentage: 50, + }, + VoteChoice { + rank: 0, + weight_percentage: 30, + }, + ]); + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, vote2) + .await + .expect("Voting the vote 1 of owner 1 should succeed"); + + // Advance timestamp past max_voting_time + governance_test + .advance_clock_by_min_timespan(governance_cookie.account.config.max_voting_time as u64) + .await; + governance_test + .finalize_vote(&realm_cookie, &proposal_cookie, None) + .await + .unwrap(); + // Advance timestamp past hold_up_time + governance_test + .advance_clock_by_min_timespan(proposal_transaction_cookie1.account.hold_up_time as u64) + .await; + + let mut proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(ProposalState::Succeeded, proposal_account.state); + + // Act + let transaction1_err = governance_test + .execute_proposal_transaction(&proposal_cookie, &proposal_transaction_cookie1) + .await + .expect_err("Choice 1 should fail to execute, it hasn't got enough votes"); + governance_test + .execute_proposal_transaction(&proposal_cookie, &proposal_transaction_cookie2) + .await + .expect("Choice 2 should be executed as it passed the poll"); + governance_test + .execute_proposal_transaction(&proposal_cookie, &proposal_transaction_cookie3) + .await + .expect("Choice 3 should be executed as it passed the poll"); + + // Assert + proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(ProposalState::Completed, proposal_account.state); + + assert_eq!( + transaction1_err, + GovernanceError::CannotExecuteDefeatedOption.into() + ); +} + +#[tokio::test] +async fn test_vote_multi_weighted_choice_proposal_executable_with_full_deny() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_mint_cookie = governance_test.with_governed_mint().await; + + // 100 tokens + let token_owner_record_cookie1 = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + // 100 tokens + let token_owner_record_cookie2 = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_config = governance_test.get_default_governance_config(); + governance_config.community_vote_threshold = VoteThreshold::YesVotePercentage(3); + + let mut governance_cookie = governance_test + .with_mint_governance_using_config( + &realm_cookie, + &governed_mint_cookie, + &token_owner_record_cookie1, + &governance_config, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_multi_option_proposal( + &token_owner_record_cookie1, + &mut governance_cookie, + vec!["option 1".to_string(), "option 2".to_string()], + true, + VoteType::MultiWeightedChoice { + max_winning_options: 2, + max_voter_options: 2, + }, + ) + .await + .unwrap(); + + let proposal_transaction_cookie1 = governance_test + .with_mint_tokens_transaction( + &governed_mint_cookie, + &mut proposal_cookie, + &token_owner_record_cookie1, + 0, + Some(0), + None, + ) + .await + .unwrap(); + + let proposal_transaction_cookie2 = governance_test + .with_mint_tokens_transaction( + &governed_mint_cookie, + &mut proposal_cookie, + &token_owner_record_cookie1, + 1, + Some(0), + None, + ) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie1) + .await + .unwrap(); + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie1, Vote::Deny) + .await + .expect("Casting deny vote for owner 1 should succeed"); + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, Vote::Deny) + .await + .expect("Casting deny vote for owner 1 should succeed"); + + // Advance timestamp past max_voting_time + governance_test + .advance_clock_by_min_timespan(governance_cookie.account.config.max_voting_time as u64) + .await; + + governance_test + .finalize_vote(&realm_cookie, &proposal_cookie, None) + .await + .unwrap(); + + // Advance timestamp past hold_up_time + governance_test + .advance_clock_by_min_timespan(proposal_transaction_cookie1.account.hold_up_time as u64) + .await; + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(ProposalState::Defeated, proposal_account.state); + + // Act + let transaction1_err = governance_test + .execute_proposal_transaction(&proposal_cookie, &proposal_transaction_cookie1) + .await + .expect_err("The proposal was denied, error on choice 1 execution expected"); + let transaction2_err = governance_test + .execute_proposal_transaction(&proposal_cookie, &proposal_transaction_cookie2) + .await + .expect_err("The proposal was denied, error on choice 2 execution expected"); + + // Assert + assert_eq!( + transaction1_err, + GovernanceError::InvalidStateCannotExecuteTransaction.into() + ); + assert_eq!( + transaction2_err, + GovernanceError::InvalidStateCannotExecuteTransaction.into() + ); +}