From 3d3b32dcf6b64bfe609600735d067ebe735ed36d Mon Sep 17 00:00:00 2001 From: Sebastian Bor Date: Tue, 17 May 2022 16:37:43 +0100 Subject: [PATCH] Governance: Veto vote (#3156) * chore: remove #[repr(C)] * wip: resolve proposal governing token mint * fix: remove optionality from veto vote * feat: implement tipping for veto vote * fix: move vote_threshold to the end of Proposal struct * chore: remove use super * chore: make clippy happy * chore: add change log * feat: add council_veto_vote_threshold * fix: resolve vote threshold for voting token mint * chore: revert old function name * fix: calculate max veto in coerce_max_voter_weight * chore: make clippy happy * chore: make clippy happy * feat: Implement RelinquishVote for Veto * chore: update comments * chore: rename with_cast_vote to with_cast_yes_no_vote * chore: rename with_cast_multi_option_vote to with_cast_vote * chore: create use_veto_vote test scenario * chore: Add veto vote disabled tests * chore: Add partial Veto vote tests * chore: update comments * chore: test_cast_veto_vote_with_no_council_error * chore: test_relinquish_veto_vote * chore: rename with_token_owner_record to with_community_token_owner_record * chore: test_relinquish_veto_vote_with_vote_record_for_different_voting_mint_error * chore: test_cast_veto_vote_with_invalid_voting_mint_error * chore: fix chat build * chore: test_cast_veto_vote_with_council_only_allowed_to_veto * fix: Use VoteKind to distinguish between Veto and Electorate votes * chore: make clippy happy * Update change log Co-authored-by: Jon Cinque * chore: rename voting_token_mint to vote_governing_token_mint * chore: test_cast_yes_and_veto_votes_with_yes_as_winning_vote * fix: throw error for Community veto * chore: Update comments * chore: Update names and commnents * chore: split try_get_tipped_vote_state into Electorate and Veto cases * chore: Update comments * chore: remove #[allow(clippy::float_cmp)] Co-authored-by: Jon Cinque --- governance/CHANGELOG.md | 34 + governance/chat/program/src/state.rs | 1 - .../chat/program/tests/program_test/mod.rs | 2 +- governance/program/src/error.rs | 50 +- governance/program/src/instruction.rs | 37 +- .../src/processor/process_cast_vote.rs | 42 +- .../src/processor/process_create_proposal.rs | 11 +- .../src/processor/process_finalize_vote.rs | 10 +- .../src/processor/process_relinquish_vote.rs | 49 +- governance/program/src/state/enums.rs | 10 +- governance/program/src/state/governance.rs | 79 +- governance/program/src/state/legacy.rs | 4 - .../program/src/state/program_metadata.rs | 1 - governance/program/src/state/proposal.rs | 286 +++++-- .../program/src/state/proposal_transaction.rs | 3 - governance/program/src/state/realm.rs | 35 +- .../program/src/state/signatory_record.rs | 5 +- .../program/src/state/token_owner_record.rs | 1 - governance/program/src/state/vote_record.rs | 45 +- .../program/tests/process_cancel_proposal.rs | 2 +- governance/program/tests/process_cast_vote.rs | 74 +- .../tests/process_create_native_treasury.rs | 2 +- .../process_create_token_owner_record.rs | 4 +- .../tests/process_execute_transaction.rs | 12 +- .../program/tests/process_finalize_vote.rs | 10 +- .../tests/process_flag_transaction_error.rs | 10 +- .../program/tests/process_relinquish_vote.rs | 28 +- .../tests/process_set_governance_config.rs | 4 +- .../process_withdraw_governing_tokens.rs | 4 +- governance/program/tests/program_test/args.rs | 21 +- governance/program/tests/program_test/mod.rs | 42 +- .../use_proposals_with_multiple_options.rs | 22 +- .../tests/use_realm_with_all_addins.rs | 21 +- .../use_realm_with_max_voter_weight_addin.rs | 12 +- .../use_realm_with_voter_weight_addin.rs | 64 +- governance/program/tests/use_veto_vote.rs | 719 ++++++++++++++++++ 36 files changed, 1406 insertions(+), 350 deletions(-) create mode 100644 governance/CHANGELOG.md create mode 100644 governance/program/tests/use_veto_vote.rs diff --git a/governance/CHANGELOG.md b/governance/CHANGELOG.md new file mode 100644 index 00000000..50ea5951 --- /dev/null +++ b/governance/CHANGELOG.md @@ -0,0 +1,34 @@ +# SPL Governance Changelog + +## v3.0.0 - development + +- Support separate vote threshold for `Council` +- `Council` Veto vote + +## v2.2.4 - 24 Mar 2022 + +- Support Anchor native account discriminators for `MaxVoterWeightRecord` and `VoterWeightRecord` + +## v2.2.3 - 09 Feb 2022 + +- Fix serialisation of multiple instructions within a single proposal transaction + +## v2.2.2 - 07 Feb 2022 + +- Native SOL Treasuries +- Multi choice and survey style proposals +- `voter_weight` and `max_voter_weight` addins +- Multiple instructions per proposal transaction +- Configurable tipping point (`Strict`, `Early`, `Disabled`) +- Owner signed off proposals +- `realm_authority` can create governances +- Program metadata and version detection +- Custom deposit amount for governance tokens + +## v1.1.1 - 23 Sep 2021 + +- Constrain number of outstanding proposals per token owner to 10 at a time + +## v1.0.8 - 1 Aug 2021 + +- First release diff --git a/governance/chat/program/src/state.rs b/governance/chat/program/src/state.rs index 25c435e2..9379271b 100644 --- a/governance/chat/program/src/state.rs +++ b/governance/chat/program/src/state.rs @@ -8,7 +8,6 @@ use solana_program::{ use spl_governance_tools::account::{assert_is_valid_account_of_type, AccountMaxSize}; /// Defines all GovernanceChat accounts types -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum GovernanceChatAccountType { /// Default uninitialized account state diff --git a/governance/chat/program/tests/program_test/mod.rs b/governance/chat/program/tests/program_test/mod.rs index 3d574350..29b7039e 100644 --- a/governance/chat/program/tests/program_test/mod.rs +++ b/governance/chat/program/tests/program_test/mod.rs @@ -186,7 +186,7 @@ impl GovernanceChatProgramTest { community_vote_threshold: VoteThreshold::YesVotePercentage(60), vote_tipping: spl_governance::state::enums::VoteTipping::Strict, council_vote_threshold: VoteThreshold::YesVotePercentage(10), - reserved: [0; 2], + council_veto_vote_threshold: VoteThreshold::YesVotePercentage(50), }; 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 66dddf9b..fb5146ca 100644 --- a/governance/program/src/error.rs +++ b/governance/program/src/error.rs @@ -25,7 +25,7 @@ pub enum GovernanceError { /// Invalid Governing Token Mint #[error("Invalid Governing Token Mint")] - InvalidGoverningTokenMint, + InvalidGoverningTokenMint, // 503 /// Governing Token Owner must sign transaction #[error("Governing Token Owner must sign transaction")] @@ -45,11 +45,11 @@ pub enum GovernanceError { /// Invalid GoverningMint for TokenOwnerRecord #[error("Invalid GoverningMint for TokenOwnerRecord")] - InvalidGoverningMintForTokenOwnerRecord, + InvalidGoverningMintForTokenOwnerRecord, // 508 /// Invalid Realm for TokenOwnerRecord #[error("Invalid Realm for TokenOwnerRecord")] - InvalidRealmForTokenOwnerRecord, + InvalidRealmForTokenOwnerRecord, // 509 /// Invalid Proposal for ProposalTransaction, #[error("Invalid Proposal for ProposalTransaction,")] @@ -164,95 +164,95 @@ pub enum GovernanceError { /// Proposal does not belong to the given Governance #[error("Proposal does not belong to the given Governance")] - InvalidGovernanceForProposal, + InvalidGovernanceForProposal, // 538 /// Proposal does not belong to given Governing Mint" #[error("Proposal does not belong to given Governing Mint")] - InvalidGoverningMintForProposal, + InvalidGoverningMintForProposal, // 539 /// Current mint authority must sign transaction #[error("Current mint authority must sign transaction")] - MintAuthorityMustSign, + MintAuthorityMustSign, // 540 /// Invalid mint authority #[error("Invalid mint authority")] - InvalidMintAuthority, + InvalidMintAuthority, // 542 /// Mint has no authority #[error("Mint has no authority")] - MintHasNoAuthority, + MintHasNoAuthority, // 542 /// ---- SPL Token Tools Errors ---- /// Invalid Token account owner #[error("Invalid Token account owner")] - SplTokenAccountWithInvalidOwner, + SplTokenAccountWithInvalidOwner, // 543 /// Invalid Mint account owner #[error("Invalid Mint account owner")] - SplTokenMintWithInvalidOwner, + SplTokenMintWithInvalidOwner, // 544 /// Token Account is not initialized #[error("Token Account is not initialized")] - SplTokenAccountNotInitialized, + SplTokenAccountNotInitialized, // 545 /// Token Account doesn't exist #[error("Token Account doesn't exist")] - SplTokenAccountDoesNotExist, + SplTokenAccountDoesNotExist, // 546 /// Token account data is invalid #[error("Token account data is invalid")] - SplTokenInvalidTokenAccountData, + SplTokenInvalidTokenAccountData, // 547 /// Token mint account data is invalid #[error("Token mint account data is invalid")] - SplTokenInvalidMintAccountData, + SplTokenInvalidMintAccountData, // 548 /// Token Mint is not initialized #[error("Token Mint account is not initialized")] - SplTokenMintNotInitialized, + SplTokenMintNotInitialized, // 549 /// Token Mint account doesn't exist #[error("Token Mint account doesn't exist")] - SplTokenMintDoesNotExist, + SplTokenMintDoesNotExist, // 550 /// ---- Bpf Upgradable Loader Tools Errors ---- /// Invalid ProgramData account Address #[error("Invalid ProgramData account address")] - InvalidProgramDataAccountAddress, + InvalidProgramDataAccountAddress, // 551 /// Invalid ProgramData account data #[error("Invalid ProgramData account Data")] - InvalidProgramDataAccountData, + InvalidProgramDataAccountData, // 552 /// Provided upgrade authority doesn't match current program upgrade authority #[error("Provided upgrade authority doesn't match current program upgrade authority")] - InvalidUpgradeAuthority, + InvalidUpgradeAuthority, // 553 /// Current program upgrade authority must sign transaction #[error("Current program upgrade authority must sign transaction")] - UpgradeAuthorityMustSign, + UpgradeAuthorityMustSign, // 554 /// Given program is not upgradable #[error("Given program is not upgradable")] - ProgramNotUpgradable, + ProgramNotUpgradable, // 555 /// Invalid token owner #[error("Invalid token owner")] - InvalidTokenOwner, + InvalidTokenOwner, // 556 /// Current token owner must sign transaction #[error("Current token owner must sign transaction")] - TokenOwnerMustSign, + TokenOwnerMustSign, // 557 /// Given VoteThresholdType is not supported #[error("Given VoteThresholdType is not supported")] - VoteThresholdTypeNotSupported, + VoteThresholdTypeNotSupported, // 558 /// Given VoteWeightSource is not supported #[error("Given VoteWeightSource is not supported")] - VoteWeightSourceNotSupported, + VoteWeightSourceNotSupported, // 559 /// GoverningTokenMint not allowed to vote #[error("GoverningTokenMint not allowed to vote")] diff --git a/governance/program/src/instruction.rs b/governance/program/src/instruction.rs index 8c5753b4..72bd717e 100644 --- a/governance/program/src/instruction.rs +++ b/governance/program/src/instruction.rs @@ -30,7 +30,6 @@ use solana_program::{ /// Instructions supported by the Governance program #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -#[repr(C)] #[allow(clippy::large_enum_variant)] pub enum GovernanceInstruction { /// Creates Governance Realm account which aggregates governances for given Community Mint and optional Council Mint @@ -286,10 +285,14 @@ pub enum GovernanceInstruction { /// 1. `[writable]` Governance account /// 2. `[writable]` Proposal account /// 3. `[writable]` TokenOwnerRecord of the Proposal owner - /// 4. `[writable]` TokenOwnerRecord of the voter. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] + /// 4. `[writable]` TokenOwnerRecord of the voter. PDA seeds: ['governance',realm, vote_governing_token_mint, governing_token_owner] /// 5. `[signer]` Governance Authority (Token Owner or Governance Delegate) - /// 6. `[writable]` Proposal VoteRecord account. PDA seeds: ['governance',proposal,governing_token_owner_record] - /// 7. `[]` Governing Token Mint + /// 6. `[writable]` Proposal VoteRecord account. PDA seeds: ['governance',proposal,token_owner_record] + /// 7. `[]` The Governing Token Mint which is used to cast the vote (vote_governing_token_mint) + /// The voting token mint is the governing_token_mint of the Proposal for Approve, Deny and Abstain votes + /// For Veto vote the voting token mint is the mint of the opposite voting population + /// Council mint to veto Community proposals and Community mint to veto Council proposals + /// Note: In the current version only Council veto is supported /// 8. `[signer]` Payer /// 9. `[]` System program /// 10. `[]` Realm Config @@ -317,14 +320,15 @@ pub enum GovernanceInstruction { /// If the Proposal is already in decided state then the instruction has no impact on the Proposal /// and only allows voters to prune their outstanding votes in case they wanted to withdraw Governing tokens from the Realm /// - /// 0. `[]` Governance account - /// 1. `[writable]` Proposal account - /// 2. `[writable]` TokenOwnerRecord account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] - /// 3. `[writable]` Proposal VoteRecord account. PDA seeds: ['governance',proposal,governing_token_owner_record] - /// 4. `[]` Governing Token Mint - /// 5. `[signer]` Optional Governance Authority (Token Owner or Governance Delegate) + /// 0. `[]` Realm account + /// 1. `[]` Governance account + /// 2. `[writable]` Proposal account + /// 3. `[writable]` TokenOwnerRecord account. PDA seeds: ['governance',realm, vote_governing_token_mint, governing_token_owner] + /// 4. `[writable]` Proposal VoteRecord account. PDA seeds: ['governance',proposal, token_owner_record] + /// 5. `[]` The Governing Token Mint which was used to cast the vote (vote_governing_token_mint) + /// 6. `[signer]` Optional Governance Authority (Token Owner or Governance Delegate) /// It's required only when Proposal is still being voted on - /// 6. `[writable]` Optional Beneficiary account which would receive lamports when VoteRecord Account is disposed + /// 7. `[writable]` Optional Beneficiary account which would receive lamports when VoteRecord Account is disposed /// It's required only when Proposal is still being voted on RelinquishVote, @@ -1020,7 +1024,7 @@ pub fn cast_vote( proposal_owner_record: &Pubkey, voter_token_owner_record: &Pubkey, governance_authority: &Pubkey, - governing_token_mint: &Pubkey, + vote_governing_token_mint: &Pubkey, payer: &Pubkey, voter_weight_record: Option, max_voter_weight_record: Option, @@ -1038,7 +1042,7 @@ pub fn cast_vote( AccountMeta::new(*voter_token_owner_record, false), AccountMeta::new_readonly(*governance_authority, true), AccountMeta::new(vote_record_address, false), - AccountMeta::new_readonly(*governing_token_mint, false), + AccountMeta::new_readonly(*vote_governing_token_mint, false), AccountMeta::new(*payer, true), AccountMeta::new_readonly(system_program::id(), false), ]; @@ -1097,24 +1101,27 @@ pub fn finalize_vote( } /// Creates RelinquishVote instruction +#[allow(clippy::too_many_arguments)] pub fn relinquish_vote( program_id: &Pubkey, // Accounts + realm: &Pubkey, governance: &Pubkey, proposal: &Pubkey, token_owner_record: &Pubkey, - governing_token_mint: &Pubkey, + vote_governing_token_mint: &Pubkey, governance_authority: Option, beneficiary: Option, ) -> Instruction { let vote_record_address = get_vote_record_address(program_id, proposal, token_owner_record); let mut accounts = vec![ + AccountMeta::new_readonly(*realm, false), AccountMeta::new_readonly(*governance, false), AccountMeta::new(*proposal, false), AccountMeta::new(*token_owner_record, false), AccountMeta::new(vote_record_address, false), - AccountMeta::new_readonly(*governing_token_mint, false), + AccountMeta::new_readonly(*vote_governing_token_mint, false), ]; if let Some(governance_authority) = governance_authority { diff --git a/governance/program/src/processor/process_cast_vote.rs b/governance/program/src/processor/process_cast_vote.rs index 346397f9..aec2e1e8 100644 --- a/governance/program/src/processor/process_cast_vote.rs +++ b/governance/program/src/processor/process_cast_vote.rs @@ -22,7 +22,7 @@ use crate::{ get_token_owner_record_data_for_proposal_owner, get_token_owner_record_data_for_realm_and_governing_mint, }, - vote_record::{get_vote_record_address_seeds, Vote, VoteRecordV2}, + vote_record::{get_vote_kind, get_vote_record_address_seeds, Vote, VoteRecordV2}, }, }; @@ -44,7 +44,7 @@ pub fn process_cast_vote( let governance_authority_info = next_account_info(account_info_iter)?; // 5 let vote_record_info = next_account_info(account_info_iter)?; // 6 - let governing_token_mint_info = next_account_info(account_info_iter)?; // 7 + let vote_governing_token_mint_info = next_account_info(account_info_iter)?; // 7 let payer_info = next_account_info(account_info_iter)?; // 8 let system_info = next_account_info(account_info_iter)?; // 9 @@ -59,16 +59,27 @@ pub fn process_cast_vote( let mut realm_data = get_realm_data_for_governing_token_mint( program_id, realm_info, - governing_token_mint_info.key, + vote_governing_token_mint_info.key, )?; + let mut governance_data = get_governance_data_for_realm(program_id, governance_info, realm_info.key)?; + let vote_kind = get_vote_kind(&vote); + + // Get the governing_token_mint which the Proposal should be configured with as the voting population for the given vote + // For Approve, Deny and Abstain votes it's the same as vote_governing_token_mint + // For Veto it's the governing token mint of the opposite voting population + let proposal_governing_token_mint = realm_data.get_proposal_governing_token_mint_for_vote( + vote_governing_token_mint_info.key, + &vote_kind, + )?; + let mut proposal_data = get_proposal_data_for_governance_and_governing_mint( program_id, proposal_info, governance_info.key, - governing_token_mint_info.key, + &proposal_governing_token_mint, )?; proposal_data.assert_can_cast_vote(&governance_data.config, clock.unix_timestamp)?; @@ -77,7 +88,7 @@ pub fn process_cast_vote( program_id, voter_token_owner_record_info, &governance_data.realm, - governing_token_mint_info.key, + vote_governing_token_mint_info.key, )?; voter_token_owner_record_data .assert_token_owner_or_delegate_is_signer(governance_authority_info)?; @@ -129,7 +140,13 @@ pub fn process_cast_vote( .unwrap(), ) } - Vote::Abstain | Vote::Veto => { + Vote::Veto => { + proposal_data.veto_vote_weight = proposal_data + .veto_vote_weight + .checked_add(voter_weight) + .unwrap(); + } + Vote::Abstain => { return Err(GovernanceError::NotSupportedVoteType.into()); } } @@ -137,20 +154,25 @@ pub fn process_cast_vote( let max_voter_weight = proposal_data.resolve_max_voter_weight( program_id, realm_config_info, - governing_token_mint_info, + vote_governing_token_mint_info, account_info_iter, // max_voter_weight_record 11 realm_info.key, &realm_data, + &vote_kind, )?; - let vote_threshold = - governance_data.resolve_vote_threshold(&realm_data, governing_token_mint_info.key)?; + let vote_threshold = governance_data.resolve_vote_threshold( + &realm_data, + vote_governing_token_mint_info.key, + &vote_kind, + )?; if proposal_data.try_tip_vote( max_voter_weight, - &governance_data.config, + &governance_data.config.vote_tipping, clock.unix_timestamp, &vote_threshold, + &vote_kind, )? { // Deserialize proposal owner and validate it's the actual owner of the proposal let mut proposal_owner_record_data = get_token_owner_record_data_for_proposal_owner( diff --git a/governance/program/src/processor/process_create_proposal.rs b/governance/program/src/processor/process_create_proposal.rs index aad4905d..f135fba0 100644 --- a/governance/program/src/processor/process_create_proposal.rs +++ b/governance/program/src/processor/process_create_proposal.rs @@ -22,6 +22,7 @@ use crate::{ }, realm::get_realm_data_for_governing_token_mint, token_owner_record::get_token_owner_record_data_for_realm, + vote_record::VoteKind, }, }; @@ -64,8 +65,11 @@ pub fn process_create_proposal( let mut governance_data = get_governance_data_for_realm(program_id, governance_info, realm_info.key)?; - governance_data - .assert_governing_token_mint_can_vote(&realm_data, governing_token_mint_info.key)?; + governance_data.assert_governing_token_mint_can_vote( + &realm_data, + governing_token_mint_info.key, + &VoteKind::Electorate, + )?; let mut proposal_owner_record_data = get_token_owner_record_data_for_realm( program_id, @@ -146,7 +150,7 @@ pub fn process_create_proposal( options: proposal_options, deny_vote_weight, - veto_vote_weight: None, + veto_vote_weight: 0, abstain_vote_weight: None, max_vote_weight: None, @@ -154,6 +158,7 @@ pub fn process_create_proposal( vote_threshold: None, reserved: [0; 64], + reserved1: 0, }; create_and_serialize_account_signed::( diff --git a/governance/program/src/processor/process_finalize_vote.rs b/governance/program/src/processor/process_finalize_vote.rs index e29d494f..0075654c 100644 --- a/governance/program/src/processor/process_finalize_vote.rs +++ b/governance/program/src/processor/process_finalize_vote.rs @@ -12,7 +12,7 @@ use crate::state::{ governance::get_governance_data_for_realm, proposal::get_proposal_data_for_governance_and_governing_mint, realm::get_realm_data_for_governing_token_mint, - token_owner_record::get_token_owner_record_data_for_proposal_owner, + token_owner_record::get_token_owner_record_data_for_proposal_owner, vote_record::VoteKind, }; /// Processes FinalizeVote instruction @@ -52,10 +52,14 @@ pub fn process_finalize_vote(program_id: &Pubkey, accounts: &[AccountInfo]) -> P account_info_iter, // *6 realm_info.key, &realm_data, + &VoteKind::Electorate, )?; - let vote_threshold = - governance_data.resolve_vote_threshold(&realm_data, governing_token_mint_info.key)?; + let vote_threshold = governance_data.resolve_vote_threshold( + &realm_data, + governing_token_mint_info.key, + &VoteKind::Electorate, + )?; proposal_data.finalize_vote( max_voter_weight, diff --git a/governance/program/src/processor/process_relinquish_vote.rs b/governance/program/src/processor/process_relinquish_vote.rs index a4a27ec9..55d6f878 100644 --- a/governance/program/src/processor/process_relinquish_vote.rs +++ b/governance/program/src/processor/process_relinquish_vote.rs @@ -13,10 +13,11 @@ use crate::{ error::GovernanceError, state::{ enums::ProposalState, - governance::get_governance_data, - proposal::get_proposal_data_for_governance_and_governing_mint, + governance::get_governance_data_for_realm, + proposal::get_proposal_data_for_governance, + realm::get_realm_data_for_governing_token_mint, token_owner_record::get_token_owner_record_data_for_realm_and_governing_mint, - vote_record::{get_vote_record_data_for_proposal_and_token_owner, Vote}, + vote_record::{get_vote_record_data_for_proposal_and_token_owner_record, Vote}, }, }; @@ -24,34 +25,40 @@ use crate::{ pub fn process_relinquish_vote(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); - let governance_info = next_account_info(account_info_iter)?; // 0 - let proposal_info = next_account_info(account_info_iter)?; // 1 - let token_owner_record_info = next_account_info(account_info_iter)?; // 2 + let realm_info = next_account_info(account_info_iter)?; // 0 + let governance_info = next_account_info(account_info_iter)?; // 1 + let proposal_info = next_account_info(account_info_iter)?; // 2 + let token_owner_record_info = next_account_info(account_info_iter)?; // 3 - let vote_record_info = next_account_info(account_info_iter)?; // 3 - let governing_token_mint_info = next_account_info(account_info_iter)?; // 4 + let vote_record_info = next_account_info(account_info_iter)?; // 4 + let vote_governing_token_mint_info = next_account_info(account_info_iter)?; // 5 - let governance_data = get_governance_data(program_id, governance_info)?; - - let mut proposal_data = get_proposal_data_for_governance_and_governing_mint( + let realm_data = get_realm_data_for_governing_token_mint( program_id, - proposal_info, - governance_info.key, - governing_token_mint_info.key, + realm_info, + vote_governing_token_mint_info.key, )?; + let governance_data = + get_governance_data_for_realm(program_id, governance_info, realm_info.key)?; + + let mut proposal_data = + get_proposal_data_for_governance(program_id, proposal_info, governance_info.key)?; + let mut token_owner_record_data = get_token_owner_record_data_for_realm_and_governing_mint( program_id, token_owner_record_info, &governance_data.realm, - governing_token_mint_info.key, + vote_governing_token_mint_info.key, )?; - let mut vote_record_data = get_vote_record_data_for_proposal_and_token_owner( + let mut vote_record_data = get_vote_record_data_for_proposal_and_token_owner_record( program_id, vote_record_info, + &realm_data, proposal_info.key, - &token_owner_record_data.governing_token_owner, + &proposal_data, + &token_owner_record_data, )?; vote_record_data.assert_can_relinquish_vote()?; @@ -89,7 +96,13 @@ pub fn process_relinquish_vote(program_id: &Pubkey, accounts: &[AccountInfo]) -> .unwrap(), ) } - Vote::Abstain | Vote::Veto => { + Vote::Veto => { + proposal_data.veto_vote_weight = proposal_data + .veto_vote_weight + .checked_sub(vote_record_data.voter_weight) + .unwrap(); + } + Vote::Abstain => { return Err(GovernanceError::NotSupportedVoteType.into()); } } diff --git a/governance/program/src/state/enums.rs b/governance/program/src/state/enums.rs index 9eb92634..e0811a16 100644 --- a/governance/program/src/state/enums.rs +++ b/governance/program/src/state/enums.rs @@ -3,7 +3,6 @@ use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; /// Defines all Governance accounts types -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum GovernanceAccountType { /// Default uninitialized account state @@ -97,7 +96,6 @@ impl Default for GovernanceAccountType { } /// What state a Proposal is in -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum ProposalState { /// Draft - Proposal enters Draft state when it's created @@ -129,6 +127,9 @@ pub enum ProposalState { /// Same as Executing but indicates some instructions failed to execute /// Proposal can't be transitioned from ExecutingWithErrors to Completed state ExecutingWithErrors, + + /// The Proposal was vetoed + Vetoed, } impl Default for ProposalState { @@ -140,7 +141,6 @@ impl Default for ProposalState { /// The type of the vote threshold used to resolve a vote on a Proposal /// /// Note: In the current version only YesVotePercentage and Disabled thresholds are supported -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum VoteThreshold { /// Voting threshold of Yes votes in % required to tip the vote (Approval Quorum) @@ -173,7 +173,6 @@ pub enum VoteThreshold { /// The type of vote tipping to use on a Proposal. /// /// Vote tipping means that under some conditions voting will complete early. -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum VoteTipping { /// Tip when there is no way for another option to win and the vote threshold @@ -193,7 +192,6 @@ pub enum VoteTipping { } /// The status of instruction execution -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum TransactionExecutionStatus { /// Transaction was not executed yet @@ -207,7 +205,6 @@ pub enum TransactionExecutionStatus { } /// Transaction execution flags defining how instructions are executed for a Proposal -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum InstructionExecutionFlags { /// No execution flags are specified @@ -227,7 +224,6 @@ pub enum InstructionExecutionFlags { /// The source of max vote weight used for voting /// Values below 100% mint supply can be used when the governing token is fully minted but not distributed yet -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum MintMaxVoteWeightSource { /// Fraction (10^10 precision) of the governing mint supply is used as max vote weight diff --git a/governance/program/src/state/governance.rs b/governance/program/src/state/governance.rs index 0b25b72d..6a7af6be 100644 --- a/governance/program/src/state/governance.rs +++ b/governance/program/src/state/governance.rs @@ -7,6 +7,7 @@ use crate::{ enums::{GovernanceAccountType, VoteThreshold, VoteTipping}, legacy::{is_governance_v1_account_type, GovernanceV1}, realm::{assert_is_valid_realm, RealmV2}, + vote_record::VoteKind, }, }; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; @@ -20,7 +21,6 @@ use spl_governance_tools::{ }; /// Governance config -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct GovernanceConfig { /// The type of the vote threshold used for community vote @@ -43,15 +43,19 @@ pub struct GovernanceConfig { /// Note: In the current version only YesVotePercentage and Disabled thresholds are supported pub council_vote_threshold: VoteThreshold, - /// Reserved space for future versions - pub reserved: [u8; 2], + /// The threshold for Council Veto votes + pub council_veto_vote_threshold: VoteThreshold, /// Minimum council weight a governance token owner must possess to be able to create a proposal pub min_council_weight_to_create_proposal: u64, + // + // The threshold for Community Veto votes + // Note: Community Veto vote is not supported in the current version + // In order to use this threshold the space from GovernanceV2.reserved must be taken to expand GovernanceConfig size + // pub community_veto_vote_threshold: VoteThreshold, } /// Governance Account -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct GovernanceV2 { /// Account type. It can be Uninitialized, Governance, ProgramGovernance, TokenGovernance or MintGovernance @@ -198,28 +202,39 @@ impl GovernanceV2 { } /// Asserts the provided voting population represented by the given governing_token_mint - /// can vote on proposals for the Governance + /// can cast the given vote type on proposals for the Governance pub fn assert_governing_token_mint_can_vote( &self, realm_data: &RealmV2, - governing_token_mint: &Pubkey, + vote_governing_token_mint: &Pubkey, + vote_kind: &VoteKind, ) -> Result<(), ProgramError> { // resolve_vote_threshold() asserts the vote threshold exists for the given governing_token_mint and is not disabled - let _ = self.resolve_vote_threshold(realm_data, governing_token_mint)?; + let _ = self.resolve_vote_threshold(realm_data, vote_governing_token_mint, vote_kind)?; Ok(()) } - /// Resolves VoteThreshold for the given realm and governing token + /// Resolves VoteThreshold for the given realm, governing token and Vote kind pub fn resolve_vote_threshold( &self, realm_data: &RealmV2, - governing_token_mint: &Pubkey, + vote_governing_token_mint: &Pubkey, + vote_kind: &VoteKind, ) -> Result { - let vote_threshold = if realm_data.community_mint == *governing_token_mint { - &self.config.community_vote_threshold - } else if realm_data.config.council_mint == Some(*governing_token_mint) { - &self.config.council_vote_threshold + let vote_threshold = if realm_data.community_mint == *vote_governing_token_mint { + match vote_kind { + VoteKind::Electorate => &self.config.community_vote_threshold, + VoteKind::Veto => { + // Community Veto vote is not supported in current version + return Err(GovernanceError::GoverningTokenMintNotAllowedToVote.into()); + } + } + } else if realm_data.config.council_mint == Some(*vote_governing_token_mint) { + match vote_kind { + VoteKind::Electorate => &self.config.council_vote_threshold, + VoteKind::Veto => &self.config.council_veto_vote_threshold, + } } else { return Err(GovernanceError::InvalidGoverningTokenMint.into()); }; @@ -265,7 +280,7 @@ 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 + // 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 @@ -275,6 +290,10 @@ pub fn get_governance_data( if governance_data.config.council_vote_threshold == VoteThreshold::YesVotePercentage(0) { governance_data.config.council_vote_threshold = governance_data.config.community_vote_threshold.clone(); + + // The assumption here is that council should have Veto vote enabled by default and equal to council_vote_threshold + governance_data.config.council_veto_vote_threshold = + governance_data.config.council_vote_threshold.clone(); } Ok(governance_data) @@ -430,8 +449,9 @@ pub fn assert_is_valid_governance_config( ) -> Result<(), ProgramError> { assert_is_valid_vote_threshold(&governance_config.community_vote_threshold)?; assert_is_valid_vote_threshold(&governance_config.council_vote_threshold)?; + assert_is_valid_vote_threshold(&governance_config.council_veto_vote_threshold)?; - // Setting both thresholds to Disabled for now, however we might reconsider it as + // Setting both thresholds to Disabled is not allowed, however we might reconsider it as // a way to disable Governance permanently if governance_config.community_vote_threshold == VoteThreshold::Disabled && governance_config.council_vote_threshold == VoteThreshold::Disabled @@ -439,10 +459,6 @@ pub fn assert_is_valid_governance_config( return Err(GovernanceError::AtLeastOneVoteThresholdRequired.into()); } - if governance_config.reserved != [0, 0] { - return Err(GovernanceError::ReservedBufferMustBeEmpty.into()); - } - Ok(()) } @@ -470,12 +486,12 @@ mod test { use super::*; #[test] - fn test_deserialize_legacy_governance_account_without_council_threshold() { + fn test_deserialize_legacy_governance_account_without_council_vote_thresholds() { // Arrange // Legacy GovernanceV2 with // 1) config.community_vote_threshold = YesVotePercentage(10) - // 2) config.proposal_cool_off_tim = 0 + // 2) config.proposal_cool_off_time = 0 let mut account_data = [ 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -514,6 +530,11 @@ mod test { governance.config.community_vote_threshold, governance.config.council_vote_threshold ); + + assert_eq!( + governance.config.council_vote_threshold, + governance.config.council_veto_vote_threshold + ); } #[test] @@ -526,7 +547,7 @@ mod test { max_voting_time: 1, vote_tipping: VoteTipping::Strict, council_vote_threshold: VoteThreshold::YesVotePercentage(0), - reserved: [0; 2], + council_veto_vote_threshold: VoteThreshold::YesVotePercentage(1), min_council_weight_to_create_proposal: 1, }; @@ -549,7 +570,7 @@ mod test { max_voting_time: 1, vote_tipping: VoteTipping::Strict, council_vote_threshold: VoteThreshold::YesVotePercentage(1), - reserved: [0; 2], + council_veto_vote_threshold: VoteThreshold::YesVotePercentage(1), min_council_weight_to_create_proposal: 1, }; @@ -572,7 +593,7 @@ mod test { max_voting_time: 1, vote_tipping: VoteTipping::Strict, council_vote_threshold: VoteThreshold::Disabled, - reserved: [0; 2], + council_veto_vote_threshold: VoteThreshold::YesVotePercentage(1), min_council_weight_to_create_proposal: 1, }; @@ -586,16 +607,16 @@ mod test { } #[test] - fn test_assert_config_invalid_with_reserved_buffer_set() { + fn test_assert_config_invalid_with_council_zero_yes_veto_vote_threshold() { // Arrange let governance_config = GovernanceConfig { - community_vote_threshold: VoteThreshold::YesVotePercentage(10), + community_vote_threshold: VoteThreshold::YesVotePercentage(1), min_community_weight_to_create_proposal: 1, min_transaction_hold_up_time: 1, max_voting_time: 1, vote_tipping: VoteTipping::Strict, - council_vote_threshold: VoteThreshold::Disabled, - reserved: [0, 1], + council_vote_threshold: VoteThreshold::YesVotePercentage(1), + council_veto_vote_threshold: VoteThreshold::YesVotePercentage(0), min_council_weight_to_create_proposal: 1, }; @@ -605,6 +626,6 @@ mod test { .unwrap(); // Assert - assert_eq!(err, GovernanceError::ReservedBufferMustBeEmpty.into()); + assert_eq!(err, GovernanceError::InvalidVoteThresholdPercentage.into()); } } diff --git a/governance/program/src/state/legacy.rs b/governance/program/src/state/legacy.rs index a979e13e..39fcd71d 100644 --- a/governance/program/src/state/legacy.rs +++ b/governance/program/src/state/legacy.rs @@ -53,7 +53,6 @@ impl IsInitialized for RealmV1 { /// Governance Token Owner Record /// Account PDA seeds: ['governance', realm, token_mint, token_owner ] -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct TokenOwnerRecordV1 { /// Governance account type @@ -102,7 +101,6 @@ impl IsInitialized for TokenOwnerRecordV1 { } /// Governance Account -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct GovernanceV1 { /// Account type. It can be Uninitialized, Governance, ProgramGovernance, TokenGovernance or MintGovernance @@ -173,7 +171,6 @@ impl IsInitialized for GovernanceV1 { } /// Governance Proposal -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct ProposalV1 { /// Governance account type @@ -263,7 +260,6 @@ impl IsInitialized for ProposalV1 { } /// Account PDA seeds: ['governance', proposal, signatory] -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct SignatoryRecordV1 { /// Governance account type diff --git a/governance/program/src/state/program_metadata.rs b/governance/program/src/state/program_metadata.rs index d36c2061..1c2fbf0e 100644 --- a/governance/program/src/state/program_metadata.rs +++ b/governance/program/src/state/program_metadata.rs @@ -10,7 +10,6 @@ use spl_governance_tools::account::{get_account_data, AccountMaxSize}; use crate::state::enums::GovernanceAccountType; /// Program metadata account. It stores information about the particular SPL-Governance program instance -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct ProgramMetadata { /// Governance account type diff --git a/governance/program/src/state/proposal.rs b/governance/program/src/state/proposal.rs index c7640738..3d4ef70d 100644 --- a/governance/program/src/state/proposal.rs +++ b/governance/program/src/state/proposal.rs @@ -32,6 +32,7 @@ use crate::{ realm::RealmV2, realm_config::get_realm_config_data_for_realm, vote_record::Vote, + vote_record::VoteKind, }, PROGRAM_AUTHORITY_SEED, }; @@ -139,9 +140,9 @@ pub struct ProposalV2 { /// Without the deny option a proposal is only non executable survey pub deny_vote_weight: Option, - /// The total weight of Veto votes - /// Note: Veto is not supported in the current version - pub veto_vote_weight: Option, + /// Reserved space for future versions + /// This field is a leftover from unused veto_vote_weight: Option + pub reserved1: u8, /// The total weight of votes /// Note: Abstain is not supported in the current version @@ -200,6 +201,9 @@ pub struct ProposalV2 { /// Link to proposal's description pub description_link: String, + + /// The total weight of Veto votes + pub veto_vote_weight: u64, } impl AccountMaxSize for ProposalV2 { @@ -232,7 +236,8 @@ impl ProposalV2 { | ProposalState::Cancelled | ProposalState::Voting | ProposalState::Succeeded - | ProposalState::Defeated => Err(GovernanceError::InvalidStateCannotSignOff.into()), + | ProposalState::Defeated + | ProposalState::Vetoed => Err(GovernanceError::InvalidStateCannotSignOff.into()), } } @@ -414,10 +419,12 @@ impl ProposalV2 { fn get_max_voter_weight_from_mint_supply( &mut self, realm_data: &RealmV2, + governing_token_mint: &Pubkey, governing_token_mint_supply: u64, + vote_kind: &VoteKind, ) -> Result { // max vote weight fraction is only used for community mint - if Some(self.governing_token_mint) == realm_data.config.council_mint { + if Some(*governing_token_mint) == realm_data.config.council_mint { return Ok(governing_token_mint_supply); } @@ -435,7 +442,7 @@ impl ProposalV2 { // When the fraction is used it's possible we can go over the calculated max_vote_weight // and we have to adjust it in case more votes have been cast - Ok(self.coerce_max_voter_weight(max_voter_weight)) + Ok(self.coerce_max_voter_weight(max_voter_weight, vote_kind)) } MintMaxVoteWeightSource::Absolute(_) => { Err(GovernanceError::VoteWeightSourceNotSupported.into()) @@ -444,61 +451,72 @@ impl ProposalV2 { } /// Adjusts max voter weight to ensure it's not lower than total cast votes - fn coerce_max_voter_weight(&self, max_voter_weight: u64) -> u64 { - let deny_vote_weight = self.deny_vote_weight.unwrap_or(0); + fn coerce_max_voter_weight(&self, max_voter_weight: u64, vote_kind: &VoteKind) -> u64 { + let total_vote_weight = match vote_kind { + VoteKind::Electorate => { + let deny_vote_weight = self.deny_vote_weight.unwrap_or(0); - let max_option_vote_weight = self.options.iter().map(|o| o.vote_weight).max().unwrap(); + let max_option_vote_weight = + self.options.iter().map(|o| o.vote_weight).max().unwrap(); - let total_vote_weight = max_option_vote_weight - .checked_add(deny_vote_weight) - .unwrap(); + max_option_vote_weight + .checked_add(deny_vote_weight) + .unwrap() + } + VoteKind::Veto => self.veto_vote_weight, + }; max_voter_weight.max(total_vote_weight) } /// Resolves max voter weight + #[allow(clippy::too_many_arguments)] pub fn resolve_max_voter_weight( &mut self, program_id: &Pubkey, realm_config_info: &AccountInfo, - governing_token_mint_info: &AccountInfo, + vote_governing_token_mint_info: &AccountInfo, account_info_iter: &mut Iter, realm: &Pubkey, realm_data: &RealmV2, + vote_kind: &VoteKind, ) -> Result { // if the realm uses addin for max community voter weight then use the externally provided max weight if realm_data.config.use_max_community_voter_weight_addin - && realm_data.community_mint == self.governing_token_mint + && realm_data.community_mint == *vote_governing_token_mint_info.key { let realm_config_data = get_realm_config_data_for_realm(program_id, realm_config_info, realm)?; let max_voter_weight_record_info = next_account_info(account_info_iter)?; - let max_voter_weight_data = + let max_voter_weight_record_data = get_max_voter_weight_record_data_for_realm_and_governing_token_mint( &realm_config_data.max_community_voter_weight_addin.unwrap(), max_voter_weight_record_info, realm, - &self.governing_token_mint, + vote_governing_token_mint_info.key, )?; - assert_is_valid_max_voter_weight(&max_voter_weight_data)?; + assert_is_valid_max_voter_weight(&max_voter_weight_record_data)?; // When the max voter weight addin is used it's possible it can be inaccurate and we can have more votes then the max provided by the addin // and we have to adjust it to whatever result is higher - return Ok(self.coerce_max_voter_weight(max_voter_weight_data.max_voter_weight)); + return Ok(self.coerce_max_voter_weight( + max_voter_weight_record_data.max_voter_weight, + vote_kind, + )); } - // Note: governing_token_mint_info is already verified at this point and this - // check is just a safety net in case some future refactoring would remove the validation - if self.governing_token_mint != *governing_token_mint_info.key { - return Err(GovernanceError::InvalidGoverningMintForProposal.into()); - } - let governing_token_mint_supply = get_spl_token_mint_supply(governing_token_mint_info)?; + let vote_governing_token_mint_supply = + get_spl_token_mint_supply(vote_governing_token_mint_info)?; - let max_voter_weight = - self.get_max_voter_weight_from_mint_supply(realm_data, governing_token_mint_supply)?; + let max_voter_weight = self.get_max_voter_weight_from_mint_supply( + realm_data, + vote_governing_token_mint_info.key, + vote_governing_token_mint_supply, + vote_kind, + )?; Ok(max_voter_weight) } @@ -508,17 +526,22 @@ impl ProposalV2 { pub fn try_tip_vote( &mut self, max_voter_weight: u64, - config: &GovernanceConfig, + vote_tipping: &VoteTipping, current_unix_timestamp: UnixTimestamp, vote_threshold: &VoteThreshold, + vote_kind: &VoteKind, ) -> Result { - if let Some(tipped_state) = - self.try_get_tipped_vote_state(max_voter_weight, config, vote_threshold) - { + if let Some(tipped_state) = self.try_get_tipped_vote_state( + max_voter_weight, + vote_tipping, + vote_threshold, + vote_kind, + ) { self.state = tipped_state; self.voting_completed_at = Some(current_unix_timestamp); // Capture vote params to correctly display historical results + // Note: For Veto vote the captured params are from the Veto config self.max_vote_weight = Some(max_voter_weight); self.vote_threshold = Some(vote_threshold.clone()); @@ -528,22 +551,43 @@ impl ProposalV2 { } } - /// Checks if vote can be tipped and automatically transitioned to Succeeded or Defeated state + /// Checks if vote can be tipped and automatically transitioned to Succeeded, Defeated or Vetoed state /// If yes then Some(ProposalState) is returned and None otherwise - #[allow(clippy::float_cmp)] pub fn try_get_tipped_vote_state( &mut self, - max_vote_weight: u64, - config: &GovernanceConfig, + max_voter_weight: u64, + vote_tipping: &VoteTipping, vote_threshold: &VoteThreshold, + vote_kind: &VoteKind, + ) -> Option { + let min_vote_threshold_weight = + get_min_vote_threshold_weight(vote_threshold, max_voter_weight).unwrap(); + + match vote_kind { + VoteKind::Electorate => self.try_get_tipped_electorate_vote_state( + max_voter_weight, + vote_tipping, + min_vote_threshold_weight, + ), + VoteKind::Veto => self.try_get_tipped_veto_vote_state(min_vote_threshold_weight), + } + } + + /// Checks if Electorate vote can be tipped and automatically transitioned to Succeeded or Defeated state + /// If yes then Some(ProposalState) is returned and None otherwise + fn try_get_tipped_electorate_vote_state( + &mut self, + max_voter_weight: u64, + vote_tipping: &VoteTipping, + min_vote_threshold_weight: u64, ) -> Option { // Vote tipping is currently supported for SingleChoice votes with single Yes and No (rejection) options only // Note: Tipping for multiple options (single choice and multiple choices) should be possible but it requires a great deal of considerations // and I decided to fight it another day if self.vote_type != VoteType::SingleChoice - // Tipping should not be allowed for opinion only proposals (surveys without rejection) to allow everybody's voice to be heard - || self.deny_vote_weight.is_none() - || self.options.len() != 1 + // Tipping should not be allowed for opinion only proposals (surveys without rejection) to allow everybody's voice to be heard + || self.deny_vote_weight.is_none() + || self.options.len() != 1 { return None; }; @@ -553,24 +597,21 @@ impl ProposalV2 { let yes_vote_weight = yes_option.vote_weight; let deny_vote_weight = self.deny_vote_weight.unwrap(); - if yes_vote_weight == max_vote_weight { + if yes_vote_weight == max_voter_weight { yes_option.vote_result = OptionVoteResult::Succeeded; return Some(ProposalState::Succeeded); } - if deny_vote_weight == max_vote_weight { + if deny_vote_weight == max_voter_weight { yes_option.vote_result = OptionVoteResult::Defeated; return Some(ProposalState::Defeated); } - let min_vote_threshold_weight = - get_min_vote_threshold_weight(vote_threshold, max_vote_weight).unwrap(); - - match config.vote_tipping { + match vote_tipping { VoteTipping::Disabled => {} VoteTipping::Strict => { if yes_vote_weight >= min_vote_threshold_weight - && yes_vote_weight > (max_vote_weight.saturating_sub(yes_vote_weight)) + && yes_vote_weight > (max_voter_weight.saturating_sub(yes_vote_weight)) { yes_option.vote_result = OptionVoteResult::Succeeded; return Some(ProposalState::Succeeded); @@ -590,9 +631,9 @@ impl ProposalV2 { // "defeated" if there is no possible way of reaching majority or the // min_vote_threshold_weight for another option. This tipping is always // strict, there's no equivalent to "early" tipping for deny votes. - if config.vote_tipping != VoteTipping::Disabled - && (deny_vote_weight > (max_vote_weight.saturating_sub(min_vote_threshold_weight)) - || deny_vote_weight >= (max_vote_weight.saturating_sub(deny_vote_weight))) + if *vote_tipping != VoteTipping::Disabled + && (deny_vote_weight > (max_voter_weight.saturating_sub(min_vote_threshold_weight)) + || deny_vote_weight >= (max_voter_weight.saturating_sub(deny_vote_weight))) { yes_option.vote_result = OptionVoteResult::Defeated; return Some(ProposalState::Defeated); @@ -601,6 +642,22 @@ impl ProposalV2 { None } + /// Checks if vote can be tipped and transitioned to Vetoed state + /// If yes then Some(ProposalState::Vetoed) is returned and None otherwise + fn try_get_tipped_veto_vote_state( + &mut self, + min_vote_threshold_weight: u64, + ) -> Option { + // Veto vote tips as soon as the required threshold is reached + // It's irrespectively of vote_tipping config because the outcome of the Proposal can't change any longer after being vetoed + if self.veto_vote_weight >= min_vote_threshold_weight { + // Note: Since we don't tip multi option votes all options vote_result would remain as None + Some(ProposalState::Vetoed) + } else { + None + } + } + /// Checks if Proposal can be canceled in the given state pub fn assert_can_cancel( &self, @@ -622,7 +679,8 @@ impl ProposalV2 { | ProposalState::Completed | ProposalState::Cancelled | ProposalState::Succeeded - | ProposalState::Defeated => { + | ProposalState::Defeated + | ProposalState::Vetoed => { Err(GovernanceError::InvalidStateCannotCancelProposal.into()) } } @@ -658,7 +716,8 @@ impl ProposalV2 { | ProposalState::Completed | ProposalState::Voting | ProposalState::Cancelled - | ProposalState::Defeated => { + | ProposalState::Defeated + | ProposalState::Vetoed => { return Err(GovernanceError::InvalidStateCannotExecuteTransaction.into()) } } @@ -745,9 +804,10 @@ impl ProposalV2 { return Err(GovernanceError::InvalidVote.into()); } } - Vote::Abstain | Vote::Veto => { + Vote::Abstain => { return Err(GovernanceError::NotSupportedVoteType.into()); } + Vote::Veto => {} } Ok(()) @@ -764,7 +824,7 @@ impl ProposalV2 { panic!("ProposalV1 doesn't support Abstain vote") } - if self.veto_vote_weight.is_some() { + if self.veto_vote_weight > 0 { panic!("ProposalV1 doesn't support Veto vote") } @@ -818,7 +878,7 @@ impl ProposalV2 { /// and returns the min weight required for a proposal option to pass fn get_min_vote_threshold_weight( vote_threshold: &VoteThreshold, - max_vote_weight: u64, + max_voter_weight: u64, ) -> Result { let yes_vote_threshold_percentage = match vote_threshold { VoteThreshold::YesVotePercentage(yes_vote_threshold_percentage) => { @@ -830,7 +890,7 @@ fn get_min_vote_threshold_weight( }; let numerator = (yes_vote_threshold_percentage as u128) - .checked_mul(max_vote_weight as u128) + .checked_mul(max_voter_weight as u128) .unwrap(); let mut yes_vote_threshold = numerator.checked_div(100).unwrap(); @@ -863,7 +923,7 @@ pub fn get_proposal_data( | ProposalState::Executing | ProposalState::ExecutingWithErrors | ProposalState::Completed => OptionVoteResult::Succeeded, - ProposalState::Defeated => OptionVoteResult::None, + ProposalState::Vetoed | ProposalState::Defeated => OptionVoteResult::None, }; return Ok(ProposalV2 { @@ -884,7 +944,7 @@ pub fn get_proposal_data( transactions_next_index: proposal_data_v1.instructions_next_index, }], deny_vote_weight: Some(proposal_data_v1.no_votes_count), - veto_vote_weight: None, + veto_vote_weight: 0, abstain_vote_weight: None, start_voting_at: None, draft_at: proposal_data_v1.draft_at, @@ -901,13 +961,14 @@ pub fn get_proposal_data( name: proposal_data_v1.name, description_link: proposal_data_v1.description_link, reserved: [0; 64], + reserved1: 0, }); } get_account_data::(program_id, proposal_info) } -/// Deserializes Proposal and validates it belongs to the given Governance and Governing Mint +/// Deserializes Proposal and validates it belongs to the given Governance and governing_token_mint pub fn get_proposal_data_for_governance_and_governing_mint( program_id: &Pubkey, proposal_info: &AccountInfo, @@ -1047,7 +1108,7 @@ mod test { }], deny_vote_weight: Some(0), abstain_vote_weight: Some(0), - veto_vote_weight: Some(0), + veto_vote_weight: 0, execution_flags: InstructionExecutionFlags::Ordered, @@ -1055,6 +1116,7 @@ mod test { vote_threshold: Some(VoteThreshold::YesVotePercentage(100)), reserved: [0; 64], + reserved1: 0, } } @@ -1122,7 +1184,7 @@ mod test { community_vote_threshold: VoteThreshold::YesVotePercentage(60), vote_tipping: VoteTipping::Strict, council_vote_threshold: VoteThreshold::YesVotePercentage(60), - reserved: [0; 2], + council_veto_vote_threshold: VoteThreshold::YesVotePercentage(50), } } @@ -1186,6 +1248,7 @@ mod test { Just(ProposalState::Completed), Just(ProposalState::Cancelled), Just(ProposalState::Defeated), + Just(ProposalState::Vetoed), Just(ProposalState::SigningOff), ] } @@ -1227,6 +1290,7 @@ mod test { Just(ProposalState::Completed), Just(ProposalState::Cancelled), Just(ProposalState::Defeated), + Just(ProposalState::Vetoed), ] } @@ -1279,6 +1343,7 @@ mod test { Just(ProposalState::Completed), Just(ProposalState::Cancelled), Just(ProposalState::Defeated), + Just(ProposalState::Vetoed), ] } @@ -1529,17 +1594,21 @@ mod test { proposal.state = ProposalState::Voting; - let governance_config = create_test_governance_config(); let current_timestamp = 15_i64; let realm = create_test_realm(); + let governing_token_mint = proposal.governing_token_mint; + let vote_kind = VoteKind::Electorate; + let vote_tipping = VoteTipping::Strict; - let max_voter_weight = proposal.get_max_voter_weight_from_mint_supply(&realm,test_case.governing_token_supply).unwrap(); + let max_voter_weight = proposal.get_max_voter_weight_from_mint_supply(&realm,&governing_token_mint, test_case.governing_token_supply,&vote_kind).unwrap(); let vote_threshold = VoteThreshold::YesVotePercentage(test_case.yes_vote_threshold_percentage); + + // Act - proposal.try_tip_vote(max_voter_weight, &governance_config,current_timestamp,&vote_threshold).unwrap(); + proposal.try_tip_vote(max_voter_weight, &vote_tipping,current_timestamp,&vote_threshold,&vote_kind).unwrap(); // Assert assert_eq!(proposal.state,test_case.expected_tipped_state,"CASE: {:?}",test_case); @@ -1578,7 +1647,10 @@ mod test { let current_timestamp = 16_i64; let realm = create_test_realm(); - let max_voter_weight = proposal.get_max_voter_weight_from_mint_supply(&realm,test_case.governing_token_supply).unwrap(); + let governing_token_mint = proposal.governing_token_mint; + let vote_kind = VoteKind::Electorate; + + let max_voter_weight = proposal.get_max_voter_weight_from_mint_supply(&realm,&governing_token_mint,test_case.governing_token_supply,&vote_kind).unwrap(); let vote_threshold = VoteThreshold::YesVotePercentage(test_case.yes_vote_threshold_percentage); // Act @@ -1633,17 +1705,22 @@ mod test { proposal.state = ProposalState::Voting; - let governance_config = create_test_governance_config(); + let yes_vote_threshold_percentage = VoteThreshold::YesVotePercentage(yes_vote_threshold_percentage); let current_timestamp = 15_i64; let realm = create_test_realm(); - let max_voter_weight = proposal.get_max_voter_weight_from_mint_supply(&realm,governing_token_supply).unwrap(); + let governing_token_mint = proposal.governing_token_mint; + let vote_kind = VoteKind::Electorate; + let vote_tipping = VoteTipping::Strict; + + let max_voter_weight = proposal.get_max_voter_weight_from_mint_supply(&realm,&governing_token_mint,governing_token_supply,&vote_kind).unwrap(); let vote_threshold = yes_vote_threshold_percentage.clone(); + // Act - proposal.try_tip_vote(max_voter_weight, &governance_config, current_timestamp,&vote_threshold).unwrap(); + proposal.try_tip_vote(max_voter_weight, &vote_tipping, current_timestamp,&vote_threshold,&vote_kind).unwrap(); // Assert let yes_vote_threshold_count = get_min_vote_threshold_weight(&yes_vote_threshold_percentage,governing_token_supply).unwrap(); @@ -1684,7 +1761,10 @@ mod test { let current_timestamp = 16_i64; let realm = create_test_realm(); - let max_voter_weight = proposal.get_max_voter_weight_from_mint_supply(&realm,governing_token_supply).unwrap(); + let governing_token_mint = proposal.governing_token_mint; + let vote_kind = VoteKind::Electorate; + + let max_voter_weight = proposal.get_max_voter_weight_from_mint_supply(&realm,&governing_token_mint,governing_token_supply,&vote_kind).unwrap(); let vote_threshold = yes_vote_threshold_percentage.clone(); // Act @@ -1714,12 +1794,14 @@ mod test { proposal.state = ProposalState::Voting; - let governance_config = create_test_governance_config(); let current_timestamp = 15_i64; let community_token_supply = 200; let mut realm = create_test_realm(); + let governing_token_mint = proposal.governing_token_mint; + let vote_kind = VoteKind::Electorate; + let vote_tipping = VoteTipping::Strict; // reduce max vote weight to 100 realm.config.community_mint_max_vote_weight_source = @@ -1728,18 +1810,25 @@ mod test { ); let max_voter_weight = proposal - .get_max_voter_weight_from_mint_supply(&realm, community_token_supply) + .get_max_voter_weight_from_mint_supply( + &realm, + &governing_token_mint, + community_token_supply, + &vote_kind, + ) .unwrap(); let vote_threshold = &VoteThreshold::YesVotePercentage(60); + let vote_kind = VoteKind::Electorate; // Act proposal .try_tip_vote( max_voter_weight, - &governance_config, + &vote_tipping, current_timestamp, vote_threshold, + &vote_kind, ) .unwrap(); @@ -1758,13 +1847,14 @@ mod test { proposal.state = ProposalState::Voting; - let governance_config = create_test_governance_config(); - let current_timestamp = 15_i64; let community_token_supply = 200; let mut realm = create_test_realm(); + let governing_token_mint = proposal.governing_token_mint; + let vote_kind = VoteKind::Electorate; + let vote_tipping = VoteTipping::Strict; // reduce max vote weight to 100 realm.config.community_mint_max_vote_weight_source = @@ -1777,7 +1867,12 @@ mod test { proposal.options[0].vote_weight = 120; let max_voter_weight = proposal - .get_max_voter_weight_from_mint_supply(&realm, community_token_supply) + .get_max_voter_weight_from_mint_supply( + &realm, + &governing_token_mint, + community_token_supply, + &vote_kind, + ) .unwrap(); let vote_threshold = VoteThreshold::YesVotePercentage(60); @@ -1786,9 +1881,10 @@ mod test { proposal .try_tip_vote( max_voter_weight, - &governance_config, + &vote_tipping, current_timestamp, &vote_threshold, + &vote_kind, ) .unwrap(); @@ -1807,13 +1903,15 @@ mod test { proposal.state = ProposalState::Voting; - let governance_config = create_test_governance_config(); - let current_timestamp = 15_i64; let community_token_supply = 200; let mut realm = create_test_realm(); + let governing_token_mint = proposal.governing_token_mint; + let vote_kind = VoteKind::Electorate; + let vote_tipping = VoteTipping::Strict; + realm.config.community_mint_max_vote_weight_source = MintMaxVoteWeightSource::SupplyFraction( MintMaxVoteWeightSource::SUPPLY_FRACTION_BASE / 2, @@ -1821,7 +1919,12 @@ mod test { realm.config.council_mint = Some(proposal.governing_token_mint); let max_voter_weight = proposal - .get_max_voter_weight_from_mint_supply(&realm, community_token_supply) + .get_max_voter_weight_from_mint_supply( + &realm, + &governing_token_mint, + community_token_supply, + &vote_kind, + ) .unwrap(); let vote_threshold = VoteThreshold::YesVotePercentage(60); @@ -1830,9 +1933,10 @@ mod test { proposal .try_tip_vote( max_voter_weight, - &governance_config, + &vote_tipping, current_timestamp, &vote_threshold, + &vote_kind, ) .unwrap(); @@ -1856,6 +1960,8 @@ mod test { let community_token_supply = 200; let mut realm = create_test_realm(); + let governing_token_mint = proposal.governing_token_mint; + let vote_kind = VoteKind::Electorate; // reduce max vote weight to 100 realm.config.community_mint_max_vote_weight_source = @@ -1864,7 +1970,12 @@ mod test { ); let max_voter_weight = proposal - .get_max_voter_weight_from_mint_supply(&realm, community_token_supply) + .get_max_voter_weight_from_mint_supply( + &realm, + &governing_token_mint, + community_token_supply, + &vote_kind, + ) .unwrap(); let vote_threshold = VoteThreshold::YesVotePercentage(60); @@ -1900,6 +2011,8 @@ mod test { let community_token_supply = 200; let mut realm = create_test_realm(); + let governing_token_mint = proposal.governing_token_mint; + let vote_kind = VoteKind::Electorate; // reduce max vote weight to 100 realm.config.community_mint_max_vote_weight_source = @@ -1911,7 +2024,12 @@ mod test { proposal.options[0].vote_weight = 120; let max_voter_weight = proposal - .get_max_voter_weight_from_mint_supply(&realm, community_token_supply) + .get_max_voter_weight_from_mint_supply( + &realm, + &governing_token_mint, + community_token_supply, + &vote_kind, + ) .unwrap(); let vote_threshold = VoteThreshold::YesVotePercentage(60); @@ -1942,8 +2060,11 @@ mod test { proposal.voting_at.unwrap() + governance_config.max_voting_time as i64; let realm = create_test_realm(); + let governing_token_mint = proposal.governing_token_mint; + let vote_kind = VoteKind::Electorate; + let max_voter_weight = proposal - .get_max_voter_weight_from_mint_supply(&realm, 100) + .get_max_voter_weight_from_mint_supply(&realm, &governing_token_mint, 100, &vote_kind) .unwrap(); let vote_threshold = &governance_config.community_vote_threshold; @@ -1974,8 +2095,11 @@ mod test { proposal.voting_at.unwrap() + governance_config.max_voting_time as i64 + 1; let realm = create_test_realm(); + let governing_token_mint = proposal.governing_token_mint; + let vote_kind = VoteKind::Electorate; + let max_voter_weight = proposal - .get_max_voter_weight_from_mint_supply(&realm, 100) + .get_max_voter_weight_from_mint_supply(&realm, &governing_token_mint, 100, &vote_kind) .unwrap(); let vote_threshold = &governance_config.community_vote_threshold; diff --git a/governance/program/src/state/proposal_transaction.rs b/governance/program/src/state/proposal_transaction.rs index 824cdb32..dcba1f6c 100644 --- a/governance/program/src/state/proposal_transaction.rs +++ b/governance/program/src/state/proposal_transaction.rs @@ -26,7 +26,6 @@ use spl_governance_tools::account::{get_account_data, AccountMaxSize}; /// InstructionData wrapper. It can be removed once Borsh serialization for Instruction is supported in the SDK #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -#[repr(C)] pub struct InstructionData { /// Pubkey of the instruction processor that executes this instruction pub program_id: Pubkey, @@ -38,7 +37,6 @@ pub struct InstructionData { /// Account metadata used to define Instructions #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -#[repr(C)] pub struct AccountMetaData { /// An account's public key pub pubkey: Pubkey, @@ -85,7 +83,6 @@ impl From<&InstructionData> for Instruction { } /// Account for an instruction to be executed for Proposal -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct ProposalTransactionV2 { /// Governance Account type diff --git a/governance/program/src/state/realm.rs b/governance/program/src/state/realm.rs index c6056e63..12b0165e 100644 --- a/governance/program/src/state/realm.rs +++ b/governance/program/src/state/realm.rs @@ -22,12 +22,12 @@ use crate::{ enums::{GovernanceAccountType, MintMaxVoteWeightSource}, legacy::RealmV1, token_owner_record::get_token_owner_record_data_for_realm, + vote_record::VoteKind, }, PROGRAM_AUTHORITY_SEED, }; /// Realm Config instruction args -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct RealmConfigArgs { /// Indicates whether council_mint should be used @@ -66,7 +66,6 @@ pub enum SetRealmAuthorityAction { } /// Realm Config defining Realm parameters. -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct RealmConfig { /// Indicates whether an external addin program should be used to provide voters weights for the community mint @@ -90,7 +89,6 @@ pub struct RealmConfig { /// Governance Realm Account /// Account PDA seeds" ['governance', name] -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct RealmV2 { /// Governance account type @@ -177,6 +175,37 @@ impl RealmV2 { Err(GovernanceError::InvalidGoverningTokenMint.into()) } + /// Returns the governing token mint which is used to vote on a proposal given the provided Vote kind and vote_governing_token_mint + /// + /// Veto vote is cast on a proposal configured for the opposite voting population defined using governing_token_mint + /// Council can veto Community vote and Community can veto Council assuming the veto for the voting population is enabled + /// + /// For all votes other than Veto (Electorate votes) the vote_governing_token_mint is the same as Proposal governing_token_mint + pub fn get_proposal_governing_token_mint_for_vote( + &self, + vote_governing_token_mint: &Pubkey, + vote_kind: &VoteKind, + ) -> Result { + match vote_kind { + VoteKind::Electorate => Ok(*vote_governing_token_mint), + VoteKind::Veto => { + // When Community veto Council proposal then return council_token_mint as the Proposal governing_token_mint + if self.community_mint == *vote_governing_token_mint { + // Community Veto is not supported in the current version + return Err(GovernanceError::GoverningTokenMintNotAllowedToVote.into()); + //return Ok(self.config.council_mint.unwrap()); + } + + // When Council veto Community proposal then return community_token_mint as the Proposal governing_token_mint + if self.config.council_mint == Some(*vote_governing_token_mint) { + return Ok(self.community_mint); + } + + Err(GovernanceError::InvalidGoverningTokenMint.into()) + } + } + } + /// Asserts the given governing token mint and holding accounts are valid for the realm pub fn assert_is_valid_governing_token_mint_and_holding( &self, diff --git a/governance/program/src/state/signatory_record.rs b/governance/program/src/state/signatory_record.rs index efeec258..b97b200a 100644 --- a/governance/program/src/state/signatory_record.rs +++ b/governance/program/src/state/signatory_record.rs @@ -12,12 +12,9 @@ use spl_governance_tools::account::{get_account_data, AccountMaxSize}; use crate::{error::GovernanceError, PROGRAM_AUTHORITY_SEED}; -use crate::state::enums::GovernanceAccountType; - -use super::legacy::SignatoryRecordV1; +use crate::state::{enums::GovernanceAccountType, legacy::SignatoryRecordV1}; /// Account PDA seeds: ['governance', proposal, signatory] -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct SignatoryRecordV2 { /// Governance account type diff --git a/governance/program/src/state/token_owner_record.rs b/governance/program/src/state/token_owner_record.rs index 750da3ad..1cd43af3 100644 --- a/governance/program/src/state/token_owner_record.rs +++ b/governance/program/src/state/token_owner_record.rs @@ -28,7 +28,6 @@ use spl_governance_tools::account::{get_account_data, AccountMaxSize}; /// Governance Token Owner Record /// Account PDA seeds: ['governance', realm, token_mint, token_owner ] -#[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct TokenOwnerRecordV2 { /// Governance account type diff --git a/governance/program/src/state/vote_record.rs b/governance/program/src/state/vote_record.rs index dfae8e5a..c9e8b99a 100644 --- a/governance/program/src/state/vote_record.rs +++ b/governance/program/src/state/vote_record.rs @@ -17,6 +17,9 @@ use crate::PROGRAM_AUTHORITY_SEED; use crate::state::{ enums::GovernanceAccountType, legacy::{VoteRecordV1, VoteWeightV1}, + proposal::ProposalV2, + realm::RealmV2, + token_owner_record::TokenOwnerRecordV2, }; /// Voter choice for a proposal option @@ -57,10 +60,28 @@ pub enum Vote { Abstain, /// Veto proposal - /// Note: Not supported in the current version Veto, } +/// VoteKind defines the type of the vote being cast +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] +pub enum VoteKind { + /// Electorate vote is cast by the voting population identified by governing_token_mint + /// Approve, Deny and Abstain votes are Electorate votes + Electorate, + + /// Vote cast by the opposite voting population to the Electorate identified by governing_token_mint + Veto, +} + +/// Returns the VoteKind for the given Vote +pub fn get_vote_kind(vote: &Vote) -> VoteKind { + match vote { + Vote::Approve(_) | Vote::Deny | Vote::Abstain => VoteKind::Electorate, + Vote::Veto => VoteKind::Veto, + } +} + /// Proposal VoteRecord #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct VoteRecordV2 { @@ -177,12 +198,14 @@ pub fn get_vote_record_data( get_account_data::(program_id, vote_record_info) } -/// Deserializes VoteRecord and checks it belongs to the provided Proposal and Governing Token Owner -pub fn get_vote_record_data_for_proposal_and_token_owner( +/// Deserializes VoteRecord and checks it belongs to the provided Proposal and TokenOwnerRecord +pub fn get_vote_record_data_for_proposal_and_token_owner_record( program_id: &Pubkey, vote_record_info: &AccountInfo, + realm_data: &RealmV2, proposal: &Pubkey, - governing_token_owner: &Pubkey, + proposal_data: &ProposalV2, + token_owner_record_data: &TokenOwnerRecordV2, ) -> Result { let vote_record_data = get_vote_record_data(program_id, vote_record_info)?; @@ -190,10 +213,22 @@ pub fn get_vote_record_data_for_proposal_and_token_owner( return Err(GovernanceError::InvalidProposalForVoterRecord.into()); } - if vote_record_data.governing_token_owner != *governing_token_owner { + if vote_record_data.governing_token_owner != token_owner_record_data.governing_token_owner { return Err(GovernanceError::InvalidGoverningTokenOwnerForVoteRecord.into()); } + // Assert governing_token_mint between Proposal and TokenOwnerRecord match for the deserialized VoteRecord + // For Approve, Deny and Abstain votes Proposal.governing_token_mint must equal TokenOwnerRecord.governing_token_mint + // For Veto vote it must be the governing_token_mint of the opposite voting population + let proposal_governing_token_mint = realm_data.get_proposal_governing_token_mint_for_vote( + &token_owner_record_data.governing_token_mint, + &get_vote_kind(&vote_record_data.vote), + )?; + + if proposal_data.governing_token_mint != proposal_governing_token_mint { + return Err(GovernanceError::InvalidGoverningMintForProposal.into()); + } + Ok(vote_record_data) } diff --git a/governance/program/tests/process_cancel_proposal.rs b/governance/program/tests/process_cancel_proposal.rs index 007a98db..382efc89 100644 --- a/governance/program/tests/process_cancel_proposal.rs +++ b/governance/program/tests/process_cancel_proposal.rs @@ -97,7 +97,7 @@ async fn test_cancel_proposal_with_already_completed_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); diff --git a/governance/program/tests/process_cast_vote.rs b/governance/program/tests/process_cast_vote.rs index bba23ca5..5050f231 100644 --- a/governance/program/tests/process_cast_vote.rs +++ b/governance/program/tests/process_cast_vote.rs @@ -42,7 +42,7 @@ async fn test_cast_vote() { // Act let vote_record_cookie = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -139,7 +139,7 @@ async fn test_cast_vote_with_invalid_governance_error() { // Act let err = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .err() .unwrap(); @@ -155,7 +155,7 @@ async fn test_cast_vote_with_invalid_mint_error() { 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 + let mut token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) .await .unwrap(); @@ -169,18 +169,18 @@ async fn test_cast_vote_with_invalid_mint_error() { .await .unwrap(); - let mut proposal_cookie = governance_test + let proposal_cookie = governance_test .with_signed_off_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); // Try to use Council Mint with Community Proposal - proposal_cookie.account.governing_token_mint = + token_owner_record_cookie.account.governing_token_mint = realm_cookie.account.config.council_mint.unwrap(); // Act let err = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .err() .unwrap(); @@ -225,7 +225,7 @@ async fn test_cast_vote_with_invalid_token_owner_record_mint_error() { // Act let err = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .err() .unwrap(); @@ -275,7 +275,7 @@ async fn test_cast_vote_with_invalid_token_owner_record_from_different_realm_err // Act let err = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .err() .unwrap(); @@ -320,7 +320,7 @@ async fn test_cast_vote_with_governance_authority_must_sign_error() { // Act let err = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .err() .unwrap(); @@ -374,7 +374,7 @@ async fn test_cast_vote_with_strict_vote_tipped_to_succeeded() { // Act governance_test - .with_cast_vote( + .with_cast_yes_no_vote( &proposal_cookie, &token_owner_record_cookie1, YesNoVote::Yes, @@ -392,7 +392,7 @@ async fn test_cast_vote_with_strict_vote_tipped_to_succeeded() { // Act governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie2, YesNoVote::No) .await .unwrap(); @@ -406,7 +406,7 @@ async fn test_cast_vote_with_strict_vote_tipped_to_succeeded() { // Act governance_test - .with_cast_vote( + .with_cast_yes_no_vote( &proposal_cookie, &token_owner_record_cookie3, YesNoVote::Yes, @@ -488,7 +488,7 @@ async fn test_cast_vote_with_strict_vote_tipped_to_defeated() { // Act governance_test - .with_cast_vote( + .with_cast_yes_no_vote( &proposal_cookie, &token_owner_record_cookie1, YesNoVote::Yes, @@ -506,7 +506,7 @@ async fn test_cast_vote_with_strict_vote_tipped_to_defeated() { // Act governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie2, YesNoVote::No) .await .unwrap(); @@ -520,7 +520,7 @@ async fn test_cast_vote_with_strict_vote_tipped_to_defeated() { // Act governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie3, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie3, YesNoVote::No) .await .unwrap(); @@ -597,7 +597,7 @@ async fn test_cast_vote_with_early_vote_tipped_to_succeeded() { .await .unwrap(); governance_test - .with_cast_vote( + .with_cast_yes_no_vote( &proposal_cookie, &token_owner_record_cookie1, YesNoVote::Yes, @@ -610,7 +610,7 @@ async fn test_cast_vote_with_early_vote_tipped_to_succeeded() { assert_eq!(ProposalState::Voting, proposal_account.state); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie2, YesNoVote::No) .await .unwrap(); let proposal_account = governance_test @@ -619,7 +619,7 @@ async fn test_cast_vote_with_early_vote_tipped_to_succeeded() { assert_eq!(ProposalState::Voting, proposal_account.state); governance_test - .with_cast_vote( + .with_cast_yes_no_vote( &proposal_cookie, &token_owner_record_cookie3, YesNoVote::Yes, @@ -641,7 +641,7 @@ async fn test_cast_vote_with_early_vote_tipped_to_succeeded() { .await .unwrap(); governance_test - .with_cast_vote( + .with_cast_yes_no_vote( &proposal_cookie, &token_owner_record_cookie1, YesNoVote::Yes, @@ -654,7 +654,7 @@ async fn test_cast_vote_with_early_vote_tipped_to_succeeded() { assert_eq!(ProposalState::Voting, proposal_account.state); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie2, YesNoVote::No) .await .unwrap(); let proposal_account = governance_test @@ -663,7 +663,7 @@ async fn test_cast_vote_with_early_vote_tipped_to_succeeded() { assert_eq!(ProposalState::Voting, proposal_account.state); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie3, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie3, YesNoVote::No) .await .unwrap(); let proposal_account = governance_test @@ -672,7 +672,7 @@ async fn test_cast_vote_with_early_vote_tipped_to_succeeded() { assert_eq!(ProposalState::Voting, proposal_account.state); governance_test - .with_cast_vote( + .with_cast_yes_no_vote( &proposal_cookie, &token_owner_record_cookie4, YesNoVote::Yes, @@ -686,7 +686,7 @@ async fn test_cast_vote_with_early_vote_tipped_to_succeeded() { // Act: 300 vs 200 makes it tip governance_test - .with_cast_vote( + .with_cast_yes_no_vote( &proposal_cookie, &token_owner_record_cookie5, YesNoVote::Yes, @@ -756,7 +756,7 @@ async fn test_cast_vote_with_early_vote_tipped_to_defeated() { // Act governance_test - .with_cast_vote( + .with_cast_yes_no_vote( &proposal_cookie, &token_owner_record_cookie1, YesNoVote::Yes, @@ -774,7 +774,7 @@ async fn test_cast_vote_with_early_vote_tipped_to_defeated() { // Act governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie2, YesNoVote::No) .await .unwrap(); @@ -788,7 +788,7 @@ async fn test_cast_vote_with_early_vote_tipped_to_defeated() { // Act governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie3, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie3, YesNoVote::No) .await .unwrap(); @@ -846,7 +846,7 @@ async fn test_cast_vote_with_threshold_below_50_and_vote_not_tipped() { // Act governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -915,7 +915,7 @@ async fn test_cast_vote_with_disabled_tipping_yes_votes() { // Act governance_test - .with_cast_vote( + .with_cast_yes_no_vote( &proposal_cookie, &token_owner_record_cookie1, YesNoVote::Yes, @@ -936,7 +936,7 @@ async fn test_cast_vote_with_disabled_tipping_yes_votes() { .await .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie1, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie1, YesNoVote::No) .await .unwrap(); @@ -986,7 +986,7 @@ async fn test_cast_vote_with_disabled_tipping_no_votes() { // Act governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie1, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie1, YesNoVote::No) .await .unwrap(); @@ -1039,7 +1039,7 @@ async fn test_cast_vote_with_voting_time_expired_error() { // Act let err = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) .await .err() .unwrap(); @@ -1081,7 +1081,7 @@ async fn test_cast_vote_with_cast_twice_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -1089,7 +1089,7 @@ async fn test_cast_vote_with_cast_twice_error() { // Act let err = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .err() .unwrap(); @@ -1130,7 +1130,7 @@ async fn test_cast_vote_with_invalid_proposal_owner_error() { // Act let err = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .err() .unwrap(); @@ -1172,7 +1172,7 @@ async fn test_cast_tipping_vote_with_invalid_proposal_owner_error() { .unwrap(); governance_test - .with_cast_vote( + .with_cast_yes_no_vote( &proposal_cookie, &token_owner_record_cookie2, YesNoVote::Yes, @@ -1185,7 +1185,7 @@ async fn test_cast_tipping_vote_with_invalid_proposal_owner_error() { // Act let err = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .err() .unwrap(); @@ -1226,7 +1226,7 @@ async fn test_cast_council_vote() { // Act governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); diff --git a/governance/program/tests/process_create_native_treasury.rs b/governance/program/tests/process_create_native_treasury.rs index c0170596..e44f0527 100644 --- a/governance/program/tests/process_create_native_treasury.rs +++ b/governance/program/tests/process_create_native_treasury.rs @@ -98,7 +98,7 @@ async fn test_execute_transfer_from_native_treasury() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); diff --git a/governance/program/tests/process_create_token_owner_record.rs b/governance/program/tests/process_create_token_owner_record.rs index 2172cd15..2b81bd6d 100644 --- a/governance/program/tests/process_create_token_owner_record.rs +++ b/governance/program/tests/process_create_token_owner_record.rs @@ -13,7 +13,9 @@ async fn test_create_token_owner_record() { let realm_cookie = governance_test.with_realm().await; // Act - let token_owner_record_cookie = governance_test.with_token_owner_record(&realm_cookie).await; + let token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; // Assert let token_owner_record_account = governance_test diff --git a/governance/program/tests/process_execute_transaction.rs b/governance/program/tests/process_execute_transaction.rs index d8b43452..1dd244e7 100644 --- a/governance/program/tests/process_execute_transaction.rs +++ b/governance/program/tests/process_execute_transaction.rs @@ -64,7 +64,7 @@ async fn test_execute_mint_transaction() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -163,7 +163,7 @@ async fn test_execute_transfer_transaction() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -261,7 +261,7 @@ async fn test_execute_upgrade_program_transaction() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -447,7 +447,7 @@ async fn test_execute_proposal_transaction_with_invalid_state_errors() { // Arrange governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -553,7 +553,7 @@ async fn test_execute_proposal_transaction_for_other_proposal_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -643,7 +643,7 @@ async fn test_execute_mint_transaction_twice_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); diff --git a/governance/program/tests/process_finalize_vote.rs b/governance/program/tests/process_finalize_vote.rs index cdaa8f75..223110a7 100644 --- a/governance/program/tests/process_finalize_vote.rs +++ b/governance/program/tests/process_finalize_vote.rs @@ -48,7 +48,7 @@ async fn test_finalize_vote_to_succeeded() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -147,7 +147,7 @@ async fn test_finalize_vote_to_defeated() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) .await .unwrap(); @@ -215,7 +215,7 @@ async fn test_finalize_vote_with_invalid_mint_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) .await .unwrap(); @@ -275,7 +275,7 @@ async fn test_finalize_vote_with_invalid_governance_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) .await .unwrap(); @@ -353,7 +353,7 @@ async fn test_finalize_council_vote() { // Cast vote with 47% weight, above 40% quorum but below 50%+1 to tip automatically governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); diff --git a/governance/program/tests/process_flag_transaction_error.rs b/governance/program/tests/process_flag_transaction_error.rs index ede09820..49d6f246 100644 --- a/governance/program/tests/process_flag_transaction_error.rs +++ b/governance/program/tests/process_flag_transaction_error.rs @@ -53,7 +53,7 @@ async fn test_execute_flag_transaction_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -148,7 +148,7 @@ async fn test_execute_proposal_transaction_after_flagged_with_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -244,7 +244,7 @@ async fn test_execute_second_transaction_after_first_transaction_flagged_with_er .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -332,7 +332,7 @@ async fn test_flag_transaction_error_with_proposal_transaction_already_executed_ .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -406,7 +406,7 @@ async fn test_flag_transaction_error_with_owner_or_delegate_must_sign_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); diff --git a/governance/program/tests/process_relinquish_vote.rs b/governance/program/tests/process_relinquish_vote.rs index e9424133..75d9f05b 100644 --- a/governance/program/tests/process_relinquish_vote.rs +++ b/governance/program/tests/process_relinquish_vote.rs @@ -36,7 +36,7 @@ async fn test_relinquish_voted_proposal() { .unwrap(); let mut vote_record_cookie = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -103,7 +103,7 @@ async fn test_relinquish_active_yes_vote() { .unwrap(); let vote_record_cookie = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -171,7 +171,7 @@ async fn test_relinquish_active_no_vote() { .unwrap(); let vote_record_cookie = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) .await .unwrap(); @@ -214,7 +214,7 @@ async fn test_relinquish_vote_with_invalid_mint_error() { 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 + let mut token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) .await .unwrap(); @@ -228,17 +228,17 @@ async fn test_relinquish_vote_with_invalid_mint_error() { .await .unwrap(); - let mut proposal_cookie = governance_test + let proposal_cookie = governance_test .with_signed_off_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) .await .unwrap(); - proposal_cookie.account.governing_token_mint = Pubkey::new_unique(); + token_owner_record_cookie.account.governing_token_mint = Pubkey::new_unique(); // Act @@ -250,7 +250,7 @@ async fn test_relinquish_vote_with_invalid_mint_error() { // Assert - assert_eq!(err, GovernanceError::InvalidGoverningMintForProposal.into()); + assert_eq!(err, GovernanceError::InvalidGoverningTokenMint.into()); } #[tokio::test] @@ -286,7 +286,7 @@ async fn test_relinquish_vote_with_governance_authority_must_sign_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) .await .unwrap(); @@ -352,12 +352,12 @@ async fn test_relinquish_vote_with_invalid_vote_record_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) .await .unwrap(); let vote_record_cookie2 = governance_test - .with_cast_vote( + .with_cast_yes_no_vote( &proposal_cookie, &token_owner_record_cookie2, YesNoVote::Yes, @@ -369,7 +369,7 @@ async fn test_relinquish_vote_with_invalid_vote_record_error() { let err = governance_test .relinquish_vote_using_instruction(&proposal_cookie, &token_owner_record_cookie, |i| { - i.accounts[3] = AccountMeta::new(vote_record_cookie2.address, false) + i.accounts[4] = AccountMeta::new(vote_record_cookie2.address, false) // Try to use a vote_record for other token owner }) .await @@ -412,7 +412,7 @@ async fn test_relinquish_vote_with_already_relinquished_error() { .unwrap(); let vote_record_cookie = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) .await .unwrap(); @@ -482,7 +482,7 @@ async fn test_relinquish_proposal_in_voting_state_after_vote_time_ended() { let clock = governance_test.bench.get_clock().await; let mut vote_record_cookie = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); diff --git a/governance/program/tests/process_set_governance_config.rs b/governance/program/tests/process_set_governance_config.rs index e87f0909..2b6442ff 100644 --- a/governance/program/tests/process_set_governance_config.rs +++ b/governance/program/tests/process_set_governance_config.rs @@ -64,7 +64,7 @@ async fn test_set_governance_config() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -217,7 +217,7 @@ async fn test_set_governance_config_with_invalid_governance_authority_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); diff --git a/governance/program/tests/process_withdraw_governing_tokens.rs b/governance/program/tests/process_withdraw_governing_tokens.rs index b9f44e2a..7702a60b 100644 --- a/governance/program/tests/process_withdraw_governing_tokens.rs +++ b/governance/program/tests/process_withdraw_governing_tokens.rs @@ -207,7 +207,7 @@ async fn test_withdraw_governing_tokens_with_unrelinquished_votes_error() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -253,7 +253,7 @@ async fn test_withdraw_governing_tokens_after_relinquishing_vote() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); diff --git a/governance/program/tests/program_test/args.rs b/governance/program/tests/program_test/args.rs index 2d90dde6..75dd8c58 100644 --- a/governance/program/tests/program_test/args.rs +++ b/governance/program/tests/program_test/args.rs @@ -1,5 +1,5 @@ use solana_program::pubkey::Pubkey; -use spl_governance::state::realm::RealmConfigArgs; +use spl_governance::state::{enums::MintMaxVoteWeightSource, realm::RealmConfigArgs}; #[derive(Clone, Debug, PartialEq)] pub struct SetRealmConfigArgs { @@ -7,3 +7,22 @@ pub struct SetRealmConfigArgs { pub community_voter_weight_addin: Option, pub max_community_voter_weight_addin: Option, } + +impl Default for SetRealmConfigArgs { + fn default() -> Self { + let realm_config_args = RealmConfigArgs { + use_council_mint: true, + + community_mint_max_vote_weight_source: MintMaxVoteWeightSource::SupplyFraction(100), + min_community_weight_to_create_governance: 10, + use_community_voter_weight_addin: false, + use_max_community_voter_weight_addin: false, + }; + + Self { + realm_config_args, + community_voter_weight_addin: None, + max_community_voter_weight_addin: None, + } + } +} diff --git a/governance/program/tests/program_test/mod.rs b/governance/program/tests/program_test/mod.rs index be5c66ba..5f2da4cc 100644 --- a/governance/program/tests/program_test/mod.rs +++ b/governance/program/tests/program_test/mod.rs @@ -448,12 +448,13 @@ impl GovernanceProgramTest { &realm_cookie.account.community_mint, &realm_cookie.community_mint_authority, 100, + None, ) .await } #[allow(dead_code)] - pub async fn with_token_owner_record( + pub async fn with_community_token_owner_record( &mut self, realm_cookie: &RealmCookie, ) -> TokenOwnerRecordCookie { @@ -576,6 +577,7 @@ impl GovernanceProgramTest { &realm_cookie.account.community_mint, &realm_cookie.community_mint_authority, amount, + None, ) .await } @@ -625,6 +627,7 @@ impl GovernanceProgramTest { &realm_cookie.account.config.council_mint.unwrap(), &realm_cookie.council_mint_authority.as_ref().unwrap(), amount, + None, ) .await } @@ -639,6 +642,24 @@ impl GovernanceProgramTest { &realm_cookie.account.config.council_mint.unwrap(), realm_cookie.council_mint_authority.as_ref().unwrap(), 100, + None, + ) + .await + } + + #[allow(dead_code)] + pub async fn with_community_token_deposit_by_owner( + &mut self, + realm_cookie: &RealmCookie, + amount: u64, + token_owner: Keypair, + ) -> Result { + self.with_initial_governing_token_deposit( + &realm_cookie.address, + &realm_cookie.account.community_mint, + &realm_cookie.community_mint_authority, + amount, + Some(token_owner), ) .await } @@ -650,8 +671,9 @@ impl GovernanceProgramTest { governing_mint: &Pubkey, governing_mint_authority: &Keypair, amount: u64, + token_owner: Option, ) -> Result { - let token_owner = Keypair::new(); + let token_owner = token_owner.unwrap_or(Keypair::new()); let token_source = Keypair::new(); let transfer_authority = Keypair::new(); @@ -1194,7 +1216,7 @@ impl GovernanceProgramTest { community_vote_threshold: VoteThreshold::YesVotePercentage(60), vote_tipping: spl_governance::state::enums::VoteTipping::Strict, council_vote_threshold: VoteThreshold::YesVotePercentage(80), - reserved: [0; 2], + council_veto_vote_threshold: VoteThreshold::YesVotePercentage(55), } } @@ -1833,7 +1855,7 @@ impl GovernanceProgramTest { options: proposal_options, deny_vote_weight, - veto_vote_weight: None, + veto_vote_weight: 0, abstain_vote_weight: None, execution_flags: InstructionExecutionFlags::None, @@ -1842,6 +1864,7 @@ impl GovernanceProgramTest { vote_threshold: None, reserved: [0; 64], + reserved1: 0, }; let proposal_address = get_proposal_address( @@ -2068,10 +2091,11 @@ impl GovernanceProgramTest { ) -> Result<(), ProgramError> { let mut relinquish_vote_ix = relinquish_vote( &self.program_id, + &token_owner_record_cookie.account.realm, &proposal_cookie.account.governance, &proposal_cookie.address, &token_owner_record_cookie.address, - &proposal_cookie.account.governing_token_mint, + &token_owner_record_cookie.account.governing_token_mint, Some(token_owner_record_cookie.token_owner.pubkey()), Some(self.bench.payer.pubkey()), ); @@ -2114,7 +2138,7 @@ impl GovernanceProgramTest { } #[allow(dead_code)] - pub async fn with_cast_vote( + pub async fn with_cast_yes_no_vote( &mut self, proposal_cookie: &ProposalCookie, token_owner_record_cookie: &TokenOwnerRecordCookie, @@ -2128,12 +2152,12 @@ impl GovernanceProgramTest { YesNoVote::No => Vote::Deny, }; - self.with_cast_multi_option_vote(proposal_cookie, token_owner_record_cookie, vote) + self.with_cast_vote(proposal_cookie, token_owner_record_cookie, vote) .await } #[allow(dead_code)] - pub async fn with_cast_multi_option_vote( + pub async fn with_cast_vote( &mut self, proposal_cookie: &ProposalCookie, token_owner_record_cookie: &TokenOwnerRecordCookie, @@ -2162,7 +2186,7 @@ impl GovernanceProgramTest { &proposal_cookie.account.token_owner_record, &token_owner_record_cookie.address, &token_owner_record_cookie.token_owner.pubkey(), - &proposal_cookie.account.governing_token_mint, + &token_owner_record_cookie.account.governing_token_mint, &self.bench.payer.pubkey(), voter_weight_record, max_voter_weight_record, diff --git a/governance/program/tests/use_proposals_with_multiple_options.rs b/governance/program/tests/use_proposals_with_multiple_options.rs index 9c3e8d67..d5d10d0a 100644 --- a/governance/program/tests/use_proposals_with_multiple_options.rs +++ b/governance/program/tests/use_proposals_with_multiple_options.rs @@ -291,7 +291,7 @@ async fn test_vote_on_none_executable_single_choice_proposal_with_multiple_optio // Act governance_test - .with_cast_multi_option_vote(&proposal_cookie, &token_owner_record_cookie, vote) + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, vote) .await .unwrap(); @@ -396,7 +396,7 @@ async fn test_vote_on_none_executable_multi_choice_proposal_with_multiple_option // Act governance_test - .with_cast_multi_option_vote(&proposal_cookie, &token_owner_record_cookie, vote) + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, vote) .await .unwrap(); @@ -531,7 +531,7 @@ async fn test_vote_on_executable_proposal_with_multiple_options_and_partial_succ ]); governance_test - .with_cast_multi_option_vote(&proposal_cookie, &token_owner_record_cookie1, vote1) + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie1, vote1) .await .unwrap(); @@ -551,12 +551,12 @@ async fn test_vote_on_executable_proposal_with_multiple_options_and_partial_succ ]); governance_test - .with_cast_multi_option_vote(&proposal_cookie, &token_owner_record_cookie2, vote2) + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, vote2) .await .unwrap(); governance_test - .with_cast_multi_option_vote(&proposal_cookie, &token_owner_record_cookie3, Vote::Deny) + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie3, Vote::Deny) .await .unwrap(); @@ -706,7 +706,7 @@ async fn test_execute_proposal_with_multiple_options_and_partial_success() { // yes threshold: 100 governance_test - .with_cast_multi_option_vote(&proposal_cookie, &token_owner_record_cookie3, Vote::Deny) + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie3, Vote::Deny) .await .unwrap(); @@ -726,7 +726,7 @@ async fn test_execute_proposal_with_multiple_options_and_partial_success() { ]); governance_test - .with_cast_multi_option_vote(&proposal_cookie, &token_owner_record_cookie1, vote1) + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie1, vote1) .await .unwrap(); @@ -746,7 +746,7 @@ async fn test_execute_proposal_with_multiple_options_and_partial_success() { ]); governance_test - .with_cast_multi_option_vote(&proposal_cookie, &token_owner_record_cookie2, vote2) + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, vote2) .await .unwrap(); @@ -904,12 +904,12 @@ async fn test_try_execute_proposal_with_multiple_options_and_full_deny() { .unwrap(); governance_test - .with_cast_multi_option_vote(&proposal_cookie, &token_owner_record_cookie1, Vote::Deny) + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie1, Vote::Deny) .await .unwrap(); governance_test - .with_cast_multi_option_vote(&proposal_cookie, &token_owner_record_cookie2, Vote::Deny) + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, Vote::Deny) .await .unwrap(); @@ -1039,7 +1039,7 @@ async fn test_create_proposal_with_10_options_and_cast_vote() { // Act governance_test - .with_cast_multi_option_vote(&proposal_cookie, &token_owner_record_cookie, vote) + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, vote) .await .unwrap(); diff --git a/governance/program/tests/use_realm_with_all_addins.rs b/governance/program/tests/use_realm_with_all_addins.rs index 2f08914f..ab566a23 100644 --- a/governance/program/tests/use_realm_with_all_addins.rs +++ b/governance/program/tests/use_realm_with_all_addins.rs @@ -14,8 +14,9 @@ async fn test_cast_vote_with_all_addin() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; // voter weight 120 governance_test @@ -48,7 +49,7 @@ async fn test_cast_vote_with_all_addin() { // Act let vote_record_cookie = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -73,8 +74,9 @@ async fn test_tip_vote_with_all_addin() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; // voter weight 120 governance_test @@ -107,7 +109,7 @@ async fn test_tip_vote_with_all_addin() { // Act let vote_record_cookie = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) .await .unwrap(); @@ -132,8 +134,9 @@ async fn test_finalize_vote_with_all_addin() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; // voter weight 120 governance_test @@ -164,7 +167,7 @@ async fn test_finalize_vote_with_all_addin() { .unwrap(); let vote_record_cookie = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::No) .await .unwrap(); diff --git a/governance/program/tests/use_realm_with_max_voter_weight_addin.rs b/governance/program/tests/use_realm_with_max_voter_weight_addin.rs index 81b78ac1..75df30e8 100644 --- a/governance/program/tests/use_realm_with_max_voter_weight_addin.rs +++ b/governance/program/tests/use_realm_with_max_voter_weight_addin.rs @@ -43,7 +43,7 @@ async fn test_cast_vote_with_max_voter_weight_addin() { // Act governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -92,7 +92,7 @@ async fn test_tip_vote_with_max_voter_weight_addin() { // Act governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -142,7 +142,7 @@ async fn test_tip_vote_with_max_voter_weight_addin_and_max_below_total_cast_vote // Act governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -191,7 +191,7 @@ async fn test_finalize_vote_with_max_voter_weight_addin() { .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -262,7 +262,7 @@ async fn test_finalize_vote_with_max_voter_weight_addin_and_max_below_total_cast .unwrap(); governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -336,7 +336,7 @@ async fn test_cast_vote_with_max_voter_weight_addin_and_expired_record_error() { // Act let err = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .err() .unwrap(); diff --git a/governance/program/tests/use_realm_with_voter_weight_addin.rs b/governance/program/tests/use_realm_with_voter_weight_addin.rs index 8bdf20bb..4a7428f8 100644 --- a/governance/program/tests/use_realm_with_voter_weight_addin.rs +++ b/governance/program/tests/use_realm_with_voter_weight_addin.rs @@ -20,8 +20,9 @@ async fn test_create_governance_with_voter_weight_addin() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; governance_test .with_voter_weight_addin_record(&mut token_owner_record_cookie) @@ -54,8 +55,9 @@ async fn test_create_proposal_with_voter_weight_addin() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; governance_test .with_voter_weight_addin_record(&mut token_owner_record_cookie) @@ -93,8 +95,9 @@ async fn test_cast_vote_with_voter_weight_addin() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; governance_test .with_voter_weight_addin_record(&mut token_owner_record_cookie) @@ -117,7 +120,7 @@ async fn test_cast_vote_with_voter_weight_addin() { // Act let vote_record_cookie = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .unwrap(); @@ -151,8 +154,9 @@ async fn test_create_token_governance_with_voter_weight_addin() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; governance_test .with_voter_weight_addin_record(&mut token_owner_record_cookie) @@ -185,8 +189,9 @@ async fn test_create_mint_governance_with_voter_weight_addin() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; governance_test .with_voter_weight_addin_record(&mut token_owner_record_cookie) @@ -219,8 +224,9 @@ async fn test_create_program_governance_with_voter_weight_addin() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; governance_test .with_voter_weight_addin_record(&mut token_owner_record_cookie) @@ -277,8 +283,9 @@ async fn test_create_governance_with_voter_weight_action_error() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; governance_test .with_voter_weight_addin_record_impl( @@ -314,8 +321,9 @@ async fn test_create_governance_with_voter_weight_expiry_error() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; governance_test .with_voter_weight_addin_record_impl( @@ -353,8 +361,9 @@ async fn test_cast_vote_with_voter_weight_action_error() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; governance_test .with_voter_weight_addin_record_impl(&mut token_owner_record_cookie, 100, None, None, None) @@ -391,7 +400,7 @@ async fn test_cast_vote_with_voter_weight_action_error() { // Act let err = governance_test - .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) .await .err() .unwrap(); @@ -408,8 +417,9 @@ async fn test_create_governance_with_voter_weight_action_target_error() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; governance_test .with_voter_weight_addin_record_impl( @@ -450,8 +460,9 @@ async fn test_create_proposal_with_voter_weight_action_error() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; governance_test .with_voter_weight_addin_record_impl( @@ -493,8 +504,9 @@ async fn test_create_governance_with_voter_weight_record() { let realm_cookie = governance_test.with_realm().await; - let mut token_owner_record_cookie = - governance_test.with_token_owner_record(&realm_cookie).await; + let mut token_owner_record_cookie = governance_test + .with_community_token_owner_record(&realm_cookie) + .await; governance_test.advance_clock().await; let clock = governance_test.bench.get_clock().await; diff --git a/governance/program/tests/use_veto_vote.rs b/governance/program/tests/use_veto_vote.rs new file mode 100644 index 00000000..2b80a3ff --- /dev/null +++ b/governance/program/tests/use_veto_vote.rs @@ -0,0 +1,719 @@ +#![cfg(feature = "test-bpf")] + +mod program_test; + +use solana_program::instruction::AccountMeta; +use solana_program_test::tokio; + +use program_test::*; +use spl_governance::{ + error::GovernanceError, + state::{ + enums::{ProposalState, VoteThreshold}, + vote_record::Vote, + }, +}; +use spl_governance_test_sdk::tools::clone_keypair; + +use self::args::SetRealmConfigArgs; + +#[tokio::test] +async fn test_cast_veto_vote() { + // 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; + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .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(); + + let clock = governance_test.bench.get_clock().await; + + // Act + let vote_record_cookie = governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Veto) + .await + .unwrap(); + + // Assert + let vote_record_account = governance_test + .get_vote_record_account(&vote_record_cookie.address) + .await; + + assert_eq!(vote_record_cookie.account, vote_record_account); + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!( + token_owner_record_cookie + .account + .governing_token_deposit_amount, + proposal_account.veto_vote_weight + ); + + assert_eq!(proposal_account.state, ProposalState::Vetoed); + assert_eq!( + proposal_account.voting_completed_at, + Some(clock.unix_timestamp) + ); + + assert_eq!(Some(120), proposal_account.max_vote_weight); + assert_eq!( + Some(governance_cookie.account.config.council_veto_vote_threshold), + proposal_account.vote_threshold + ); + + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(1, token_owner_record.unrelinquished_votes_count); + assert_eq!(1, token_owner_record.total_votes_count); + + let realm_account = governance_test + .get_realm_account(&realm_cookie.address) + .await; + + assert_eq!(0, realm_account.voting_proposal_count); + + let governance_account = governance_test + .get_governance_account(&governance_cookie.address) + .await; + + assert_eq!(0, governance_account.voting_proposal_count); +} + +#[tokio::test] +async fn test_cast_veto_vote_with_community_not_allowed_to_vote_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(); + + let proposal_owner_record_cookie = governance_test + .with_council_token_deposit(&realm_cookie) + .await + .unwrap(); + + let proposal_cookie = governance_test + .with_signed_off_proposal(&proposal_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + // Act + let err = governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Veto) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::GoverningTokenMintNotAllowedToVote.into() + ); +} + +#[tokio::test] +async fn test_cast_veto_vote_with_invalid_voting_mint_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_council_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(); + + let proposal_owner_record_cookie = governance_test + .with_council_token_deposit(&realm_cookie) + .await + .unwrap(); + + let proposal_cookie = governance_test + .with_signed_off_proposal(&proposal_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + // Act + + // Try to use Council Veto on Council vote Proposal + let err = governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Veto) + .await + .err() + .unwrap(); + + // Assert + assert_eq!(err, GovernanceError::InvalidGoverningMintForProposal.into()); +} + +#[tokio::test] +async fn test_cast_veto_vote_with_council_veto_vote_disabled_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_council_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_config = governance_test.get_default_governance_config(); + governance_config.council_veto_vote_threshold = VoteThreshold::Disabled; + + 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(); + + // Act + let err = governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Veto) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::GoverningTokenMintNotAllowedToVote.into() + ); +} + +#[tokio::test] +async fn test_cast_veto_vote_without_tipping() { + // 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 201 to prevent tipping + governance_test + .mint_council_tokens(&realm_cookie, 101) + .await; + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .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(); + + // Act + let vote_record_cookie = governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Veto) + .await + .unwrap(); + + // Assert + let vote_record_account = governance_test + .get_vote_record_account(&vote_record_cookie.address) + .await; + + assert_eq!(vote_record_cookie.account, vote_record_account); + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!( + token_owner_record_cookie + .account + .governing_token_deposit_amount, + proposal_account.veto_vote_weight + ); + + assert_eq!(proposal_account.state, ProposalState::Voting); +} + +#[tokio::test] +async fn test_cast_multiple_veto_votes_for_partially_approved_proposal() { + // 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(); + + let token_owner_record_cookie2 = governance_test + .with_council_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Mint extra council tokens for total supply of 210 to prevent single vote tipping + governance_test.mint_council_tokens(&realm_cookie, 10).await; + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let proposal_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Mint extra council tokens for total supply of 200 to prevent single vote tipping + governance_test + .mint_community_tokens(&realm_cookie, 100) + .await; + + let proposal_cookie = governance_test + .with_signed_off_proposal(&proposal_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + // Partially approve Proposal + governance_test + .with_cast_yes_no_vote( + &proposal_cookie, + &proposal_owner_record_cookie, + YesNoVote::Yes, + ) + .await + .unwrap(); + + // Partially Veto Proposal + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Veto) + .await + .unwrap(); + + // Act + + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, Vote::Veto) + .await + .unwrap(); + + // Assert + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(200, proposal_account.veto_vote_weight); + + assert_eq!(proposal_account.state, ProposalState::Vetoed); +} + +#[tokio::test] +async fn test_cast_veto_vote_with_no_council_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut 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(); + + let mut governance_config = governance_test.get_default_governance_config(); + governance_config.council_veto_vote_threshold = VoteThreshold::Disabled; + + 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(); + + // Remove Council + let mut set_realm_config_args = SetRealmConfigArgs::default(); + set_realm_config_args.realm_config_args.use_council_mint = false; + + governance_test + .set_realm_config(&mut realm_cookie, &set_realm_config_args) + .await + .unwrap(); + + // Act + let err = governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Veto) + .await + .err() + .unwrap(); + + // Assert + assert_eq!(err, GovernanceError::InvalidGoverningTokenMint.into()); +} + +#[tokio::test] +async fn test_relinquish_veto_vote() { + // 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 201 to prevent tipping + governance_test + .mint_council_tokens(&realm_cookie, 101) + .await; + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .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(); + + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Veto) + .await + .unwrap(); + // Act + + governance_test + .relinquish_vote(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + // Assert + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(0, proposal_account.veto_vote_weight); + + assert_eq!(proposal_account.state, ProposalState::Voting); +} + +#[tokio::test] +async fn test_relinquish_veto_vote_with_vote_record_for_different_voting_mint_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 council_token_owner_record_cookie = governance_test + .with_council_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Mint extra council tokens for total supply of 210 + governance_test + .mint_council_tokens(&realm_cookie, 110) + .await; + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &council_token_owner_record_cookie, + ) + .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(); + + governance_test + .with_cast_vote( + &proposal_cookie, + &council_token_owner_record_cookie, + Vote::Veto, + ) + .await + .unwrap(); + + // Create Community TokenOwnerRecord for council_token_owner and Cast Community vote + let community_token_owner_record_cookie = governance_test + .with_community_token_deposit_by_owner( + &realm_cookie, + 100, + clone_keypair(&council_token_owner_record_cookie.token_owner), + ) + .await + .unwrap(); + + // Mint extra council tokens for total supply of 250 + governance_test + .mint_community_tokens(&realm_cookie, 150) + .await; + + let community_vote_record_cookie = governance_test + .with_cast_yes_no_vote( + &proposal_cookie, + &community_token_owner_record_cookie, + YesNoVote::Yes, + ) + .await + .unwrap(); + + // Act + + let err = governance_test + .relinquish_vote_using_instruction( + &proposal_cookie, + &council_token_owner_record_cookie, + |i| { + // Try to use a vote_record from community Yes vote to relinquish council Veto vote + i.accounts[4] = AccountMeta::new(community_vote_record_cookie.address, false) + }, + ) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::InvalidGoverningMintForProposal.into()); +} + +#[tokio::test] +async fn test_cast_veto_vote_with_council_only_allowed_to_veto() { + // 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(); + + // Allow Council to cast only Veto votes + let mut governance_config = governance_test.get_default_governance_config(); + governance_config.council_vote_threshold = VoteThreshold::Disabled; + + 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(); + + // 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); +} + +#[tokio::test] +async fn test_cast_yes_and_veto_votes_with_yes_as_winning_vote() { + // 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 210 to prevent single vote tipping + governance_test + .mint_council_tokens(&realm_cookie, 110) + .await; + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .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(); + + // Partially Veto Proposal + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Veto) + .await + .unwrap(); + + // Act + + // Approve Proposal + governance_test + .with_cast_yes_no_vote( + &proposal_cookie, + &proposal_owner_record_cookie, + YesNoVote::Yes, + ) + .await + .unwrap(); + + // Assert + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(100, proposal_account.veto_vote_weight); + + assert_eq!(proposal_account.state, ProposalState::Succeeded); +} + +// TODO: Once Veto for Community or plugin support for Council is implemented write Veto tests with plugin +// The tests should cover scenarios where Veto voter_weight and/or max_voter_weight is resolved using the plugins