From 94350d0e8e0ef22cb62ea64be0176bba6d24fef4 Mon Sep 17 00:00:00 2001 From: Sebastian Bor Date: Wed, 26 May 2021 16:40:18 +0100 Subject: [PATCH] Governance: Create Proposals and Sign Off workflow (#1767) Co-authored-by: Jon Cinque --- Cargo.lock | 1 + governance/program/Cargo.toml | 1 + governance/program/src/error.rs | 56 ++- governance/program/src/instruction.rs | 317 ++++++++++---- governance/program/src/processor/mod.rs | 49 ++- .../src/processor/process_add_signatory.rs | 76 ++++ .../process_create_account_governance.rs | 2 +- .../process_create_program_governance.rs | 2 +- .../src/processor/process_create_proposal.rs | 115 +++++ .../process_deposit_governing_tokens.rs | 54 ++- .../src/processor/process_remove_signatory.rs | 68 +++ .../process_set_governance_delegate.rs | 33 ++ .../processor/process_set_vote_authority.rs | 39 -- .../processor/process_sign_off_proposal.rs | 58 +++ .../process_withdraw_governing_tokens.rs | 24 +- governance/program/src/state/enums.rs | 22 +- governance/program/src/state/governance.rs | 12 +- governance/program/src/state/mod.rs | 3 +- governance/program/src/state/proposal.rs | 241 ++++++++-- .../program/src/state/proposal_vote_record.rs | 2 +- governance/program/src/state/realm.rs | 23 +- .../program/src/state/signatory_record.rs | 108 +++++ .../src/state/single_signer_instruction.rs | 2 +- .../program/src/state/token_owner_record.rs | 166 +++++++ governance/program/src/state/vote_record.rs | 55 +++ governance/program/src/state/voter_record.rs | 126 ------ governance/program/src/tools/account.rs | 22 +- governance/program/src/tools/asserts.rs | 20 +- .../src/tools/bpf_loader_upgradeable.rs | 30 +- governance/program/src/tools/token.rs | 4 +- .../program/tests/process_add_signatory.rs | 132 ++++++ .../program/tests/process_create_proposal.rs | 255 +++++++++++ .../tests/process_deposit_governing_tokens.rs | 76 ++-- .../program/tests/process_remove_signatory.rs | 219 ++++++++++ .../tests/process_set_governance_delegate.rs | 165 +++++++ .../tests/process_set_vote_authority.rs | 165 ------- .../tests/process_sign_off_proposal.rs | 59 +++ .../process_withdraw_governing_tokens.rs | 49 ++- .../program/tests/program_test/cookies.rs | 46 +- governance/program/tests/program_test/mod.rs | 410 ++++++++++++------ .../program/tests/program_test/tools.rs | 10 +- 41 files changed, 2571 insertions(+), 746 deletions(-) create mode 100644 governance/program/src/processor/process_add_signatory.rs create mode 100644 governance/program/src/processor/process_create_proposal.rs create mode 100644 governance/program/src/processor/process_remove_signatory.rs create mode 100644 governance/program/src/processor/process_set_governance_delegate.rs delete mode 100644 governance/program/src/processor/process_set_vote_authority.rs create mode 100644 governance/program/src/processor/process_sign_off_proposal.rs create mode 100644 governance/program/src/state/signatory_record.rs create mode 100644 governance/program/src/state/token_owner_record.rs create mode 100644 governance/program/src/state/vote_record.rs delete mode 100644 governance/program/src/state/voter_record.rs create mode 100644 governance/program/tests/process_add_signatory.rs create mode 100644 governance/program/tests/process_create_proposal.rs create mode 100644 governance/program/tests/process_remove_signatory.rs create mode 100644 governance/program/tests/process_set_governance_delegate.rs delete mode 100644 governance/program/tests/process_set_vote_authority.rs create mode 100644 governance/program/tests/process_sign_off_proposal.rs diff --git a/Cargo.lock b/Cargo.lock index 96ff2b6b68c..aad701b1bb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3819,6 +3819,7 @@ dependencies = [ "borsh 0.8.2", "num-derive", "num-traits", + "proptest", "serde", "serde_derive", "solana-program", diff --git a/governance/program/Cargo.toml b/governance/program/Cargo.toml index 96183e65b4c..f5eba1b1b81 100644 --- a/governance/program/Cargo.toml +++ b/governance/program/Cargo.toml @@ -25,6 +25,7 @@ thiserror = "1.0" [dev-dependencies] assert_matches = "1.5.0" +proptest = "0.10" solana-program-test = "1.6.7" solana-sdk = "1.6.7" diff --git a/governance/program/src/error.rs b/governance/program/src/error.rs index 75772478418..8a180cfa92f 100644 --- a/governance/program/src/error.rs +++ b/governance/program/src/error.rs @@ -31,22 +31,66 @@ pub enum GovernanceError { #[error("Governing Token Owner must sign transaction")] GoverningTokenOwnerMustSign, - /// Governing Token Owner or Vote Authority must sign transaction - #[error("Governing Token Owner or Vote Authority must sign transaction")] - GoverningTokenOwnerOrVoteAuthrotiyMustSign, + /// Governing Token Owner or Delegate must sign transaction + #[error("Governing Token Owner or Delegate must sign transaction")] + GoverningTokenOwnerOrDelegateMustSign, /// All active votes must be relinquished to withdraw governing tokens #[error("All active votes must be relinquished to withdraw governing tokens")] CannotWithdrawGoverningTokensWhenActiveVotesExist, - /// Invalid Voter account address - #[error("Invalid Voter account address")] - InvalidVoterAccountAddress, + /// Invalid Token Owner Record account address + #[error("Invalid Token Owner Record account address")] + InvalidTokenOwnerRecordAccountAddress, + + /// Invalid Token Owner Record Governing mint + #[error("Invalid Token Owner Record Governing mint")] + InvalidTokenOwnerRecordGoverningMint, + + /// Invalid Token Owner Record Realm + #[error("Invalid Token Owner Record Realm")] + InvalidTokenOwnerRecordRealm, + + /// Invalid Signatory account address + #[error("Invalid Signatory account address")] + InvalidSignatoryAddress, + + /// Signatory already signed off + #[error("Signatory already signed off")] + SignatoryAlreadySignedOff, + + /// Signatory must sign + #[error("Signatory must sign")] + SignatoryMustSign, + + /// Invalid Proposal Owner + #[error("Invalid Proposal Owner")] + InvalidProposalOwnerAccount, /// Invalid Governance config #[error("Invalid Governance config")] InvalidGovernanceConfig, + /// Proposal for the given Governance, Governing Token Mint and index already exists + #[error("Proposal for the given Governance, Governing Token Mint and index already exists")] + ProposalAlreadyExists, + + /// Owner doesn't have enough governing tokens to create Proposal + #[error("Owner doesn't have enough governing tokens to create Proposal")] + NotEnoughTokensToCreateProposal, + + /// Invalid State: Can't edit Signatories + #[error("Invalid State: Can't edit Signatories")] + InvalidStateCannotEditSignatories, + + /// Invalid State: Can't sign off + #[error("Invalid State: Can't sign off")] + InvalidStateCannotSignOff, + + /// Invalid Signatory Mint + #[error("Invalid Signatory Mint")] + InvalidSignatoryMint, + /// ---- Account Tools Errors ---- /// Invalid account owner diff --git a/governance/program/src/instruction.rs b/governance/program/src/instruction.rs index 6a46c1a246f..5fc25cf96ba 100644 --- a/governance/program/src/instruction.rs +++ b/governance/program/src/instruction.rs @@ -3,13 +3,14 @@ use crate::{ id, state::{ - enums::GoverningTokenType, governance::{ get_account_governance_address, get_program_governance_address, GovernanceConfig, }, + proposal::get_proposal_address, realm::{get_governing_token_holding_address, get_realm_address}, + signatory_record::get_signatory_record_address, single_signer_instruction::InstructionData, - voter_record::get_voter_record_address, + token_owner_record::get_token_owner_record_address, }, tools::bpf_loader_upgradeable::get_program_data_address, }; @@ -64,7 +65,7 @@ pub enum GovernanceInstruction { /// 2. `[writable]` Governing Token Source account. All tokens from the account will be transferred to the Holding account /// 3. `[signer]` Governing Token Owner account /// 4. `[signer]` Governing Token Transfer authority - /// 5. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] + /// 5. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] /// 6. `[signer]` Payer /// 7. `[]` System /// 8. `[]` SPL Token @@ -79,40 +80,30 @@ pub enum GovernanceInstruction { /// 1. `[writable]` Governing Token Holding account. PDA seeds: ['governance',realm, governing_token_mint] /// 2. `[writable]` Governing Token Destination account. All tokens will be transferred to this account /// 3. `[signer]` Governing Token Owner account - /// 4. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] + /// 4. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] /// 5. `[]` SPL Token WithdrawGoverningTokens {}, - /// Sets vote authority for the given Realm and Governing Token Mint (Community or Council) - /// The vote authority would have voting rights and could vote on behalf of the Governing Token Owner - /// Note: This doesn't take voting rights from the Token Owner who still can vote and change vote_authority + /// Sets Governance Delegate for the given Realm and Governing Token Mint (Community or Council) + /// The Delegate would have voting rights and could vote on behalf of the Governing Token Owner + /// The Delegate would also be able to create Proposals on behalf of the Governing Token Owner + /// Note: This doesn't take voting rights from the Token Owner who still can vote and change governance_delegate /// - /// 0. `[signer]` Current Vote authority or Governing Token owner - /// 1. `[writable]` Voter Record - SetVoteAuthority { + /// 0. `[signer]` Current Governance Delegate or Governing Token owner + /// 1. `[writable]` Token Owner Record + SetGovernanceDelegate { #[allow(dead_code)] - /// Governance Realm the new vote authority is set for - realm: Pubkey, - - #[allow(dead_code)] - /// Governing Token Mint the vote authority is granted over - governing_token_mint: Pubkey, - - #[allow(dead_code)] - /// Governing Token Owner the vote authority is set for - governing_token_owner: Pubkey, - - #[allow(dead_code)] - /// New vote authority - new_vote_authority: Option, + /// New Governance Delegate + new_governance_delegate: Option, }, /// Creates Account Governance account which can be used to govern an arbitrary account /// - /// 0. `[writable]` Account Governance account. PDA seeds: ['account-governance', realm, governed_account] - /// 1. `[signer]` Payer - /// 2. `[]` System program - /// 3. `[]` Sysvar Rent + /// 0. `[]` Realm account the created Governance belongs to + /// 1. `[writable]` Account Governance account. PDA seeds: ['account-governance', realm, governed_account] + /// 2. `[signer]` Payer + /// 3. `[]` System program + /// 4. `[]` Sysvar Rent CreateAccountGovernance { /// Governance config #[allow(dead_code)] @@ -121,13 +112,14 @@ pub enum GovernanceInstruction { /// Creates Program Governance account which governs an upgradable program /// - /// 0. `[writable]` Program Governance account. PDA seeds: ['program-governance', realm, governed_program] - /// 1. `[writable]` Program Data account of the Program governed by this Governance account - /// 2. `[signer]` Current Upgrade Authority account of the Program governed by this Governance account - /// 3. `[signer]` Payer - /// 4. `[]` bpf_upgradeable_loader program - /// 5. `[]` System program - /// 6. `[]` Sysvar Rent + /// 0. `[]` Realm account the created Governance belongs to + /// 1. `[writable]` Program Governance account. PDA seeds: ['program-governance', realm, governed_program] + /// 2. `[writable]` Program Data account of the Program governed by this Governance account + /// 3. `[signer]` Current Upgrade Authority account of the Program governed by this Governance account + /// 4. `[signer]` Payer + /// 5. `[]` bpf_upgradeable_loader program + /// 6. `[]` System program + /// 7. `[]` Sysvar Rent CreateProgramGovernance { /// Governance config #[allow(dead_code)] @@ -140,58 +132,64 @@ pub enum GovernanceInstruction { transfer_upgrade_authority: bool, }, - /// Create Proposal account for Instructions that will be executed at various slots in the future - /// The instruction also grants Admin and Signatory token to the provided account + /// Creates Proposal account for Instructions that will be executed at various slots in the future /// - /// 0. `[writable]` Uninitialized Proposal account - /// 1. `[writable]` Initialized Governance account - /// 2. `[writable]` Initialized Signatory Mint account - /// 3. `[writable]` Initialized Admin Mint account - /// 4. `[writable]` Initialized Admin account for the issued admin token - /// 5. `[writable]` Initialized Signatory account for the issued signatory token - /// 6. '[]` Token program account - /// 7. `[]` Rent sysvar + /// 0. `[writable]` Proposal account. PDA seeds ['governance',governance, governing_token_mint, proposal_index] + /// 1. `[writable]` Governance account + /// 2. `[]` Token Owner Record account + /// 3. `[signer]` Governance Authority (Token Owner or Governance Delegate) + /// 4. `[signer]` Payer + /// 5. `[]` System program + /// 6. `[]` Rent sysvar + /// 7. `[]` Clock sysvar CreateProposal { - #[allow(dead_code)] - /// Link to gist explaining proposal - description_link: String, - #[allow(dead_code)] /// UTF-8 encoded name of the proposal name: String, #[allow(dead_code)] - /// The Governing token (Community or Council) which will be used for voting on the Proposal - governing_token_type: GoverningTokenType, + /// Link to gist explaining proposal + description_link: String, + + #[allow(dead_code)] + /// Governing Token Mint the Proposal is created for + governing_token_mint: Pubkey, }, - /// [Requires Admin token] /// Adds a signatory to the Proposal which means this Proposal can't leave Draft state until yet another Signatory signs - /// As a result of this call the new Signatory will receive a Signatory Token which then can be used to Sign proposal /// /// 0. `[writable]` Proposal account - /// 1. `[writable]` Initialized Signatory account - /// 2. `[writable]` Initialized Signatory Mint account - /// 3. `[signer]` Admin account - /// 4. '[]` Token program account - AddSignatory, + /// 1. `[]` Token Owner Record account + /// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate) + /// 3. `[writable]` Signatory Record Account + /// 4. `[signer]` Payer + /// 5. `[]` System program + /// 6. `[]` Rent sysvar + AddSignatory { + #[allow(dead_code)] + /// Signatory to add to the Proposal + signatory: Pubkey, + }, - /// [Requires Admin token] /// Removes a Signatory from the Proposal /// /// 0. `[writable]` Proposal account - /// 1. `[writable]` Signatory account to remove token from - /// 2. `[writable]` Signatory Mint account - /// 3. `[signer]` Admin account - /// 4. '[]` Token program account - RemoveSignatory, + /// 1. `[]` Token Owner Record account + /// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate) + /// 3. `[writable]` Signatory Record Account + /// 4. `[writable]` Beneficiary Account which would receive lamports from the disposed Signatory Record Account + /// 5. `[]` Clock sysvar + RemoveSignatory { + #[allow(dead_code)] + /// Signatory to remove from the Proposal + signatory: Pubkey, + }, - /// [Requires Admin token] /// Adds an instruction to the Proposal. Max of 5 of any type. More than 5 will throw error /// /// 0. `[writable]` Proposal account /// 1. `[writable]` Uninitialized Proposal SingleSignerInstruction account - /// 2. `[signer]` Admin account + /// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate) AddSingleSignerInstruction { #[allow(dead_code)] /// Slot waiting time between vote period ending and this being eligible for execution @@ -206,42 +204,37 @@ pub enum GovernanceInstruction { position: u8, }, - /// [Requires Admin token] /// Remove instruction from the Proposal /// /// 0. `[writable]` Proposal account /// 1. `[writable]` Proposal SingleSignerInstruction account - /// 2. `[signer]` Admin account + /// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate) RemoveInstruction, - /// [Requires Admin token] /// Update instruction hold up time in the Proposal /// /// 0. `[]` Proposal account /// 1. `[writable]` Proposal SingleSignerInstruction account - /// 2. `[signer]` Admin account + /// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate) UpdateInstructionHoldUpTime { #[allow(dead_code)] /// Minimum waiting time in slots for an instruction to be executed after proposal is voted on hold_up_time: u64, }, - /// [Requires Admin token] /// Cancels Proposal and moves it into Canceled /// /// 0. `[writable]` Proposal account - /// 1. `[writable]` Admin account + /// 1. `[signer]` Governance Authority (Token Owner or Governance Delegate) CancelProposal, - /// [Requires Signatory token] - /// Burns signatory token, indicating you approve and sign off on moving this Proposal from Draft state to Voting state - /// The last Signatory token to be burned moves the state to Voting + /// Signs off Proposal indicating the Signatory approves the Proposal + /// When the last Signatory signs the Proposal state moves to Voting state /// /// 0. `[writable]` Proposal account - /// 1. `[writable]` Signatory account - /// 2. `[writable]` Signatory Mint account - /// 3. `[]` Token program account - /// 4. `[]` Clock sysvar + /// 1. `[writable]` Signatory Record account + /// 2. `[signer]` Signatory account + /// 3. `[]` Clock sysvar SignOffProposal, /// Uses your voter weight (deposited Community or Council tokens) to cast a vote on a Proposal @@ -249,9 +242,9 @@ pub enum GovernanceInstruction { /// If you tip the consensus then the instructions can begin to be run after their hold up time /// /// 0. `[writable]` Proposal account - /// 1. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] + /// 1. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] /// 2. `[writable]` Proposal Vote Record account. PDA seeds: ['governance',proposal,governing_token_owner] - /// 3. `[signer]` Vote Authority account + /// 3. `[signer]` Governance Authority account /// 4. `[]` Governance account Vote { #[allow(dead_code)] @@ -265,9 +258,9 @@ pub enum GovernanceInstruction { /// and only allows voters to prune their outstanding votes in case they wanted to withdraw Governing tokens from the Realm /// /// 0. `[writable]` Proposal account - /// 1. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] + /// 1. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] /// 2. `[writable]` Proposal Vote Record account. PDA seeds: ['governance',proposal,governing_token_owner] - /// 3. `[signer]` Vote Authority account + /// 3. `[signer]` Governance Authority account RelinquishVote, /// Executes an instruction in the Proposal @@ -336,7 +329,7 @@ pub fn deposit_governing_tokens( governing_token_mint: &Pubkey, ) -> Instruction { let vote_record_address = - get_voter_record_address(realm, governing_token_mint, governing_token_owner); + get_token_owner_record_address(realm, governing_token_mint, governing_token_owner); let governing_token_holding_address = get_governing_token_holding_address(realm, governing_token_mint); @@ -373,7 +366,7 @@ pub fn withdraw_governing_tokens( governing_token_mint: &Pubkey, ) -> Instruction { let vote_record_address = - get_voter_record_address(realm, governing_token_mint, governing_token_owner); + get_token_owner_record_address(realm, governing_token_mint, governing_token_owner); let governing_token_holding_address = get_governing_token_holding_address(realm, governing_token_mint); @@ -396,29 +389,26 @@ pub fn withdraw_governing_tokens( } } -/// Creates SetVoteAuthority instruction -pub fn set_vote_authority( +/// Creates SetGovernanceDelegate instruction +pub fn set_governance_delegate( // Accounts - vote_authority: &Pubkey, + governance_authority: &Pubkey, // Args realm: &Pubkey, governing_token_mint: &Pubkey, governing_token_owner: &Pubkey, - new_vote_authority: &Option, + new_governance_delegate: &Option, ) -> Instruction { let vote_record_address = - get_voter_record_address(realm, governing_token_mint, governing_token_owner); + get_token_owner_record_address(realm, governing_token_mint, governing_token_owner); let accounts = vec![ - AccountMeta::new_readonly(*vote_authority, true), + AccountMeta::new_readonly(*governance_authority, true), AccountMeta::new(vote_record_address, false), ]; - let instruction = GovernanceInstruction::SetVoteAuthority { - realm: *realm, - governing_token_mint: *governing_token_mint, - governing_token_owner: *governing_token_owner, - new_vote_authority: *new_vote_authority, + let instruction = GovernanceInstruction::SetGovernanceDelegate { + new_governance_delegate: *new_governance_delegate, }; Instruction { @@ -490,3 +480,138 @@ pub fn create_program_governance( data: instruction.try_to_vec().unwrap(), } } + +/// Creates CreateProposal instruction +#[allow(clippy::too_many_arguments)] +pub fn create_proposal( + // Accounts + governance: &Pubkey, + governing_token_owner: &Pubkey, + governance_authority: &Pubkey, + payer: &Pubkey, + // Args + realm: &Pubkey, + name: String, + description_link: String, + governing_token_mint: &Pubkey, + proposal_index: u16, +) -> Instruction { + let proposal_address = get_proposal_address( + governance, + governing_token_mint, + &proposal_index.to_le_bytes(), + ); + let token_owner_record_address = + get_token_owner_record_address(realm, governing_token_mint, governing_token_owner); + + let accounts = vec![ + AccountMeta::new(proposal_address, false), + AccountMeta::new(*governance, false), + AccountMeta::new_readonly(token_owner_record_address, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new_readonly(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + ]; + + let instruction = GovernanceInstruction::CreateProposal { + name, + description_link, + governing_token_mint: *governing_token_mint, + }; + + Instruction { + program_id: id(), + accounts, + data: instruction.try_to_vec().unwrap(), + } +} + +/// Creates AddSignatory instruction +pub fn add_signatory( + // Accounts + proposal: &Pubkey, + token_owner_record: &Pubkey, + governance_authority: &Pubkey, + payer: &Pubkey, + // Args + signatory: &Pubkey, +) -> Instruction { + let signatory_record_address = get_signatory_record_address(proposal, signatory); + + let accounts = vec![ + AccountMeta::new(*proposal, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(signatory_record_address, false), + AccountMeta::new_readonly(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + ]; + + let instruction = GovernanceInstruction::AddSignatory { + signatory: *signatory, + }; + + Instruction { + program_id: id(), + accounts, + data: instruction.try_to_vec().unwrap(), + } +} + +/// Creates RemoveSignatory instruction +pub fn remove_signatory( + // Accounts + proposal: &Pubkey, + token_owner_record: &Pubkey, + governance_authority: &Pubkey, + signatory: &Pubkey, + beneficiary: &Pubkey, +) -> Instruction { + let signatory_record_address = get_signatory_record_address(proposal, signatory); + + let accounts = vec![ + AccountMeta::new(*proposal, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(signatory_record_address, false), + AccountMeta::new(*beneficiary, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + ]; + + let instruction = GovernanceInstruction::RemoveSignatory { + signatory: *signatory, + }; + + Instruction { + program_id: id(), + accounts, + data: instruction.try_to_vec().unwrap(), + } +} + +/// Creates SignOffProposal instruction +pub fn sign_off_proposal( + // Accounts + proposal: &Pubkey, + signatory: &Pubkey, +) -> Instruction { + let signatory_record_address = get_signatory_record_address(proposal, signatory); + + let accounts = vec![ + AccountMeta::new(*proposal, false), + AccountMeta::new(signatory_record_address, false), + AccountMeta::new_readonly(*signatory, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), + ]; + + let instruction = GovernanceInstruction::SignOffProposal; + + Instruction { + program_id: id(), + accounts, + data: instruction.try_to_vec().unwrap(), + } +} diff --git a/governance/program/src/processor/mod.rs b/governance/program/src/processor/mod.rs index 788bbc7a441..21edaa4270d 100644 --- a/governance/program/src/processor/mod.rs +++ b/governance/program/src/processor/mod.rs @@ -1,20 +1,28 @@ //! Program processor +mod process_add_signatory; mod process_create_account_governance; mod process_create_program_governance; +mod process_create_proposal; mod process_create_realm; mod process_deposit_governing_tokens; -mod process_set_vote_authority; +mod process_remove_signatory; +mod process_set_governance_delegate; +mod process_sign_off_proposal; mod process_withdraw_governing_tokens; use crate::instruction::GovernanceInstruction; use borsh::BorshDeserialize; +use process_add_signatory::*; use process_create_account_governance::*; use process_create_program_governance::*; +use process_create_proposal::*; use process_create_realm::*; use process_deposit_governing_tokens::*; -use process_set_vote_authority::*; +use process_remove_signatory::*; +use process_set_governance_delegate::*; +use process_sign_off_proposal::*; use process_withdraw_governing_tokens::*; use solana_program::{ @@ -31,7 +39,7 @@ pub fn process_instruction( let instruction = GovernanceInstruction::try_from_slice(input) .map_err(|_| ProgramError::InvalidInstructionData)?; - msg!("Instruction: {:?}", instruction); + msg!("GOVERNANCE-INSTRUCTION: {:?}", instruction); match instruction { GovernanceInstruction::CreateRealm { name } => { @@ -46,18 +54,9 @@ pub fn process_instruction( process_withdraw_governing_tokens(program_id, accounts) } - GovernanceInstruction::SetVoteAuthority { - realm, - governing_token_mint, - governing_token_owner, - new_vote_authority, - } => process_set_vote_authority( - accounts, - &realm, - &governing_token_mint, - &governing_token_owner, - &new_vote_authority, - ), + GovernanceInstruction::SetGovernanceDelegate { + new_governance_delegate, + } => process_set_governance_delegate(accounts, &new_governance_delegate), GovernanceInstruction::CreateProgramGovernance { config, transfer_upgrade_authority, @@ -70,6 +69,26 @@ pub fn process_instruction( GovernanceInstruction::CreateAccountGovernance { config } => { process_create_account_governance(program_id, accounts, config) } + GovernanceInstruction::CreateProposal { + name, + description_link, + governing_token_mint, + } => process_create_proposal( + program_id, + accounts, + name, + description_link, + governing_token_mint, + ), + GovernanceInstruction::AddSignatory { signatory } => { + process_add_signatory(program_id, accounts, signatory) + } + GovernanceInstruction::RemoveSignatory { signatory } => { + process_remove_signatory(program_id, accounts, signatory) + } + GovernanceInstruction::SignOffProposal {} => { + process_sign_off_proposal(program_id, accounts) + } _ => todo!("Instruction not implemented yet"), } } diff --git a/governance/program/src/processor/process_add_signatory.rs b/governance/program/src/processor/process_add_signatory.rs new file mode 100644 index 00000000000..2099ec9de93 --- /dev/null +++ b/governance/program/src/processor/process_add_signatory.rs @@ -0,0 +1,76 @@ +//! Program state processor + +use borsh::BorshSerialize; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, +}; + +use crate::{ + state::{ + enums::GovernanceAccountType, + proposal::deserialize_proposal_raw, + signatory_record::{get_signatory_record_address_seeds, SignatoryRecord}, + token_owner_record::deserialize_token_owner_record_for_proposal_owner, + }, + tools::{ + account::create_and_serialize_account_signed, + asserts::assert_token_owner_or_delegate_is_signer, + }, +}; + +/// Processes AddSignatory instruction +pub fn process_add_signatory( + program_id: &Pubkey, + accounts: &[AccountInfo], + signatory: Pubkey, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let proposal_info = next_account_info(account_info_iter)?; // 0 + let token_owner_record_info = next_account_info(account_info_iter)?; // 1 + let governance_authority_info = next_account_info(account_info_iter)?; // 2 + + let signatory_record_info = next_account_info(account_info_iter)?; // 3 + + let payer_info = next_account_info(account_info_iter)?; // 4 + let system_info = next_account_info(account_info_iter)?; // 5 + + let rent_sysvar_info = next_account_info(account_info_iter)?; // 6 + let rent = &Rent::from_account_info(rent_sysvar_info)?; + + let mut proposal_data = deserialize_proposal_raw(proposal_info)?; + proposal_data.assert_can_edit_signatories()?; + + let token_owner_record_data = deserialize_token_owner_record_for_proposal_owner( + token_owner_record_info, + &proposal_data.token_owner_record, + )?; + + assert_token_owner_or_delegate_is_signer(&token_owner_record_data, governance_authority_info)?; + + let signatory_record_data = SignatoryRecord { + account_type: GovernanceAccountType::SignatoryRecord, + proposal: *proposal_info.key, + signatory, + signed_off: false, + }; + + create_and_serialize_account_signed::( + payer_info, + signatory_record_info, + &signatory_record_data, + &get_signatory_record_address_seeds(proposal_info.key, &signatory), + program_id, + system_info, + rent, + )?; + + proposal_data.signatories_count = proposal_data.signatories_count.checked_add(1).unwrap(); + proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?; + + Ok(()) +} diff --git a/governance/program/src/processor/process_create_account_governance.rs b/governance/program/src/processor/process_create_account_governance.rs index 9226637d226..1e462e75b9c 100644 --- a/governance/program/src/processor/process_create_account_governance.rs +++ b/governance/program/src/processor/process_create_account_governance.rs @@ -39,7 +39,7 @@ pub fn process_create_account_governance( let account_governance_data = Governance { account_type: GovernanceAccountType::AccountGovernance, config: config.clone(), - proposal_count: 0, + proposals_count: 0, }; create_and_serialize_account_signed::( diff --git a/governance/program/src/processor/process_create_program_governance.rs b/governance/program/src/processor/process_create_program_governance.rs index 32fa56f0598..df2dcadb80e 100644 --- a/governance/program/src/processor/process_create_program_governance.rs +++ b/governance/program/src/processor/process_create_program_governance.rs @@ -52,7 +52,7 @@ pub fn process_create_program_governance( let program_governance_data = Governance { account_type: GovernanceAccountType::ProgramGovernance, config: config.clone(), - proposal_count: 0, + proposals_count: 0, }; create_and_serialize_account_signed::( diff --git a/governance/program/src/processor/process_create_proposal.rs b/governance/program/src/processor/process_create_proposal.rs new file mode 100644 index 00000000000..f772a221742 --- /dev/null +++ b/governance/program/src/processor/process_create_proposal.rs @@ -0,0 +1,115 @@ +//! Program state processor + +use borsh::BorshSerialize; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + clock::Clock, + entrypoint::ProgramResult, + pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, +}; + +use crate::{ + error::GovernanceError, + state::{ + enums::{GovernanceAccountType, ProposalState}, + governance::deserialize_governance_raw, + proposal::{get_proposal_address_seeds, Proposal}, + token_owner_record::deserialize_token_owner_record_for_realm_and_governing_mint, + }, + tools::{ + account::create_and_serialize_account_signed, + asserts::assert_token_owner_or_delegate_is_signer, + }, +}; + +/// Processes CreateProposal instruction +pub fn process_create_proposal( + program_id: &Pubkey, + accounts: &[AccountInfo], + name: String, + description_link: String, + governing_token_mint: Pubkey, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let proposal_info = next_account_info(account_info_iter)?; // 0 + let governance_info = next_account_info(account_info_iter)?; // 1 + + let token_owner_record_info = next_account_info(account_info_iter)?; // 2 + let governance_authority_info = next_account_info(account_info_iter)?; // 3 + + let payer_info = next_account_info(account_info_iter)?; // 4 + let system_info = next_account_info(account_info_iter)?; // 5 + + let rent_sysvar_info = next_account_info(account_info_iter)?; // 6 + let rent = &Rent::from_account_info(rent_sysvar_info)?; + + let clock_info = next_account_info(account_info_iter)?; // 7 + let clock = Clock::from_account_info(clock_info)?; + + if !proposal_info.data_is_empty() { + return Err(GovernanceError::ProposalAlreadyExists.into()); + } + + let mut governance_data = deserialize_governance_raw(governance_info)?; + + let token_owner_record_data = deserialize_token_owner_record_for_realm_and_governing_mint( + &token_owner_record_info, + &governance_data.config.realm, + &governing_token_mint, + )?; + + // proposal_owner must be either governing token owner or governance_delegate and must sign this transaction + assert_token_owner_or_delegate_is_signer(&token_owner_record_data, governance_authority_info)?; + + if token_owner_record_data.governing_token_deposit_amount + < governance_data.config.min_tokens_to_create_proposal as u64 + { + return Err(GovernanceError::NotEnoughTokensToCreateProposal.into()); + } + + let proposal_data = Proposal { + account_type: GovernanceAccountType::Proposal, + governance: *governance_info.key, + governing_token_mint, + state: ProposalState::Draft, + token_owner_record: *token_owner_record_info.key, + + signatories_count: 0, + signatories_signed_off_count: 0, + + name, + description_link, + + draft_at: clock.slot, + signing_off_at: None, + voting_at: None, + voting_completed_at: None, + executing_at: None, + closed_at: None, + + number_of_executed_instructions: 0, + number_of_instructions: 0, + }; + + create_and_serialize_account_signed::( + payer_info, + proposal_info, + &proposal_data, + &get_proposal_address_seeds( + governance_info.key, + &governing_token_mint, + &governance_data.proposals_count.to_le_bytes(), + ), + program_id, + system_info, + rent, + )?; + + governance_data.proposals_count = governance_data.proposals_count.checked_add(1).unwrap(); + governance_data.serialize(&mut *governance_info.data.borrow_mut())?; + + Ok(()) +} diff --git a/governance/program/src/processor/process_deposit_governing_tokens.rs b/governance/program/src/processor/process_deposit_governing_tokens.rs index c195ccae93c..9807595976f 100644 --- a/governance/program/src/processor/process_deposit_governing_tokens.rs +++ b/governance/program/src/processor/process_deposit_governing_tokens.rs @@ -12,9 +12,11 @@ use solana_program::{ use crate::{ error::GovernanceError, state::{ - enums::{GovernanceAccountType, GoverningTokenType}, - realm::deserialize_realm, - voter_record::{deserialize_voter_record, get_voter_record_address_seeds, VoterRecord}, + enums::GovernanceAccountType, + realm::deserialize_realm_raw, + token_owner_record::{ + deserialize_token_owner_record, get_token_owner_record_address_seeds, TokenOwnerRecord, + }, }, tools::{ account::create_and_serialize_account_signed, @@ -37,7 +39,7 @@ pub fn process_deposit_governing_tokens( let governing_token_source_info = next_account_info(account_info_iter)?; // 2 let governing_token_owner_info = next_account_info(account_info_iter)?; // 3 let governing_token_transfer_authority_info = next_account_info(account_info_iter)?; // 4 - let voter_record_info = next_account_info(account_info_iter)?; // 5 + let token_owner_record_info = next_account_info(account_info_iter)?; // 5 let payer_info = next_account_info(account_info_iter)?; // 6 let system_info = next_account_info(account_info_iter)?; // 7 let spl_token_info = next_account_info(account_info_iter)?; // 8 @@ -45,16 +47,10 @@ pub fn process_deposit_governing_tokens( let rent_sysvar_info = next_account_info(account_info_iter)?; // 9 let rent = &Rent::from_account_info(rent_sysvar_info)?; - let realm_data = deserialize_realm(realm_info)?; + let realm_data = deserialize_realm_raw(realm_info)?; let governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?; - let governing_token_type = if governing_token_mint == realm_data.community_mint { - GoverningTokenType::Community - } else if Some(governing_token_mint) == realm_data.council_mint { - GoverningTokenType::Council - } else { - return Err(GovernanceError::InvalidGoverningTokenMint.into()); - }; + realm_data.assert_is_valid_governing_token_mint(&governing_token_mint)?; let amount = get_amount_from_token_account(governing_token_source_info)?; @@ -66,13 +62,13 @@ pub fn process_deposit_governing_tokens( spl_token_info, )?; - let voter_record_address_seeds = get_voter_record_address_seeds( + let token_owner_record_address_seeds = get_token_owner_record_address_seeds( realm_info.key, &governing_token_mint, governing_token_owner_info.key, ); - if voter_record_info.data_is_empty() { + if token_owner_record_info.data_is_empty() { // Deposited tokens can only be withdrawn by the owner so let's make sure the owner signed the transaction let governing_token_owner = get_owner_from_token_account(&governing_token_source_info)?; @@ -82,36 +78,38 @@ pub fn process_deposit_governing_tokens( return Err(GovernanceError::GoverningTokenOwnerMustSign.into()); } - let voter_record_data = VoterRecord { - account_type: GovernanceAccountType::VoterRecord, + let token_owner_record_data = TokenOwnerRecord { + account_type: GovernanceAccountType::TokenOwnerRecord, realm: *realm_info.key, - token_owner: *governing_token_owner_info.key, - token_deposit_amount: amount, - token_type: governing_token_type, - vote_authority: None, + governing_token_owner: *governing_token_owner_info.key, + governing_token_deposit_amount: amount, + governing_token_mint, + governance_delegate: None, active_votes_count: 0, total_votes_count: 0, }; create_and_serialize_account_signed( payer_info, - voter_record_info, - &voter_record_data, - &voter_record_address_seeds, + token_owner_record_info, + &token_owner_record_data, + &token_owner_record_address_seeds, program_id, system_info, rent, )?; } else { - let mut voter_record_data = - deserialize_voter_record(voter_record_info, &voter_record_address_seeds)?; + let mut token_owner_record_data = deserialize_token_owner_record( + token_owner_record_info, + &token_owner_record_address_seeds, + )?; - voter_record_data.token_deposit_amount = voter_record_data - .token_deposit_amount + token_owner_record_data.governing_token_deposit_amount = token_owner_record_data + .governing_token_deposit_amount .checked_add(amount) .unwrap(); - voter_record_data.serialize(&mut *voter_record_info.data.borrow_mut())?; + token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?; } Ok(()) diff --git a/governance/program/src/processor/process_remove_signatory.rs b/governance/program/src/processor/process_remove_signatory.rs new file mode 100644 index 00000000000..96fbf0a8d2c --- /dev/null +++ b/governance/program/src/processor/process_remove_signatory.rs @@ -0,0 +1,68 @@ +//! Program state processor + +use borsh::BorshSerialize; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + clock::Clock, + entrypoint::ProgramResult, + pubkey::Pubkey, + sysvar::Sysvar, +}; + +use crate::{ + state::{ + enums::ProposalState, proposal::deserialize_proposal_raw, + signatory_record::deserialize_signatory_record, + token_owner_record::deserialize_token_owner_record_for_proposal_owner, + }, + tools::{account::dispose_account, asserts::assert_token_owner_or_delegate_is_signer}, +}; + +/// Processes RemoveSignatory instruction +pub fn process_remove_signatory( + _program_id: &Pubkey, + accounts: &[AccountInfo], + signatory: Pubkey, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let proposal_info = next_account_info(account_info_iter)?; // 0 + let token_owner_record_info = next_account_info(account_info_iter)?; // 1 + let governance_authority_info = next_account_info(account_info_iter)?; // 2 + + let signatory_record_info = next_account_info(account_info_iter)?; // 3 + let beneficiary_info = next_account_info(account_info_iter)?; // 4 + + let clock_info = next_account_info(account_info_iter)?; // 5 + let clock = Clock::from_account_info(clock_info)?; + + let mut proposal_data = deserialize_proposal_raw(proposal_info)?; + proposal_data.assert_can_edit_signatories()?; + + let token_owner_record_data = deserialize_token_owner_record_for_proposal_owner( + token_owner_record_info, + &proposal_data.token_owner_record, + )?; + + assert_token_owner_or_delegate_is_signer(&token_owner_record_data, governance_authority_info)?; + + let signatory_record_data = + deserialize_signatory_record(signatory_record_info, proposal_info.key, &signatory)?; + signatory_record_data.assert_can_remove_signatory()?; + + proposal_data.signatories_count = proposal_data.signatories_count.checked_sub(1).unwrap(); + + // If all the remaining signatories signed already then we can start voting + if proposal_data.signatories_count > 0 + && proposal_data.signatories_signed_off_count == proposal_data.signatories_count + { + proposal_data.voting_at = Some(clock.slot); + proposal_data.state = ProposalState::Voting; + } + + proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?; + + dispose_account(signatory_record_info, beneficiary_info); + + Ok(()) +} diff --git a/governance/program/src/processor/process_set_governance_delegate.rs b/governance/program/src/processor/process_set_governance_delegate.rs new file mode 100644 index 00000000000..04a82567170 --- /dev/null +++ b/governance/program/src/processor/process_set_governance_delegate.rs @@ -0,0 +1,33 @@ +//! Program state processor + +use borsh::BorshSerialize; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + pubkey::Pubkey, +}; + +use crate::{ + state::token_owner_record::deserialize_token_owner_record_raw, + tools::asserts::assert_token_owner_or_delegate_is_signer, +}; + +/// Processes SetGovernanceDelegate instruction +pub fn process_set_governance_delegate( + accounts: &[AccountInfo], + new_governance_delegate: &Option, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let governance_authority_info = next_account_info(account_info_iter)?; // 0 + let token_owner_record_info = next_account_info(account_info_iter)?; // 1 + + let mut token_owner_record_data = deserialize_token_owner_record_raw(token_owner_record_info)?; + + assert_token_owner_or_delegate_is_signer(&token_owner_record_data, &governance_authority_info)?; + + token_owner_record_data.governance_delegate = *new_governance_delegate; + token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?; + + Ok(()) +} diff --git a/governance/program/src/processor/process_set_vote_authority.rs b/governance/program/src/processor/process_set_vote_authority.rs deleted file mode 100644 index 6962b434ee9..00000000000 --- a/governance/program/src/processor/process_set_vote_authority.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Program state processor - -use borsh::BorshSerialize; -use solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - pubkey::Pubkey, -}; - -use crate::{ - state::voter_record::{deserialize_voter_record, get_voter_record_address_seeds}, - tools::asserts::assert_is_signed_by_owner_or_vote_authority, -}; - -/// Processes SetVoteAuthority instruction -pub fn process_set_vote_authority( - accounts: &[AccountInfo], - realm: &Pubkey, - governing_token_mint: &Pubkey, - governing_token_owner: &Pubkey, - new_vote_authority: &Option, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let vote_authority_info = next_account_info(account_info_iter)?; // 0 - let voter_record_info = next_account_info(account_info_iter)?; // 1 - - let mut voter_record_data = deserialize_voter_record( - voter_record_info, - &get_voter_record_address_seeds(realm, &governing_token_mint, governing_token_owner), - )?; - - assert_is_signed_by_owner_or_vote_authority(&voter_record_data, &vote_authority_info)?; - - voter_record_data.vote_authority = *new_vote_authority; - voter_record_data.serialize(&mut *voter_record_info.data.borrow_mut())?; - - Ok(()) -} diff --git a/governance/program/src/processor/process_sign_off_proposal.rs b/governance/program/src/processor/process_sign_off_proposal.rs new file mode 100644 index 00000000000..90138623579 --- /dev/null +++ b/governance/program/src/processor/process_sign_off_proposal.rs @@ -0,0 +1,58 @@ +//! Program state processor + +use borsh::BorshSerialize; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + clock::Clock, + entrypoint::ProgramResult, + pubkey::Pubkey, + sysvar::Sysvar, +}; + +use crate::state::{ + enums::ProposalState, proposal::deserialize_proposal_raw, + signatory_record::deserialize_signatory_record, +}; + +/// Processes SignOffProposal instruction +pub fn process_sign_off_proposal(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let proposal_info = next_account_info(account_info_iter)?; // 0 + + let signatory_record_info = next_account_info(account_info_iter)?; // 1 + let signatory_info = next_account_info(account_info_iter)?; // 2 + + let clock_info = next_account_info(account_info_iter)?; // 3 + let clock = Clock::from_account_info(clock_info)?; + + let mut proposal_data = deserialize_proposal_raw(proposal_info)?; + proposal_data.assert_can_sign_off()?; + + let mut signatory_record_data = + deserialize_signatory_record(signatory_record_info, proposal_info.key, signatory_info.key)?; + signatory_record_data.assert_can_sign_off(signatory_info)?; + + signatory_record_data.signed_off = true; + signatory_record_data.serialize(&mut *signatory_record_info.data.borrow_mut())?; + + if proposal_data.signatories_signed_off_count == 0 { + proposal_data.signing_off_at = Some(clock.slot); + proposal_data.state = ProposalState::SigningOff; + } + + proposal_data.signatories_signed_off_count = proposal_data + .signatories_signed_off_count + .checked_add(1) + .unwrap(); + + // If all Signatories signed off we can start voting + if proposal_data.signatories_signed_off_count == proposal_data.signatories_count { + proposal_data.voting_at = Some(clock.slot); + proposal_data.state = ProposalState::Voting; + } + + proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?; + + Ok(()) +} diff --git a/governance/program/src/processor/process_withdraw_governing_tokens.rs b/governance/program/src/processor/process_withdraw_governing_tokens.rs index d6f08ac78e9..3811260fcc4 100644 --- a/governance/program/src/processor/process_withdraw_governing_tokens.rs +++ b/governance/program/src/processor/process_withdraw_governing_tokens.rs @@ -10,8 +10,10 @@ use solana_program::{ use crate::{ error::GovernanceError, state::{ - realm::{deserialize_realm, get_realm_address_seeds}, - voter_record::{deserialize_voter_record, get_voter_record_address_seeds}, + realm::{deserialize_realm_raw, get_realm_address_seeds}, + token_owner_record::{ + deserialize_token_owner_record, get_token_owner_record_address_seeds, + }, }, tools::token::{get_mint_from_token_account, transfer_spl_tokens_signed}, }; @@ -27,26 +29,26 @@ pub fn process_withdraw_governing_tokens( let governing_token_holding_info = next_account_info(account_info_iter)?; // 1 let governing_token_destination_info = next_account_info(account_info_iter)?; // 2 let governing_token_owner_info = next_account_info(account_info_iter)?; // 3 - let voter_record_info = next_account_info(account_info_iter)?; // 4 + let token_owner_record_info = next_account_info(account_info_iter)?; // 4 let spl_token_info = next_account_info(account_info_iter)?; // 5 if !governing_token_owner_info.is_signer { return Err(GovernanceError::GoverningTokenOwnerMustSign.into()); } - let realm_data = deserialize_realm(realm_info)?; + let realm_data = deserialize_realm_raw(realm_info)?; let governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?; - let voter_record_address_seeds = get_voter_record_address_seeds( + let token_owner_record_address_seeds = get_token_owner_record_address_seeds( realm_info.key, &governing_token_mint, governing_token_owner_info.key, ); - let mut voter_record_data = - deserialize_voter_record(voter_record_info, &voter_record_address_seeds)?; + let mut token_owner_record_data = + deserialize_token_owner_record(token_owner_record_info, &token_owner_record_address_seeds)?; - if voter_record_data.active_votes_count > 0 { + if token_owner_record_data.active_votes_count > 0 { return Err(GovernanceError::CannotWithdrawGoverningTokensWhenActiveVotesExist.into()); } @@ -56,12 +58,12 @@ pub fn process_withdraw_governing_tokens( &realm_info, &get_realm_address_seeds(&realm_data.name), program_id, - voter_record_data.token_deposit_amount, + token_owner_record_data.governing_token_deposit_amount, spl_token_info, )?; - voter_record_data.token_deposit_amount = 0; - voter_record_data.serialize(&mut *voter_record_info.data.borrow_mut())?; + token_owner_record_data.governing_token_deposit_amount = 0; + token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?; Ok(()) } diff --git a/governance/program/src/state/enums.rs b/governance/program/src/state/enums.rs index 58033750481..d6b1f4e27ce 100644 --- a/governance/program/src/state/enums.rs +++ b/governance/program/src/state/enums.rs @@ -12,8 +12,8 @@ pub enum GovernanceAccountType { /// Top level aggregation for governances with Community Token (and optional Council Token) Realm, - /// Voter record for each voter and given governing token type within a Realm - VoterRecord, + /// Token Owner Record for given governing token owner within a Realm + TokenOwnerRecord, /// Generic Account Governance account AccountGovernance, @@ -24,6 +24,9 @@ pub enum GovernanceAccountType { /// Proposal account for Governance account. A single Governance account can have multiple Proposal accounts Proposal, + /// Proposal Signatory account + SignatoryRecord, + /// Vote record account for a given Proposal. Proposal can have 0..n voting records ProposalVoteRecord, @@ -48,16 +51,6 @@ pub enum VoteWeight { No(u64), } -/// Governing Token type -#[repr(C)] -#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -pub enum GoverningTokenType { - /// Community token - Community, - /// Council token - Council, -} - /// What state a Proposal is in #[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] @@ -65,8 +58,9 @@ pub enum ProposalState { /// Draft - Proposal enters Draft state when it's created Draft, - /// Signing - The Proposal is being signed by Signatories. Proposal enters the state when first Signatory Sings and leaves it when last Signatory signs - Signing, + /// SigningOff - The Proposal is being signed off by Signatories + /// Proposal enters the state when first Signatory Sings and leaves it when last Signatory signs + SigningOff, /// Taking votes Voting, diff --git a/governance/program/src/state/governance.rs b/governance/program/src/state/governance.rs index 20b77eeeefa..6d797f7a52a 100644 --- a/governance/program/src/state/governance.rs +++ b/governance/program/src/state/governance.rs @@ -1,7 +1,8 @@ //! Governance Account use crate::{ - error::GovernanceError, id, state::enums::GovernanceAccountType, tools::account::AccountMaxSize, + error::GovernanceError, id, state::enums::GovernanceAccountType, + tools::account::deserialize_account, tools::account::AccountMaxSize, }; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use solana_program::{ @@ -46,7 +47,7 @@ pub struct Governance { pub config: GovernanceConfig, /// Running count of proposals - pub proposal_count: u32, + pub proposals_count: u16, } impl AccountMaxSize for Governance {} @@ -58,6 +59,13 @@ impl IsInitialized for Governance { } } +/// Deserializes account and checks owner program +pub fn deserialize_governance_raw( + governance_info: &AccountInfo, +) -> Result { + deserialize_account::(governance_info, &id()) +} + /// Returns ProgramGovernance PDA seeds pub fn get_program_governance_address_seeds<'a>( realm: &'a Pubkey, diff --git a/governance/program/src/state/mod.rs b/governance/program/src/state/mod.rs index f5eb7e28634..f3062cec038 100644 --- a/governance/program/src/state/mod.rs +++ b/governance/program/src/state/mod.rs @@ -5,5 +5,6 @@ pub mod governance; pub mod proposal; pub mod proposal_vote_record; pub mod realm; +pub mod signatory_record; pub mod single_signer_instruction; -pub mod voter_record; +pub mod token_owner_record; diff --git a/governance/program/src/state/proposal.rs b/governance/program/src/state/proposal.rs index 296cbf28bb4..ce0ee0a6971 100644 --- a/governance/program/src/state/proposal.rs +++ b/governance/program/src/state/proposal.rs @@ -1,8 +1,18 @@ //! Proposal Account -use solana_program::{epoch_schedule::Slot, pubkey::Pubkey}; - -use super::enums::{GovernanceAccountType, GoverningTokenType, ProposalState}; +use solana_program::{ + account_info::AccountInfo, epoch_schedule::Slot, program_error::ProgramError, + program_pack::IsInitialized, pubkey::Pubkey, +}; + +use crate::{ + error::GovernanceError, + id, + tools::account::{deserialize_account, AccountMaxSize}, + PROGRAM_AUTHORITY_SEED, +}; + +use crate::state::enums::{GovernanceAccountType, ProposalState}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; /// Governance Proposal @@ -15,23 +25,21 @@ pub struct Proposal { /// Governance account the Proposal belongs to pub governance: Pubkey, - /// Mint that creates signatory tokens of this Proposal - /// If there are outstanding signatory tokens, then cannot leave draft state. Signatories must burn tokens (ie agree - /// to move instruction to voting state) and bring mint to net 0 tokens outstanding. Each signatory gets 1 (serves as flag) - pub signatory_mint: Pubkey, - - /// Admin ownership mint. One token is minted, can be used to grant admin status to a new person - pub admin_mint: Pubkey, - /// Indicates which Governing Token is used to vote on the Proposal /// Whether the general Community token owners or the Council tokens owners vote on this Proposal - pub voting_token_type: GoverningTokenType, + pub governing_token_mint: Pubkey, - /// Current state of the proposal + /// Current proposal state pub state: ProposalState, - /// Total signatory tokens minted, for use comparing to supply remaining during draft period - pub total_signatory_tokens_minted: u64, + /// The TokenOwnerRecord representing the user who created and owns this Proposal + pub token_owner_record: Pubkey, + + /// The number of signatories assigned to the Proposal + pub signatories_count: u8, + + /// The number of signatories who already signed + pub signatories_signed_off_count: u8, /// Link to proposal's description pub description_link: String, @@ -39,27 +47,208 @@ pub struct Proposal { /// Proposal name pub name: String, - /// When the Proposal ended voting - this will also be when the set was defeated or began executing naturally - pub voting_ended_at: Option, + /// When the Proposal was created and entered Draft state + pub draft_at: Slot, + + /// When Signatories started signing off the Proposal + pub signing_off_at: Option, /// When the Proposal began voting - pub voting_began_at: Option, + pub voting_at: Option, - /// when the Proposal entered draft state - pub created_at: Option, + /// When the Proposal ended voting and entered either Succeeded or Defeated + pub voting_completed_at: Option, - /// when the Proposal entered completed state, also when execution ended naturally. - pub completed_at: Option, + /// When the Proposal entered Executing state + pub executing_at: Option, - /// when the Proposal entered deleted state - pub deleted_at: Option, + /// When the Proposal entered final state Completed or Cancelled and was closed + pub closed_at: Option, /// The number of the instructions already executed pub number_of_executed_instructions: u8, /// The number of instructions included in the proposal pub number_of_instructions: u8, +} + +impl AccountMaxSize for Proposal { + fn get_max_size(&self) -> Option { + Some(self.name.len() + self.description_link.len() + 163) + } +} + +impl IsInitialized for Proposal { + fn is_initialized(&self) -> bool { + self.account_type == GovernanceAccountType::Proposal + } +} + +impl Proposal { + /// Checks if Signatories can be edited (added or removed) for the Proposal in the given state + pub fn assert_can_edit_signatories(&self) -> Result<(), ProgramError> { + if !(self.state == ProposalState::Draft || self.state == ProposalState::SigningOff) { + return Err(GovernanceError::InvalidStateCannotEditSignatories.into()); + } + + Ok(()) + } + + /// Checks if Proposal can be singed off + pub fn assert_can_sign_off(&self) -> Result<(), ProgramError> { + if !(self.state == ProposalState::Draft || self.state == ProposalState::SigningOff) { + return Err(GovernanceError::InvalidStateCannotSignOff.into()); + } + + Ok(()) + } +} + +/// Deserializes Proposal account and checks owner program +pub fn deserialize_proposal_raw(proposal_info: &AccountInfo) -> Result { + deserialize_account::(proposal_info, &id()) +} + +/// Returns Proposal PDA seeds +pub fn get_proposal_address_seeds<'a>( + governance: &'a Pubkey, + governing_token_mint: &'a Pubkey, + proposal_index_le_bytes: &'a [u8], +) -> [&'a [u8]; 4] { + [ + PROGRAM_AUTHORITY_SEED, + governance.as_ref(), + governing_token_mint.as_ref(), + &proposal_index_le_bytes, + ] +} + +/// Returns Proposal PDA address +pub fn get_proposal_address<'a>( + governance: &'a Pubkey, + governing_token_mint: &'a Pubkey, + proposal_index_bytes: &'a [u8], +) -> Pubkey { + Pubkey::find_program_address( + &get_proposal_address_seeds(governance, governing_token_mint, &proposal_index_bytes), + &id(), + ) + .0 +} - /// Array of pubkeys pointing at Proposal instructions, up to 5 - pub instruction: Vec, +#[cfg(test)] +mod test { + + use {super::*, proptest::prelude::*}; + + fn create_test_proposal() -> Proposal { + Proposal { + account_type: GovernanceAccountType::TokenOwnerRecord, + governance: Pubkey::new_unique(), + governing_token_mint: Pubkey::new_unique(), + state: ProposalState::Draft, + token_owner_record: Pubkey::new_unique(), + signatories_count: 10, + signatories_signed_off_count: 5, + description_link: "This is my description".to_string(), + name: "This is my name".to_string(), + draft_at: 10, + signing_off_at: Some(10), + voting_at: Some(10), + voting_completed_at: Some(10), + executing_at: Some(10), + closed_at: Some(10), + number_of_executed_instructions: 10, + number_of_instructions: 10, + } + } + + #[test] + fn test_max_size() { + let proposal = create_test_proposal(); + let size = proposal.try_to_vec().unwrap().len(); + + assert_eq!(proposal.get_max_size(), Some(size)); + } + + fn editable_signatory_states() -> impl Strategy { + prop_oneof![Just(ProposalState::Draft), Just(ProposalState::SigningOff),] + } + + proptest! { + #[test] + fn test_assert_can_edit_signatories(state in editable_signatory_states()) { + + let mut proposal = create_test_proposal(); + proposal.state = state; + proposal.assert_can_edit_signatories().unwrap(); + + } + + } + + fn none_editable_signatory_states() -> impl Strategy { + prop_oneof![ + Just(ProposalState::Voting), + Just(ProposalState::Succeeded), + Just(ProposalState::Executing), + Just(ProposalState::Completed), + Just(ProposalState::Cancelled), + Just(ProposalState::Defeated), + ] + } + + proptest! { + #[test] + fn test_assert_can_edit_signatories_with_invalid_state_error(state in none_editable_signatory_states()) { + // Arrange + let mut proposal = create_test_proposal(); + proposal.state = state; + + // Act + let err = proposal.assert_can_edit_signatories().err().unwrap(); + + // Assert + assert_eq!(err, GovernanceError::InvalidStateCannotEditSignatories.into()); + } + + } + + fn sign_off_states() -> impl Strategy { + prop_oneof![Just(ProposalState::SigningOff), Just(ProposalState::Draft),] + } + proptest! { + #[test] + fn test_assert_can_sign_off(state in sign_off_states()) { + let mut proposal = create_test_proposal(); + proposal.state = state; + proposal.assert_can_sign_off().unwrap(); + } + } + + fn none_sign_off_states() -> impl Strategy { + prop_oneof![ + Just(ProposalState::Voting), + Just(ProposalState::Succeeded), + Just(ProposalState::Executing), + Just(ProposalState::Completed), + Just(ProposalState::Cancelled), + Just(ProposalState::Defeated), + ] + } + + proptest! { + #[test] + fn test_assert_can_sign_off_with_state_error(state in none_sign_off_states()) { + // Arrange + let mut proposal = create_test_proposal(); + proposal.state = state; + + // Act + let err = proposal.assert_can_sign_off().err().unwrap(); + + // Assert + assert_eq!(err, GovernanceError::InvalidStateCannotSignOff.into()); + } + } } diff --git a/governance/program/src/state/proposal_vote_record.rs b/governance/program/src/state/proposal_vote_record.rs index b7f069c6f76..a1c563aef54 100644 --- a/governance/program/src/state/proposal_vote_record.rs +++ b/governance/program/src/state/proposal_vote_record.rs @@ -3,7 +3,7 @@ use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use solana_program::pubkey::Pubkey; -use super::enums::{GovernanceAccountType, VoteWeight}; +use crate::state::enums::{GovernanceAccountType, VoteWeight}; /// Proposal Vote Record #[repr(C)] diff --git a/governance/program/src/state/realm.rs b/governance/program/src/state/realm.rs index 3aec3184ab9..6a333bae765 100644 --- a/governance/program/src/state/realm.rs +++ b/governance/program/src/state/realm.rs @@ -7,12 +7,13 @@ use solana_program::{ }; use crate::{ + error::GovernanceError, id, tools::account::{assert_is_valid_account, deserialize_account, AccountMaxSize}, PROGRAM_AUTHORITY_SEED, }; -use super::enums::GovernanceAccountType; +use crate::state::enums::GovernanceAccountType; /// Governance Realm Account /// Account PDA seeds" ['governance', name] @@ -40,13 +41,31 @@ impl IsInitialized for Realm { } } +impl Realm { + /// Asserts the given mint is either Community or Council mint of the Realm + pub fn assert_is_valid_governing_token_mint( + &self, + governing_token_mint: &Pubkey, + ) -> Result<(), ProgramError> { + if self.community_mint == *governing_token_mint { + return Ok(()); + } + + if self.council_mint == Some(*governing_token_mint) { + return Ok(()); + } + + Err(GovernanceError::InvalidGoverningTokenMint.into()) + } +} + /// Checks whether realm account exists, is initialized and owned by Governance program pub fn assert_is_valid_realm(realm_info: &AccountInfo) -> Result<(), ProgramError> { assert_is_valid_account(realm_info, GovernanceAccountType::Realm, &id()) } /// Deserializes account and checks owner program -pub fn deserialize_realm(realm_info: &AccountInfo) -> Result { +pub fn deserialize_realm_raw(realm_info: &AccountInfo) -> Result { deserialize_account::(realm_info, &id()) } diff --git a/governance/program/src/state/signatory_record.rs b/governance/program/src/state/signatory_record.rs new file mode 100644 index 00000000000..cdb88c67fb8 --- /dev/null +++ b/governance/program/src/state/signatory_record.rs @@ -0,0 +1,108 @@ +//! Signatory Record + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use solana_program::{ + account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized, + pubkey::Pubkey, +}; + +use crate::{ + error::GovernanceError, + id, + tools::account::{deserialize_account, AccountMaxSize}, + PROGRAM_AUTHORITY_SEED, +}; + +use crate::state::enums::GovernanceAccountType; + +/// Account PDA seeds: ['governance', proposal, signatory] +#[repr(C)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] +pub struct SignatoryRecord { + /// Governance account type + pub account_type: GovernanceAccountType, + /// Proposal the signatory is assigned for + pub proposal: Pubkey, + /// The account of the signatory who can sign off the proposal + pub signatory: Pubkey, + /// Indicates whether the signatory signed off the proposal + pub signed_off: bool, +} + +impl AccountMaxSize for SignatoryRecord {} + +impl IsInitialized for SignatoryRecord { + fn is_initialized(&self) -> bool { + self.account_type == GovernanceAccountType::SignatoryRecord + } +} + +impl SignatoryRecord { + /// Checks signatory hasn't signed off yet and is transaction signer + pub fn assert_can_sign_off(&self, signatory_info: &AccountInfo) -> Result<(), ProgramError> { + if self.signed_off { + return Err(GovernanceError::SignatoryAlreadySignedOff.into()); + } + + if !signatory_info.is_signer { + return Err(GovernanceError::SignatoryMustSign.into()); + } + + Ok(()) + } + + /// Checks signatory can be removed from Proposal + pub fn assert_can_remove_signatory(&self) -> Result<(), ProgramError> { + if self.signed_off { + return Err(GovernanceError::SignatoryAlreadySignedOff.into()); + } + + Ok(()) + } +} + +/// Returns SignatoryRecord PDA seeds +pub fn get_signatory_record_address_seeds<'a>( + proposal: &'a Pubkey, + signatory: &'a Pubkey, +) -> [&'a [u8]; 3] { + [ + PROGRAM_AUTHORITY_SEED, + proposal.as_ref(), + signatory.as_ref(), + ] +} + +/// Returns SignatoryRecord PDA address +pub fn get_signatory_record_address<'a>(proposal: &'a Pubkey, signatory: &'a Pubkey) -> Pubkey { + Pubkey::find_program_address( + &get_signatory_record_address_seeds(proposal, signatory), + &id(), + ) + .0 +} + +/// Deserializes SignatoryRecord account and checks owner program +pub fn deserialize_signatory_record_raw( + signatory_record_info: &AccountInfo, +) -> Result { + deserialize_account::(signatory_record_info, &id()) +} + +/// Deserializes SignatoryRecord and validates its PDA +pub fn deserialize_signatory_record( + signatory_record_info: &AccountInfo, + proposal: &Pubkey, + signatory: &Pubkey, +) -> Result { + let (signatory_record_address, _) = Pubkey::find_program_address( + &get_signatory_record_address_seeds(proposal, signatory), + &id(), + ); + + if signatory_record_address != *signatory_record_info.key { + return Err(GovernanceError::InvalidSignatoryAddress.into()); + } + + deserialize_signatory_record_raw(signatory_record_info) +} diff --git a/governance/program/src/state/single_signer_instruction.rs b/governance/program/src/state/single_signer_instruction.rs index c12fdc903e1..167866b4cff 100644 --- a/governance/program/src/state/single_signer_instruction.rs +++ b/governance/program/src/state/single_signer_instruction.rs @@ -1,6 +1,6 @@ //! SingleSignerInstruction Account -use super::enums::GovernanceAccountType; +use crate::state::enums::GovernanceAccountType; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; /// Account for an instruction to be executed for Proposal diff --git a/governance/program/src/state/token_owner_record.rs b/governance/program/src/state/token_owner_record.rs new file mode 100644 index 00000000000..ba4f716eb66 --- /dev/null +++ b/governance/program/src/state/token_owner_record.rs @@ -0,0 +1,166 @@ +//! Token Owner Record Account + +use crate::{ + error::GovernanceError, + id, + tools::account::{deserialize_account, AccountMaxSize}, + PROGRAM_AUTHORITY_SEED, +}; + +use crate::state::enums::GovernanceAccountType; + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use solana_program::{ + account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized, + pubkey::Pubkey, +}; + +/// Governance Token Owner Record +/// Account PDA seeds: ['governance', realm, token_mint, token_owner ] +#[repr(C)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] +pub struct TokenOwnerRecord { + /// Governance account type + pub account_type: GovernanceAccountType, + + /// The Realm the TokenOwnerRecord belongs to + pub realm: Pubkey, + + /// Governing Token Mint the TokenOwnerRecord holds deposit for + pub governing_token_mint: Pubkey, + + /// The owner (either single or multisig) of the deposited governing SPL Tokens + /// This is who can authorize a withdrawal + pub governing_token_owner: Pubkey, + + /// The amount of governing tokens deposited into the Realm + /// This amount is the voter weight used when voting on proposals + pub governing_token_deposit_amount: u64, + + /// A single account that is allowed to operate governance with the deposited governing tokens + /// It's delegated to by the governing token owner or current governance_delegate + pub governance_delegate: Option, + + /// The number of active votes cast by TokenOwner + pub active_votes_count: u16, + + /// The total number of votes cast by the TokenOwner + pub total_votes_count: u16, +} + +impl AccountMaxSize for TokenOwnerRecord { + fn get_max_size(&self) -> Option { + Some(142) + } +} + +impl IsInitialized for TokenOwnerRecord { + fn is_initialized(&self) -> bool { + self.account_type == GovernanceAccountType::TokenOwnerRecord + } +} + +/// Returns TokenOwnerRecord PDA address +pub fn get_token_owner_record_address( + realm: &Pubkey, + governing_token_mint: &Pubkey, + governing_token_owner: &Pubkey, +) -> Pubkey { + Pubkey::find_program_address( + &get_token_owner_record_address_seeds(realm, governing_token_mint, governing_token_owner), + &id(), + ) + .0 +} + +/// Returns TokenOwnerRecord PDA seeds +pub fn get_token_owner_record_address_seeds<'a>( + realm: &'a Pubkey, + governing_token_mint: &'a Pubkey, + governing_token_owner: &'a Pubkey, +) -> [&'a [u8]; 4] { + [ + PROGRAM_AUTHORITY_SEED, + realm.as_ref(), + governing_token_mint.as_ref(), + governing_token_owner.as_ref(), + ] +} + +/// Deserializes TokenOwnerRecord account and checks owner program +pub fn deserialize_token_owner_record_raw( + token_owner_record_info: &AccountInfo, +) -> Result { + deserialize_account::(token_owner_record_info, &id()) +} + +/// Deserializes TokenOwnerRecord account and checks its PDA against the provided seeds +pub fn deserialize_token_owner_record( + token_owner_record_info: &AccountInfo, + token_owner_record_seeds: &[&[u8]], +) -> Result { + let (token_owner_record_address, _) = + Pubkey::find_program_address(token_owner_record_seeds, &id()); + + if token_owner_record_address != *token_owner_record_info.key { + return Err(GovernanceError::InvalidTokenOwnerRecordAccountAddress.into()); + } + + deserialize_token_owner_record_raw(token_owner_record_info) +} + +/// Deserializes TokenOwnerRecord account and checks that its PDA matches the given realm and governing mint +pub fn deserialize_token_owner_record_for_realm_and_governing_mint( + token_owner_record_info: &AccountInfo, + realm: &Pubkey, + governing_token_mint: &Pubkey, +) -> Result { + let token_owner_record_data = deserialize_token_owner_record_raw(token_owner_record_info)?; + + if token_owner_record_data.governing_token_mint != *governing_token_mint { + return Err(GovernanceError::InvalidTokenOwnerRecordGoverningMint.into()); + } + + if token_owner_record_data.realm != *realm { + return Err(GovernanceError::InvalidTokenOwnerRecordRealm.into()); + } + + Ok(token_owner_record_data) +} + +/// Deserializes TokenOwnerRecord account and checks its address is the give proposal_owner +pub fn deserialize_token_owner_record_for_proposal_owner( + token_owner_record_info: &AccountInfo, + proposal_owner: &Pubkey, +) -> Result { + if token_owner_record_info.key != proposal_owner { + return Err(GovernanceError::InvalidProposalOwnerAccount.into()); + } + + deserialize_token_owner_record_raw(token_owner_record_info) +} + +#[cfg(test)] +mod test { + use solana_program::borsh::get_packed_len; + + use super::*; + + #[test] + fn test_max_size() { + let token_owner_record = TokenOwnerRecord { + account_type: GovernanceAccountType::TokenOwnerRecord, + realm: Pubkey::new_unique(), + governing_token_mint: Pubkey::new_unique(), + governing_token_owner: Pubkey::new_unique(), + governing_token_deposit_amount: 10, + governance_delegate: Some(Pubkey::new_unique()), + active_votes_count: 1, + total_votes_count: 1, + }; + + let size = get_packed_len::(); + + assert_eq!(token_owner_record.get_max_size(), Some(size)); + } +} diff --git a/governance/program/src/state/vote_record.rs b/governance/program/src/state/vote_record.rs new file mode 100644 index 00000000000..e260fcdda25 --- /dev/null +++ b/governance/program/src/state/vote_record.rs @@ -0,0 +1,55 @@ +//! Proposal Vote Record Account + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use solana_program::{program_pack::IsInitialized, pubkey::Pubkey}; + +use crate::{id, tools::account::AccountMaxSize, PROGRAM_AUTHORITY_SEED}; + +use crate::state::enums::{GovernanceAccountType, VoteWeight}; + +/// Proposal VoteRecord +#[repr(C)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] +pub struct VoteRecord { + /// Governance account type + pub account_type: GovernanceAccountType, + + /// Proposal account + pub proposal: Pubkey, + + /// The user who casted this vote + /// This is the Governing Token Owner who deposited governing tokens into the Realm + pub governing_token_owner: Pubkey, + + /// Voter's vote: Yes/No and amount + pub vote_weight: VoteWeight, +} + +impl AccountMaxSize for VoteRecord {} + +impl IsInitialized for VoteRecord { + fn is_initialized(&self) -> bool { + self.account_type == GovernanceAccountType::VoteRecord + } +} + +/// Returns VoteRecord PDA seeds +pub fn get_vote_record_address_seeds<'a>( + proposal: &'a Pubkey, + token_owner_record: &'a Pubkey, +) -> [&'a [u8]; 3] { + [ + PROGRAM_AUTHORITY_SEED, + proposal.as_ref(), + token_owner_record.as_ref(), + ] +} + +/// Returns VoteRecord PDA address +pub fn get_vote_record_address<'a>(proposal: &'a Pubkey, token_owner_record: &'a Pubkey) -> Pubkey { + Pubkey::find_program_address( + &get_vote_record_address_seeds(proposal, token_owner_record), + &id(), + ) + .0 +} diff --git a/governance/program/src/state/voter_record.rs b/governance/program/src/state/voter_record.rs deleted file mode 100644 index 27c46608b4a..00000000000 --- a/governance/program/src/state/voter_record.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! Voter Record Account - -use crate::{ - error::GovernanceError, - id, - tools::account::{deserialize_account, AccountMaxSize}, - PROGRAM_AUTHORITY_SEED, -}; - -use super::enums::{GovernanceAccountType, GoverningTokenType}; -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use solana_program::{ - account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized, - pubkey::Pubkey, -}; - -/// Governance Voter Record -/// Account PDA seeds: ['governance', realm, token_mint, token_owner ] -#[repr(C)] -#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -pub struct VoterRecord { - /// Governance account type - pub account_type: GovernanceAccountType, - - /// The Realm the VoterRecord belongs to - pub realm: Pubkey, - - /// The type of the Governing Token the VoteRecord is for - pub token_type: GoverningTokenType, - - /// The owner (either single or multisig) of the deposited governing SPL Tokens - /// This is who can authorize a withdrawal - pub token_owner: Pubkey, - - /// The amount of governing tokens deposited into the Realm - /// This amount is the voter weight used when voting on proposals - pub token_deposit_amount: u64, - - /// A single account that is allowed to operate governance with the deposited governing tokens - /// It's delegated to by the governing token owner or current vote_authority - pub vote_authority: Option, - - /// The number of active votes cast by voter - pub active_votes_count: u8, - - /// The total number of votes cast by the voter - pub total_votes_count: u8, -} - -impl AccountMaxSize for VoterRecord { - fn get_max_size(&self) -> Option { - Some(109) - } -} - -impl IsInitialized for VoterRecord { - fn is_initialized(&self) -> bool { - self.account_type == GovernanceAccountType::VoterRecord - } -} - -/// Returns VoteRecord PDA address -pub fn get_voter_record_address( - realm: &Pubkey, - governing_token_mint: &Pubkey, - governing_token_owner: &Pubkey, -) -> Pubkey { - Pubkey::find_program_address( - &get_voter_record_address_seeds(realm, governing_token_mint, governing_token_owner), - &id(), - ) - .0 -} - -/// Returns VoterRecord PDA seeds -pub fn get_voter_record_address_seeds<'a>( - realm: &'a Pubkey, - governing_token_mint: &'a Pubkey, - governing_token_owner: &'a Pubkey, -) -> [&'a [u8]; 4] { - [ - PROGRAM_AUTHORITY_SEED, - realm.as_ref(), - governing_token_mint.as_ref(), - governing_token_owner.as_ref(), - ] -} - -/// Deserializes VoterRecord and checks account PDA and owner program -pub fn deserialize_voter_record( - voter_record_info: &AccountInfo, - voter_record_seeds: &[&[u8]], -) -> Result { - let (voter_record_address, _) = Pubkey::find_program_address(voter_record_seeds, &id()); - - if voter_record_address != *voter_record_info.key { - return Err(GovernanceError::InvalidVoterAccountAddress.into()); - } - - deserialize_account::(voter_record_info, &id()) -} - -#[cfg(test)] -mod test { - use solana_program::borsh::get_packed_len; - - use super::*; - - #[test] - fn test_max_size() { - let vote_record = VoterRecord { - account_type: GovernanceAccountType::VoterRecord, - realm: Pubkey::new_unique(), - token_type: GoverningTokenType::Community, - token_owner: Pubkey::new_unique(), - token_deposit_amount: 10, - vote_authority: Some(Pubkey::new_unique()), - active_votes_count: 1, - total_votes_count: 1, - }; - - let size = get_packed_len::(); - - assert_eq!(vote_record.get_max_size(), Some(size)); - } -} diff --git a/governance/program/src/tools/account.rs b/governance/program/src/tools/account.rs index 04dc116711f..807073545f6 100644 --- a/governance/program/src/tools/account.rs +++ b/governance/program/src/tools/account.rs @@ -88,13 +88,13 @@ pub fn deserialize_account( account_info: &AccountInfo, owner_program_id: &Pubkey, ) -> Result { + if account_info.data_is_empty() { + return Err(ProgramError::UninitializedAccount); + } if account_info.owner != owner_program_id { return Err(GovernanceError::InvalidAccountOwner.into()); } - if account_info.data_is_empty() { - return Err(ProgramError::UninitializedAccount); - } let account: T = try_from_slice_unchecked(&account_info.data.borrow())?; if !account.is_initialized() { Err(ProgramError::UninitializedAccount) @@ -125,3 +125,19 @@ pub fn assert_is_valid_account( Ok(()) } + +/// Disposes account by transferring its lamports to the beneficiary account and zeros its data +// After transaction completes the runtime would remove the account with no lamports +pub fn dispose_account(account_info: &AccountInfo, beneficiary_account: &AccountInfo) { + let account_lamports = account_info.lamports(); + **account_info.lamports.borrow_mut() = 0; + + **beneficiary_account.lamports.borrow_mut() = beneficiary_account + .lamports() + .checked_add(account_lamports) + .unwrap(); + + let mut account_data = account_info.data.borrow_mut(); + + account_data.fill(0); +} diff --git a/governance/program/src/tools/asserts.rs b/governance/program/src/tools/asserts.rs index 1ec9ab52952..c9961dd6235 100644 --- a/governance/program/src/tools/asserts.rs +++ b/governance/program/src/tools/asserts.rs @@ -2,24 +2,24 @@ use solana_program::{account_info::AccountInfo, program_error::ProgramError}; -use crate::{error::GovernanceError, state::voter_record::VoterRecord}; +use crate::{error::GovernanceError, state::token_owner_record::TokenOwnerRecord}; -/// Checks whether the provided vote authority can set new vote authority -pub fn assert_is_signed_by_owner_or_vote_authority( - voter_record: &VoterRecord, - vote_authority_info: &AccountInfo, +/// Checks whether the provided Governance Authority signed transaction +pub fn assert_token_owner_or_delegate_is_signer( + token_owner_record: &TokenOwnerRecord, + governance_authority_info: &AccountInfo, ) -> Result<(), ProgramError> { - if vote_authority_info.is_signer { - if &voter_record.token_owner == vote_authority_info.key { + if governance_authority_info.is_signer { + if &token_owner_record.governing_token_owner == governance_authority_info.key { return Ok(()); } - if let Some(vote_authority) = voter_record.vote_authority { - if &vote_authority == vote_authority_info.key { + if let Some(governance_delegate) = token_owner_record.governance_delegate { + if &governance_delegate == governance_authority_info.key { return Ok(()); } }; } - Err(GovernanceError::GoverningTokenOwnerOrVoteAuthrotiyMustSign.into()) + Err(GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()) } diff --git a/governance/program/src/tools/bpf_loader_upgradeable.rs b/governance/program/src/tools/bpf_loader_upgradeable.rs index 52141725cd5..da285199cb8 100644 --- a/governance/program/src/tools/bpf_loader_upgradeable.rs +++ b/governance/program/src/tools/bpf_loader_upgradeable.rs @@ -72,26 +72,24 @@ pub fn assert_program_upgrade_authority_is_signer( return Err(GovernanceError::InvalidProgramDataAccountAddress.into()); } - let upgrade_authority = match deserialize(&program_data_info.data.borrow()) + let upgrade_authority = if let UpgradeableLoaderState::ProgramData { + slot: _, + upgrade_authority_address, + } = deserialize(&program_data_info.data.borrow()) .map_err(|_| GovernanceError::InvalidProgramDataAccountData)? { - UpgradeableLoaderState::ProgramData { - slot: _, - upgrade_authority_address, - } => upgrade_authority_address, - _ => None, + upgrade_authority_address + } else { + None }; - match upgrade_authority { - Some(upgrade_authority) => { - if upgrade_authority != *program_upgrade_authority_info.key { - return Err(GovernanceError::InvalidUpgradeAuthority.into()); - } - if !program_upgrade_authority_info.is_signer { - return Err(GovernanceError::UpgradeAuthorityMustSign.into()); - } - } - None => return Err(GovernanceError::ProgramNotUpgradable.into()), + let upgrade_authority = upgrade_authority.ok_or(GovernanceError::ProgramNotUpgradable)?; + + if upgrade_authority != *program_upgrade_authority_info.key { + return Err(GovernanceError::InvalidUpgradeAuthority.into()); + } + if !program_upgrade_authority_info.is_signer { + return Err(GovernanceError::UpgradeAuthorityMustSign.into()); } Ok(()) diff --git a/governance/program/src/tools/token.rs b/governance/program/src/tools/token.rs index 55f8b0a5f44..3905ada75a3 100644 --- a/governance/program/src/tools/token.rs +++ b/governance/program/src/tools/token.rs @@ -190,7 +190,7 @@ pub fn get_mint_from_token_account( } // TokeAccount layout: mint(32), owner(32), amount(8), ... - let data = token_account_info.try_borrow_data().unwrap(); + let data = token_account_info.try_borrow_data()?; let mint_data = array_ref![data, 0, 32]; Ok(Pubkey::new_from_array(*mint_data)) } @@ -205,7 +205,7 @@ pub fn get_owner_from_token_account( } // TokeAccount layout: mint(32), owner(32), amount(8) - let data = token_account_info.try_borrow_data().unwrap(); + let data = token_account_info.try_borrow_data()?; let owner_data = array_ref![data, 32, 32]; Ok(Pubkey::new_from_array(*owner_data)) } diff --git a/governance/program/tests/process_add_signatory.rs b/governance/program/tests/process_add_signatory.rs new file mode 100644 index 00000000000..2641b06095b --- /dev/null +++ b/governance/program/tests/process_add_signatory.rs @@ -0,0 +1,132 @@ +#![cfg(feature = "test-bpf")] + +mod program_test; + +use solana_program_test::tokio; + +use program_test::*; + +use spl_governance::error::GovernanceError; + +#[tokio::test] +async fn test_add_signatory() { + // 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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + let proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + // Act + let signatory_record_cookie = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + // Assert + let signatory_record_account = governance_test + .get_signatory_record_account(&signatory_record_cookie.address) + .await; + + assert_eq!(signatory_record_cookie.account, signatory_record_account); + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(1, proposal_account.signatories_count); +} + +#[tokio::test] +async fn test_add_signatory_with_owner_or_delegate_must_sign_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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let mut token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + let proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + let other_token_owner_record_cookie = governance_test + .with_initial_council_token_deposit(&realm_cookie) + .await; + + token_owner_record_cookie.token_owner = other_token_owner_record_cookie.token_owner; + + // Act + let err = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into() + ); +} + +#[tokio::test] +async fn test_add_signatory_with_invalid_proposal_owner_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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let mut token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + let proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + let other_token_owner_record_cookie = governance_test + .with_initial_council_token_deposit(&realm_cookie) + .await; + + token_owner_record_cookie.address = other_token_owner_record_cookie.address; + + // Act + let err = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie) + .await + .err() + .unwrap(); + + // Assert + assert_eq!(err, GovernanceError::InvalidProposalOwnerAccount.into()); +} diff --git a/governance/program/tests/process_create_proposal.rs b/governance/program/tests/process_create_proposal.rs new file mode 100644 index 00000000000..8085ba52be6 --- /dev/null +++ b/governance/program/tests/process_create_proposal.rs @@ -0,0 +1,255 @@ +#![cfg(feature = "test-bpf")] + +use solana_program::instruction::AccountMeta; +use solana_program_test::*; + +mod program_test; + +use program_test::*; +use solana_sdk::signature::Keypair; +use spl_governance::error::GovernanceError; + +#[tokio::test] +async fn test_community_proposal_created() { + // 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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + // Act + let proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + // Assert + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(proposal_cookie.account, proposal_account); + + let account_governance_account = governance_test + .get_governance_account(&account_governance_cookie.address) + .await; + + assert_eq!(1, account_governance_account.proposals_count); +} + +#[tokio::test] +async fn test_multiple_proposals_created() { + // 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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let community_token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + let council_token_owner_record_cookie = governance_test + .with_initial_council_token_deposit(&realm_cookie) + .await; + + // Act + let community_proposal_cookie = governance_test + .with_proposal( + &community_token_owner_record_cookie, + &mut account_governance_cookie, + ) + .await + .unwrap(); + + let council_proposal_cookie = governance_test + .with_proposal( + &council_token_owner_record_cookie, + &mut account_governance_cookie, + ) + .await + .unwrap(); + + // Assert + let community_proposal_account = governance_test + .get_proposal_account(&community_proposal_cookie.address) + .await; + + assert_eq!( + community_proposal_cookie.account, + community_proposal_account + ); + + let council_proposal_account = governance_test + .get_proposal_account(&council_proposal_cookie.address) + .await; + + assert_eq!(council_proposal_cookie.account, council_proposal_account); + + let account_governance_account = governance_test + .get_governance_account(&account_governance_cookie.address) + .await; + + assert_eq!(2, account_governance_account.proposals_count); +} + +#[tokio::test] +async fn test_create_proposal_with_not_authorized_governance_authority_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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let mut token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + token_owner_record_cookie.governance_authority = Some(Keypair::new()); + + // Act + let err = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into() + ); +} + +#[tokio::test] +async fn test_create_proposal_with_governance_delegate_signer() { + // 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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let mut token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + governance_test + .with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie) + .await; + + token_owner_record_cookie.governance_authority = + Some(token_owner_record_cookie.clone_governance_delegate()); + + // Act + let proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + // Assert + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(proposal_cookie.account, proposal_account); +} + +#[tokio::test] +async fn test_create_proposal_with_not_enough_tokens_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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let token_amount = account_governance_cookie + .account + .config + .min_tokens_to_create_proposal as u64 + - 1; + + let token_owner_record_cookie = governance_test + .with_initial_community_token_deposit_amount(&realm_cookie, token_amount) + .await; + + // Act + let err = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .err() + .unwrap(); + + // Assert + assert_eq!(err, GovernanceError::NotEnoughTokensToCreateProposal.into()); +} + +#[tokio::test] +async fn test_create_proposal_with_invalid_token_owner_record_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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + let council_token_owner_record_cookie = governance_test + .with_initial_council_token_deposit(&realm_cookie) + .await; + + // Act + let err = governance_test + .with_proposal_instruction( + &token_owner_record_cookie, + &mut account_governance_cookie, + |i| { + // Set token_owner_record_address for different (Council) mint + i.accounts[2] = + AccountMeta::new_readonly(council_token_owner_record_cookie.address, false); + }, + ) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::InvalidTokenOwnerRecordGoverningMint.into() + ); +} diff --git a/governance/program/tests/process_deposit_governing_tokens.rs b/governance/program/tests/process_deposit_governing_tokens.rs index 92744294d3b..ad288e066db 100644 --- a/governance/program/tests/process_deposit_governing_tokens.rs +++ b/governance/program/tests/process_deposit_governing_tokens.rs @@ -16,24 +16,27 @@ async fn test_deposit_initial_community_tokens() { let realm_cookie = governance_test.with_realm().await; // Act - let voter_record_cookie = governance_test + let token_owner_record_cookie = governance_test .with_initial_community_token_deposit(&realm_cookie) .await; // Assert - let voter_record = governance_test - .get_voter_record_account(&voter_record_cookie.address) + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) .await; - assert_eq!(voter_record_cookie.account, voter_record); + assert_eq!(token_owner_record_cookie.account, token_owner_record); let source_account = governance_test - .get_token_account(&voter_record_cookie.token_source) + .get_token_account(&token_owner_record_cookie.token_source) .await; assert_eq!( - voter_record_cookie.token_source_amount - voter_record_cookie.account.token_deposit_amount, + token_owner_record_cookie.token_source_amount + - token_owner_record_cookie + .account + .governing_token_deposit_amount, source_account.amount ); @@ -41,7 +44,10 @@ async fn test_deposit_initial_community_tokens() { .get_token_account(&realm_cookie.community_token_holding_account) .await; - assert_eq!(voter_record.token_deposit_amount, holding_account.amount); + assert_eq!( + token_owner_record.governing_token_deposit_amount, + holding_account.amount + ); } #[tokio::test] @@ -53,23 +59,26 @@ async fn test_deposit_initial_council_tokens() { let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap(); // Act - let voter_record_cookie = governance_test + let token_owner_record_cookie = governance_test .with_initial_council_token_deposit(&realm_cookie) .await; // Assert - let voter_record = governance_test - .get_voter_record_account(&voter_record_cookie.address) + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) .await; - assert_eq!(voter_record_cookie.account, voter_record); + assert_eq!(token_owner_record_cookie.account, token_owner_record); let source_account = governance_test - .get_token_account(&voter_record_cookie.token_source) + .get_token_account(&token_owner_record_cookie.token_source) .await; assert_eq!( - voter_record_cookie.token_source_amount - voter_record_cookie.account.token_deposit_amount, + token_owner_record_cookie.token_source_amount + - token_owner_record_cookie + .account + .governing_token_deposit_amount, source_account.amount ); @@ -77,7 +86,10 @@ async fn test_deposit_initial_council_tokens() { .get_token_account(&council_token_holding_account) .await; - assert_eq!(voter_record.token_deposit_amount, holding_account.amount); + assert_eq!( + token_owner_record.governing_token_deposit_amount, + holding_account.amount + ); } #[tokio::test] @@ -86,24 +98,30 @@ async fn test_deposit_subsequent_community_tokens() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let voter_record_cookie = governance_test + let token_owner_record_cookie = governance_test .with_initial_community_token_deposit(&realm_cookie) .await; let deposit_amount = 5; - let total_deposit_amount = voter_record_cookie.account.token_deposit_amount + deposit_amount; + let total_deposit_amount = token_owner_record_cookie + .account + .governing_token_deposit_amount + + deposit_amount; // Act governance_test - .with_community_token_deposit(&realm_cookie, &voter_record_cookie, deposit_amount) + .with_community_token_deposit(&realm_cookie, &token_owner_record_cookie, deposit_amount) .await; // Assert - let voter_record = governance_test - .get_voter_record_account(&voter_record_cookie.address) + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) .await; - assert_eq!(total_deposit_amount, voter_record.token_deposit_amount); + assert_eq!( + total_deposit_amount, + token_owner_record.governing_token_deposit_amount + ); let holding_account = governance_test .get_token_account(&realm_cookie.community_token_holding_account) @@ -120,24 +138,30 @@ async fn test_deposit_subsequent_council_tokens() { let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap(); - let voter_record_cookie = governance_test + let token_owner_record_cookie = governance_test .with_initial_council_token_deposit(&realm_cookie) .await; let deposit_amount = 5; - let total_deposit_amount = voter_record_cookie.account.token_deposit_amount + deposit_amount; + let total_deposit_amount = token_owner_record_cookie + .account + .governing_token_deposit_amount + + deposit_amount; // Act governance_test - .with_council_token_deposit(&realm_cookie, &voter_record_cookie, deposit_amount) + .with_council_token_deposit(&realm_cookie, &token_owner_record_cookie, deposit_amount) .await; // Assert - let voter_record = governance_test - .get_voter_record_account(&voter_record_cookie.address) + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) .await; - assert_eq!(total_deposit_amount, voter_record.token_deposit_amount); + assert_eq!( + total_deposit_amount, + token_owner_record.governing_token_deposit_amount + ); let holding_account = governance_test .get_token_account(&council_token_holding_account) diff --git a/governance/program/tests/process_remove_signatory.rs b/governance/program/tests/process_remove_signatory.rs new file mode 100644 index 00000000000..063ef1e7903 --- /dev/null +++ b/governance/program/tests/process_remove_signatory.rs @@ -0,0 +1,219 @@ +#![cfg(feature = "test-bpf")] + +mod program_test; + +use solana_program_test::tokio; + +use program_test::*; + +use spl_governance::{error::GovernanceError, state::enums::ProposalState}; + +#[tokio::test] +async fn test_remove_signatory() { + // 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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + let proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + // Act + governance_test + .remove_signatory( + &proposal_cookie, + &token_owner_record_cookie, + &signatory_record_cookie, + ) + .await + .unwrap(); + + // Assert + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(0, proposal_account.signatories_count); + assert_eq!(ProposalState::Draft, proposal_account.state); + + let signatory_account = governance_test + .banks_client + .get_account(signatory_record_cookie.address) + .await + .unwrap(); + + assert_eq!(None, signatory_account); +} + +#[tokio::test] +async fn test_remove_signatory_with_owner_or_delegate_must_sign_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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let mut token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + let proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + let other_token_owner_record_cookie = governance_test + .with_initial_council_token_deposit(&realm_cookie) + .await; + + token_owner_record_cookie.token_owner = other_token_owner_record_cookie.token_owner; + + // Act + let err = governance_test + .remove_signatory( + &proposal_cookie, + &token_owner_record_cookie, + &signatory_record_cookie, + ) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into() + ); +} + +#[tokio::test] +async fn test_remove_signatory_with_invalid_proposal_owner_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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let mut token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + let proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + let other_token_owner_record_cookie = governance_test + .with_initial_council_token_deposit(&realm_cookie) + .await; + + token_owner_record_cookie.address = other_token_owner_record_cookie.address; + + // Act + let err = governance_test + .remove_signatory( + &proposal_cookie, + &token_owner_record_cookie, + &signatory_record_cookie, + ) + .await + .err() + .unwrap(); + + // Assert + assert_eq!(err, GovernanceError::InvalidProposalOwnerAccount.into()); +} + +#[tokio::test] +async fn test_remove_signatory_when_all_remaining_signed() { + // 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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + let proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie1 = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + let signatory_record_cookie2 = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie1) + .await + .unwrap(); + + // Act + governance_test + .remove_signatory( + &proposal_cookie, + &token_owner_record_cookie, + &signatory_record_cookie2, + ) + .await + .unwrap(); + + // Assert + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(1, proposal_account.signatories_count); + assert_eq!(1, proposal_account.signatories_signed_off_count); + assert_eq!(ProposalState::Voting, proposal_account.state); +} diff --git a/governance/program/tests/process_set_governance_delegate.rs b/governance/program/tests/process_set_governance_delegate.rs new file mode 100644 index 00000000000..7ba419548d8 --- /dev/null +++ b/governance/program/tests/process_set_governance_delegate.rs @@ -0,0 +1,165 @@ +#![cfg(feature = "test-bpf")] + +use solana_program::instruction::AccountMeta; +use solana_program_test::*; + +mod program_test; + +use program_test::*; +use solana_sdk::signature::{Keypair, Signer}; +use spl_governance::{error::GovernanceError, instruction::set_governance_delegate}; + +#[tokio::test] +async fn test_set_community_governance_delegate() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm().await; + let mut token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + // Act + governance_test + .with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie) + .await; + + // Assert + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!( + Some(token_owner_record_cookie.governance_delegate.pubkey()), + token_owner_record.governance_delegate + ); +} + +#[tokio::test] +async fn test_set_governance_delegate_to_none() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm().await; + let mut token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + governance_test + .with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie) + .await; + + // Act + governance_test + .set_governance_delegate( + &realm_cookie, + &token_owner_record_cookie, + &token_owner_record_cookie.token_owner, + &realm_cookie.account.community_mint, + &None, + ) + .await; + + // Assert + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(None, token_owner_record.governance_delegate); +} + +#[tokio::test] +async fn test_set_council_governance_delegate() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm().await; + let mut token_owner_record_cookie = governance_test + .with_initial_council_token_deposit(&realm_cookie) + .await; + + // Act + governance_test + .with_council_governance_delegate(&realm_cookie, &mut token_owner_record_cookie) + .await; + + // Assert + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!( + Some(token_owner_record_cookie.governance_delegate.pubkey()), + token_owner_record.governance_delegate + ); +} + +#[tokio::test] +async fn test_set_community_governance_delegate_with_owner_must_sign_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm().await; + let token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + let hacker_governance_delegate = Keypair::new(); + + let mut instruction = set_governance_delegate( + &token_owner_record_cookie.token_owner.pubkey(), + &realm_cookie.address, + &realm_cookie.account.community_mint, + &token_owner_record_cookie.token_owner.pubkey(), + &Some(hacker_governance_delegate.pubkey()), + ); + + instruction.accounts[0] = + AccountMeta::new_readonly(token_owner_record_cookie.token_owner.pubkey(), false); + + // Act + let err = governance_test + .process_transaction(&[instruction], None) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into() + ); +} + +#[tokio::test] +async fn test_set_community_governance_delegate_signed_by_governance_delegate() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm().await; + let mut token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + governance_test + .with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie) + .await; + + let new_governance_delegate = Keypair::new(); + + // Act + governance_test + .set_governance_delegate( + &realm_cookie, + &token_owner_record_cookie, + &token_owner_record_cookie.governance_delegate, + &realm_cookie.account.community_mint, + &Some(new_governance_delegate.pubkey()), + ) + .await; + + // Assert + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!( + Some(new_governance_delegate.pubkey()), + token_owner_record.governance_delegate + ); +} diff --git a/governance/program/tests/process_set_vote_authority.rs b/governance/program/tests/process_set_vote_authority.rs deleted file mode 100644 index 477ccc5aae9..00000000000 --- a/governance/program/tests/process_set_vote_authority.rs +++ /dev/null @@ -1,165 +0,0 @@ -#![cfg(feature = "test-bpf")] - -use solana_program::instruction::AccountMeta; -use solana_program_test::*; - -mod program_test; - -use program_test::*; -use solana_sdk::signature::{Keypair, Signer}; -use spl_governance::{error::GovernanceError, instruction::set_vote_authority}; - -#[tokio::test] -async fn test_set_community_vote_authority() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - let realm_cookie = governance_test.with_realm().await; - let mut voter_record_cookie = governance_test - .with_initial_community_token_deposit(&realm_cookie) - .await; - - // Act - governance_test - .with_community_vote_authority(&realm_cookie, &mut voter_record_cookie) - .await; - - // Assert - let voter_record = governance_test - .get_voter_record_account(&voter_record_cookie.address) - .await; - - assert_eq!( - Some(voter_record_cookie.vote_authority.pubkey()), - voter_record.vote_authority - ); -} - -#[tokio::test] -async fn test_set_vote_authority_to_none() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - let realm_cookie = governance_test.with_realm().await; - let mut voter_record_cookie = governance_test - .with_initial_community_token_deposit(&realm_cookie) - .await; - - governance_test - .with_community_vote_authority(&realm_cookie, &mut voter_record_cookie) - .await; - - // Act - governance_test - .set_vote_authority( - &realm_cookie, - &voter_record_cookie, - &voter_record_cookie.token_owner, - &realm_cookie.account.community_mint, - &None, - ) - .await; - - // Assert - let voter_record = governance_test - .get_voter_record_account(&voter_record_cookie.address) - .await; - - assert_eq!(None, voter_record.vote_authority); -} - -#[tokio::test] -async fn test_set_council_vote_authority() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - let realm_cookie = governance_test.with_realm().await; - let mut voter_record_cookie = governance_test - .with_initial_council_token_deposit(&realm_cookie) - .await; - - // Act - governance_test - .with_council_vote_authority(&realm_cookie, &mut voter_record_cookie) - .await; - - // Assert - let voter_record = governance_test - .get_voter_record_account(&voter_record_cookie.address) - .await; - - assert_eq!( - Some(voter_record_cookie.vote_authority.pubkey()), - voter_record.vote_authority - ); -} - -#[tokio::test] -async fn test_set_community_vote_authority_with_owner_must_sign_error() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - let realm_cookie = governance_test.with_realm().await; - let voter_record_cookie = governance_test - .with_initial_community_token_deposit(&realm_cookie) - .await; - - let hacker_vote_authority = Keypair::new(); - - let mut instruction = set_vote_authority( - &voter_record_cookie.token_owner.pubkey(), - &realm_cookie.address, - &realm_cookie.account.community_mint, - &voter_record_cookie.token_owner.pubkey(), - &Some(hacker_vote_authority.pubkey()), - ); - - instruction.accounts[0] = - AccountMeta::new_readonly(voter_record_cookie.token_owner.pubkey(), false); - - // Act - let err = governance_test - .process_transaction(&[instruction], None) - .await - .err() - .unwrap(); - - // Assert - assert_eq!( - err, - GovernanceError::GoverningTokenOwnerOrVoteAuthrotiyMustSign.into() - ); -} - -#[tokio::test] -async fn test_set_community_vote_authority_signed_by_vote_authority() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - let realm_cookie = governance_test.with_realm().await; - let mut voter_record_cookie = governance_test - .with_initial_community_token_deposit(&realm_cookie) - .await; - - governance_test - .with_community_vote_authority(&realm_cookie, &mut voter_record_cookie) - .await; - - let new_vote_authority = Keypair::new(); - - // Act - governance_test - .set_vote_authority( - &realm_cookie, - &voter_record_cookie, - &voter_record_cookie.vote_authority, - &realm_cookie.account.community_mint, - &Some(new_vote_authority.pubkey()), - ) - .await; - - // Assert - let voter_record = governance_test - .get_voter_record_account(&voter_record_cookie.address) - .await; - - assert_eq!( - Some(new_vote_authority.pubkey()), - voter_record.vote_authority - ); -} diff --git a/governance/program/tests/process_sign_off_proposal.rs b/governance/program/tests/process_sign_off_proposal.rs new file mode 100644 index 00000000000..9dc1df4ca67 --- /dev/null +++ b/governance/program/tests/process_sign_off_proposal.rs @@ -0,0 +1,59 @@ +#![cfg(feature = "test-bpf")] + +mod program_test; + +use solana_program_test::tokio; + +use program_test::*; +use spl_governance::state::enums::ProposalState; + +#[tokio::test] +async fn test_sign_off_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 mut account_governance_cookie = governance_test + .with_account_governance(&realm_cookie, &governed_account_cookie) + .await + .unwrap(); + + let token_owner_record_cookie = governance_test + .with_initial_community_token_deposit(&realm_cookie) + .await; + + let proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + // Act + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + // Assert + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(1, proposal_account.signatories_count); + assert_eq!(1, proposal_account.signatories_signed_off_count); + assert_eq!(ProposalState::Voting, proposal_account.state); + assert_eq!(Some(1), proposal_account.signing_off_at); + assert_eq!(Some(1), proposal_account.voting_at); + + let signatory_record_account = governance_test + .get_signatory_record_account(&signatory_record_cookie.address) + .await; + + assert_eq!(true, signatory_record_account.signed_off); +} diff --git a/governance/program/tests/process_withdraw_governing_tokens.rs b/governance/program/tests/process_withdraw_governing_tokens.rs index b4185773a81..747b71e7822 100644 --- a/governance/program/tests/process_withdraw_governing_tokens.rs +++ b/governance/program/tests/process_withdraw_governing_tokens.rs @@ -10,7 +10,7 @@ use solana_sdk::signature::Signer; use spl_governance::{ error::GovernanceError, instruction::withdraw_governing_tokens, - state::voter_record::get_voter_record_address, + state::token_owner_record::get_token_owner_record_address, }; #[tokio::test] @@ -19,22 +19,22 @@ async fn test_withdraw_community_tokens() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let voter_record_cookie = governance_test + let token_owner_record_cookie = governance_test .with_initial_community_token_deposit(&realm_cookie) .await; // Act governance_test - .withdraw_community_tokens(&realm_cookie, &voter_record_cookie) + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie) .await .unwrap(); // Assert - let voter_record = governance_test - .get_voter_record_account(&voter_record_cookie.address) + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) .await; - assert_eq!(0, voter_record.token_deposit_amount); + assert_eq!(0, token_owner_record.governing_token_deposit_amount); let holding_account = governance_test .get_token_account(&realm_cookie.community_token_holding_account) @@ -43,11 +43,11 @@ async fn test_withdraw_community_tokens() { assert_eq!(0, holding_account.amount); let source_account = governance_test - .get_token_account(&voter_record_cookie.token_source) + .get_token_account(&token_owner_record_cookie.token_source) .await; assert_eq!( - voter_record_cookie.token_source_amount, + token_owner_record_cookie.token_source_amount, source_account.amount ); } @@ -58,22 +58,22 @@ async fn test_withdraw_council_tokens() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let voter_record_cookie = governance_test + let token_owner_record_cookie = governance_test .with_initial_council_token_deposit(&realm_cookie) .await; // Act governance_test - .withdraw_council_tokens(&realm_cookie, &voter_record_cookie) + .withdraw_council_tokens(&realm_cookie, &token_owner_record_cookie) .await .unwrap(); // Assert - let voter_record = governance_test - .get_voter_record_account(&voter_record_cookie.address) + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) .await; - assert_eq!(0, voter_record.token_deposit_amount); + assert_eq!(0, token_owner_record.governing_token_deposit_amount); let holding_account = governance_test .get_token_account(&realm_cookie.council_token_holding_account.unwrap()) @@ -82,11 +82,11 @@ async fn test_withdraw_council_tokens() { assert_eq!(0, holding_account.amount); let source_account = governance_test - .get_token_account(&voter_record_cookie.token_source) + .get_token_account(&token_owner_record_cookie.token_source) .await; assert_eq!( - voter_record_cookie.token_source_amount, + token_owner_record_cookie.token_source_amount, source_account.amount ); } @@ -97,7 +97,7 @@ async fn test_withdraw_community_tokens_with_owner_must_sign_error() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let voter_record_cookie = governance_test + let token_owner_record_cookie = governance_test .with_initial_community_token_deposit(&realm_cookie) .await; @@ -106,12 +106,12 @@ async fn test_withdraw_community_tokens_with_owner_must_sign_error() { let mut instruction = withdraw_governing_tokens( &realm_cookie.address, &hacker_token_destination, - &voter_record_cookie.token_owner.pubkey(), + &token_owner_record_cookie.token_owner.pubkey(), &realm_cookie.account.community_mint, ); instruction.accounts[3] = - AccountMeta::new_readonly(voter_record_cookie.token_owner.pubkey(), false); + AccountMeta::new_readonly(token_owner_record_cookie.token_owner.pubkey(), false); // Act let err = governance_test @@ -126,19 +126,19 @@ async fn test_withdraw_community_tokens_with_owner_must_sign_error() { } #[tokio::test] -async fn test_withdraw_community_tokens_with_voter_record_address_mismatch_error() { +async fn test_withdraw_community_tokens_with_token_owner_record_address_mismatch_error() { // Arrange let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let voter_record_cookie = governance_test + let token_owner_record_cookie = governance_test .with_initial_community_token_deposit(&realm_cookie) .await; - let vote_record_address = get_voter_record_address( + let vote_record_address = get_token_owner_record_address( &realm_cookie.address, &realm_cookie.account.community_mint, - &voter_record_cookie.token_owner.pubkey(), + &token_owner_record_cookie.token_owner.pubkey(), ); let hacker_record_cookie = governance_test @@ -163,5 +163,8 @@ async fn test_withdraw_community_tokens_with_voter_record_address_mismatch_error // Assert - assert_eq!(err, GovernanceError::InvalidVoterAccountAddress.into()); + assert_eq!( + err, + GovernanceError::InvalidTokenOwnerRecordAccountAddress.into() + ); } diff --git a/governance/program/tests/program_test/cookies.rs b/governance/program/tests/program_test/cookies.rs index 679248e86c2..c5b79513389 100644 --- a/governance/program/tests/program_test/cookies.rs +++ b/governance/program/tests/program_test/cookies.rs @@ -1,6 +1,11 @@ use solana_program::pubkey::Pubkey; use solana_sdk::signature::Keypair; -use spl_governance::state::{governance::Governance, realm::Realm, voter_record::VoterRecord}; +use spl_governance::state::{ + governance::Governance, realm::Realm, token_owner_record::TokenOwnerRecord, +}; +use spl_governance::state::{proposal::Proposal, signatory_record::SignatoryRecord}; + +use super::tools::clone_keypair; #[derive(Debug)] pub struct RealmCookie { @@ -18,10 +23,10 @@ pub struct RealmCookie { } #[derive(Debug)] -pub struct VoterRecordCookie { +pub struct TokeOwnerRecordCookie { pub address: Pubkey, - pub account: VoterRecord, + pub account: TokenOwnerRecord, pub token_source: Pubkey, @@ -29,7 +34,24 @@ pub struct VoterRecordCookie { pub token_owner: Keypair, - pub vote_authority: Keypair, + pub governance_authority: Option, + + pub governance_delegate: Keypair, + + pub governing_token_mint: Pubkey, +} + +impl TokeOwnerRecordCookie { + pub fn get_governance_authority(&self) -> &Keypair { + self.governance_authority + .as_ref() + .unwrap_or(&self.token_owner) + } + + #[allow(dead_code)] + pub fn clone_governance_delegate(&self) -> Keypair { + clone_keypair(&self.governance_delegate) + } } #[derive(Debug)] @@ -49,4 +71,20 @@ pub struct GovernedAccountCookie { pub struct GovernanceCookie { pub address: Pubkey, pub account: Governance, + pub next_proposal_index: u16, +} + +#[derive(Debug)] +pub struct ProposalCookie { + pub address: Pubkey, + pub account: Proposal, + + pub proposal_owner: Pubkey, +} + +#[derive(Debug)] +pub struct SignatoryRecordCookie { + pub address: Pubkey, + pub account: SignatoryRecord, + pub signatory: Keypair, } diff --git a/governance/program/tests/program_test/mod.rs b/governance/program/tests/program_test/mod.rs index eb8fd8f52e4..badad76c9da 100644 --- a/governance/program/tests/program_test/mod.rs +++ b/governance/program/tests/program_test/mod.rs @@ -23,25 +23,34 @@ use solana_sdk::{ }; use spl_governance::{ instruction::{ - create_account_governance, create_program_governance, create_realm, - deposit_governing_tokens, set_vote_authority, withdraw_governing_tokens, + add_signatory, create_account_governance, create_program_governance, create_proposal, + create_realm, deposit_governing_tokens, remove_signatory, set_governance_delegate, + sign_off_proposal, withdraw_governing_tokens, }, processor::process_instruction, state::{ - enums::{GovernanceAccountType, GoverningTokenType}, + enums::{GovernanceAccountType, ProposalState}, governance::{ get_account_governance_address, get_program_governance_address, Governance, GovernanceConfig, }, + proposal::{get_proposal_address, Proposal}, realm::{get_governing_token_holding_address, get_realm_address, Realm}, - voter_record::{get_voter_record_address, VoterRecord}, + signatory_record::{get_signatory_record_address, SignatoryRecord}, + token_owner_record::{get_token_owner_record_address, TokenOwnerRecord}, }, tools::bpf_loader_upgradeable::get_program_data_address, }; pub mod cookies; -use self::cookies::{ - GovernanceCookie, GovernedAccountCookie, GovernedProgramCookie, RealmCookie, VoterRecordCookie, +use crate::program_test::cookies::SignatoryRecordCookie; + +use self::{ + cookies::{ + GovernanceCookie, GovernedAccountCookie, GovernedProgramCookie, ProposalCookie, + RealmCookie, TokeOwnerRecordCookie, + }, + tools::NopOverride, }; pub mod tools; @@ -166,12 +175,27 @@ impl GovernanceProgramTest { pub async fn with_initial_community_token_deposit( &mut self, realm_cookie: &RealmCookie, - ) -> VoterRecordCookie { + ) -> TokeOwnerRecordCookie { self.with_initial_governing_token_deposit( &realm_cookie.address, - GoverningTokenType::Community, &realm_cookie.account.community_mint, &realm_cookie.community_mint_authority, + 100, + ) + .await + } + + #[allow(dead_code)] + pub async fn with_initial_community_token_deposit_amount( + &mut self, + realm_cookie: &RealmCookie, + amount: u64, + ) -> TokeOwnerRecordCookie { + self.with_initial_governing_token_deposit( + &realm_cookie.address, + &realm_cookie.account.community_mint, + &realm_cookie.community_mint_authority, + amount, ) .await } @@ -180,14 +204,14 @@ impl GovernanceProgramTest { pub async fn with_community_token_deposit( &mut self, realm_cookie: &RealmCookie, - voter_record_cookie: &VoterRecordCookie, + token_owner_record_cookie: &TokeOwnerRecordCookie, amount: u64, ) { self.with_governing_token_deposit( &realm_cookie.address, &realm_cookie.account.community_mint, &realm_cookie.community_mint_authority, - voter_record_cookie, + token_owner_record_cookie, amount, ) .await; @@ -197,14 +221,14 @@ impl GovernanceProgramTest { pub async fn with_council_token_deposit( &mut self, realm_cookie: &RealmCookie, - voter_record_cookie: &VoterRecordCookie, + token_owner_record_cookie: &TokeOwnerRecordCookie, amount: u64, ) { self.with_governing_token_deposit( &realm_cookie.address, &realm_cookie.account.council_mint.unwrap(), &realm_cookie.council_mint_authority.as_ref().unwrap(), - voter_record_cookie, + token_owner_record_cookie, amount, ) .await; @@ -214,12 +238,12 @@ impl GovernanceProgramTest { pub async fn with_initial_council_token_deposit( &mut self, realm_cookie: &RealmCookie, - ) -> VoterRecordCookie { + ) -> TokeOwnerRecordCookie { self.with_initial_governing_token_deposit( &realm_cookie.address, - GoverningTokenType::Council, &realm_cookie.account.council_mint.unwrap(), &realm_cookie.council_mint_authority.as_ref().unwrap(), + 100, ) .await } @@ -228,21 +252,20 @@ impl GovernanceProgramTest { pub async fn with_initial_governing_token_deposit( &mut self, realm_address: &Pubkey, - governing_token_type: GoverningTokenType, governing_mint: &Pubkey, governing_mint_authority: &Keypair, - ) -> VoterRecordCookie { + amount: u64, + ) -> TokeOwnerRecordCookie { let token_owner = Keypair::new(); let token_source = Keypair::new(); - let source_amount = 100; let transfer_authority = Keypair::new(); self.create_token_account_with_transfer_authority( &token_source, governing_mint, governing_mint_authority, - source_amount, + amount, &token_owner, &transfer_authority.pubkey(), ) @@ -264,30 +287,32 @@ impl GovernanceProgramTest { .await .unwrap(); - let voter_record_address = - get_voter_record_address(realm_address, &governing_mint, &token_owner.pubkey()); + let token_owner_record_address = + get_token_owner_record_address(realm_address, &governing_mint, &token_owner.pubkey()); - let account = VoterRecord { - account_type: GovernanceAccountType::VoterRecord, + let account = TokenOwnerRecord { + account_type: GovernanceAccountType::TokenOwnerRecord, realm: *realm_address, - token_type: governing_token_type, - token_owner: token_owner.pubkey(), - token_deposit_amount: source_amount, - vote_authority: None, + governing_token_mint: *governing_mint, + governing_token_owner: token_owner.pubkey(), + governing_token_deposit_amount: amount, + governance_delegate: None, active_votes_count: 0, total_votes_count: 0, }; - let vote_authority = Keypair::from_base58_string(&token_owner.to_base58_string()); + let governance_delegate = Keypair::from_base58_string(&token_owner.to_base58_string()); - VoterRecordCookie { - address: voter_record_address, + TokeOwnerRecordCookie { + address: token_owner_record_address, account, - token_source_amount: source_amount, + token_source_amount: amount, token_source: token_source.pubkey(), token_owner, - vote_authority, + governance_authority: None, + governance_delegate: governance_delegate, + governing_token_mint: *governing_mint, } } @@ -297,103 +322,103 @@ impl GovernanceProgramTest { realm: &Pubkey, governing_token_mint: &Pubkey, governing_token_mint_authority: &Keypair, - voter_record_cookie: &VoterRecordCookie, + token_owner_record_cookie: &TokeOwnerRecordCookie, amount: u64, ) { self.mint_tokens( governing_token_mint, governing_token_mint_authority, - &voter_record_cookie.token_source, + &token_owner_record_cookie.token_source, amount, ) .await; let deposit_governing_tokens_instruction = deposit_governing_tokens( realm, - &voter_record_cookie.token_source, - &voter_record_cookie.token_owner.pubkey(), - &voter_record_cookie.token_owner.pubkey(), + &token_owner_record_cookie.token_source, + &token_owner_record_cookie.token_owner.pubkey(), + &token_owner_record_cookie.token_owner.pubkey(), &self.payer.pubkey(), governing_token_mint, ); self.process_transaction( &[deposit_governing_tokens_instruction], - Some(&[&voter_record_cookie.token_owner]), + Some(&[&token_owner_record_cookie.token_owner]), ) .await .unwrap(); } #[allow(dead_code)] - pub async fn with_community_vote_authority( + pub async fn with_community_governance_delegate( &mut self, realm_cookie: &RealmCookie, - voter_record_cookie: &mut VoterRecordCookie, + token_owner_record_cookie: &mut TokeOwnerRecordCookie, ) { - self.with_governing_token_vote_authority( + self.with_governing_token_governance_delegate( &realm_cookie, &realm_cookie.account.community_mint, - voter_record_cookie, + token_owner_record_cookie, ) .await; } #[allow(dead_code)] - pub async fn with_council_vote_authority( + pub async fn with_council_governance_delegate( &mut self, realm_cookie: &RealmCookie, - voter_record_cookie: &mut VoterRecordCookie, + token_owner_record_cookie: &mut TokeOwnerRecordCookie, ) { - self.with_governing_token_vote_authority( + self.with_governing_token_governance_delegate( &realm_cookie, &realm_cookie.account.council_mint.unwrap(), - voter_record_cookie, + token_owner_record_cookie, ) .await; } #[allow(dead_code)] - pub async fn with_governing_token_vote_authority( + pub async fn with_governing_token_governance_delegate( &mut self, realm_cookie: &RealmCookie, governing_token_mint: &Pubkey, - voter_record_cookie: &mut VoterRecordCookie, + token_owner_record_cookie: &mut TokeOwnerRecordCookie, ) { - let new_vote_authority = Keypair::new(); + let new_governance_delegate = Keypair::new(); - self.set_vote_authority( + self.set_governance_delegate( realm_cookie, - voter_record_cookie, - &voter_record_cookie.token_owner, + token_owner_record_cookie, + &token_owner_record_cookie.token_owner, governing_token_mint, - &Some(new_vote_authority.pubkey()), + &Some(new_governance_delegate.pubkey()), ) .await; - voter_record_cookie.vote_authority = new_vote_authority; + token_owner_record_cookie.governance_delegate = new_governance_delegate; } #[allow(dead_code)] - pub async fn set_vote_authority( + pub async fn set_governance_delegate( &mut self, realm_cookie: &RealmCookie, - voter_record_cookie: &VoterRecordCookie, - signing_vote_authority: &Keypair, + token_owner_record_cookie: &TokeOwnerRecordCookie, + signing_governance_authority: &Keypair, governing_token_mint: &Pubkey, - new_vote_authority: &Option, + new_governance_delegate: &Option, ) { - let set_vote_authority_instruction = set_vote_authority( - &signing_vote_authority.pubkey(), + let set_governance_delegate_instruction = set_governance_delegate( + &signing_governance_authority.pubkey(), &realm_cookie.address, governing_token_mint, - &voter_record_cookie.token_owner.pubkey(), - new_vote_authority, + &token_owner_record_cookie.token_owner.pubkey(), + new_governance_delegate, ); self.process_transaction( - &[set_vote_authority_instruction], - Some(&[&signing_vote_authority]), + &[set_governance_delegate_instruction], + Some(&[&signing_governance_authority]), ) .await .unwrap(); @@ -403,13 +428,13 @@ impl GovernanceProgramTest { pub async fn withdraw_community_tokens( &mut self, realm_cookie: &RealmCookie, - voter_record_cookie: &VoterRecordCookie, + token_owner_record_cookie: &TokeOwnerRecordCookie, ) -> Result<(), ProgramError> { self.withdraw_governing_tokens( realm_cookie, - voter_record_cookie, + token_owner_record_cookie, &realm_cookie.account.community_mint, - &voter_record_cookie.token_owner, + &token_owner_record_cookie.token_owner, ) .await } @@ -418,13 +443,13 @@ impl GovernanceProgramTest { pub async fn withdraw_council_tokens( &mut self, realm_cookie: &RealmCookie, - voter_record_cookie: &VoterRecordCookie, + token_owner_record_cookie: &TokeOwnerRecordCookie, ) -> Result<(), ProgramError> { self.withdraw_governing_tokens( realm_cookie, - voter_record_cookie, + token_owner_record_cookie, &realm_cookie.account.council_mint.unwrap(), - &voter_record_cookie.token_owner, + &token_owner_record_cookie.token_owner, ) .await } @@ -433,14 +458,14 @@ impl GovernanceProgramTest { async fn withdraw_governing_tokens( &mut self, realm_cookie: &RealmCookie, - voter_record_cookie: &VoterRecordCookie, + token_owner_record_cookie: &TokeOwnerRecordCookie, governing_token_mint: &Pubkey, governing_token_owner: &Keypair, ) -> Result<(), ProgramError> { let deposit_governing_tokens_instruction = withdraw_governing_tokens( &realm_cookie.address, - &voter_record_cookie.token_source, + &token_owner_record_cookie.token_source, &governing_token_owner.pubkey(), governing_token_mint, ); @@ -491,7 +516,7 @@ impl GovernanceProgramTest { let account = Governance { account_type: GovernanceAccountType::AccountGovernance, config: governance_config, - proposal_count: 0, + proposals_count: 0, }; self.process_transaction(&[create_account_governance_instruction], None) @@ -503,6 +528,7 @@ impl GovernanceProgramTest { Ok(GovernanceCookie { address: account_governance_address, account, + next_proposal_index: 0, }) } @@ -586,7 +612,7 @@ impl GovernanceProgramTest { self.with_program_governance_instruction( realm_cookie, governed_program_cookie, - |_| {}, + NopOverride, None, ) .await @@ -603,10 +629,10 @@ impl GovernanceProgramTest { let config = GovernanceConfig { realm: realm_cookie.address, governed_account: governed_program_cookie.address, - vote_threshold_percentage: 60, min_tokens_to_create_proposal: 5, min_instruction_hold_up_time: 10, max_voting_time: 100, + vote_threshold_percentage: 60, }; let mut create_program_governance_instruction = create_program_governance( @@ -627,7 +653,7 @@ impl GovernanceProgramTest { let account = Governance { account_type: GovernanceAccountType::ProgramGovernance, config, - proposal_count: 0, + proposals_count: 0, }; let program_governance_address = @@ -636,12 +662,178 @@ impl GovernanceProgramTest { Ok(GovernanceCookie { address: program_governance_address, account, + next_proposal_index: 0, + }) + } + + #[allow(dead_code)] + pub async fn with_proposal( + &mut self, + token_owner_record_cookie: &TokeOwnerRecordCookie, + governance_cookie: &mut GovernanceCookie, + ) -> Result { + self.with_proposal_instruction(token_owner_record_cookie, governance_cookie, |_| {}) + .await + } + + #[allow(dead_code)] + pub async fn with_proposal_instruction( + &mut self, + token_owner_record_cookie: &TokeOwnerRecordCookie, + governance_cookie: &mut GovernanceCookie, + instruction_override: F, + ) -> Result { + let proposal_index = governance_cookie.next_proposal_index; + governance_cookie.next_proposal_index = governance_cookie.next_proposal_index + 1; + + let name = format!("Proposal #{}", proposal_index); + + let description_link = "Proposal Description".to_string(); + + let governance_authority = token_owner_record_cookie.get_governance_authority(); + + let mut create_proposal_instruction = create_proposal( + &governance_cookie.address, + &token_owner_record_cookie.token_owner.pubkey(), + &governance_authority.pubkey(), + &self.payer.pubkey(), + &governance_cookie.account.config.realm, + name.clone(), + description_link.clone(), + &token_owner_record_cookie.governing_token_mint, + proposal_index, + ); + + instruction_override(&mut create_proposal_instruction); + + self.process_transaction( + &[create_proposal_instruction], + Some(&[&governance_authority]), + ) + .await?; + + let account = Proposal { + account_type: GovernanceAccountType::Proposal, + description_link, + name: name.clone(), + governance: governance_cookie.address, + governing_token_mint: token_owner_record_cookie.governing_token_mint, + state: ProposalState::Draft, + signatories_count: 0, + // Clock always returns 1 when running under the test + draft_at: 1, + signing_off_at: None, + voting_at: None, + voting_completed_at: None, + executing_at: None, + closed_at: None, + number_of_executed_instructions: 0, + number_of_instructions: 0, + token_owner_record: token_owner_record_cookie.address, + signatories_signed_off_count: 0, + }; + + let proposal_address = get_proposal_address( + &governance_cookie.address, + &token_owner_record_cookie.governing_token_mint, + &proposal_index.to_le_bytes(), + ); + + Ok(ProposalCookie { + address: proposal_address, + account, + proposal_owner: governance_authority.pubkey(), }) } #[allow(dead_code)] - pub async fn get_voter_record_account(&mut self, address: &Pubkey) -> VoterRecord { - self.get_borsh_account::(address).await + pub async fn with_signatory( + &mut self, + proposal_cookie: &ProposalCookie, + token_owner_record_cookie: &TokeOwnerRecordCookie, + ) -> Result { + let signatory = Keypair::new(); + + let add_signatory_instruction = add_signatory( + &proposal_cookie.address, + &token_owner_record_cookie.address, + &token_owner_record_cookie.token_owner.pubkey(), + &self.payer.pubkey(), + &signatory.pubkey(), + ); + + self.process_transaction( + &[add_signatory_instruction], + Some(&[&token_owner_record_cookie.token_owner]), + ) + .await?; + + let signatory_record_address = + get_signatory_record_address(&proposal_cookie.address, &signatory.pubkey()); + + let signatory_record_data = SignatoryRecord { + account_type: GovernanceAccountType::SignatoryRecord, + proposal: proposal_cookie.address, + signatory: signatory.pubkey(), + signed_off: false, + }; + + let signatory_record_cookie = SignatoryRecordCookie { + address: signatory_record_address, + account: signatory_record_data, + signatory: signatory, + }; + + Ok(signatory_record_cookie) + } + + #[allow(dead_code)] + pub async fn remove_signatory( + &mut self, + proposal_cookie: &ProposalCookie, + token_owner_record_cookie: &TokeOwnerRecordCookie, + signatory_record_cookie: &SignatoryRecordCookie, + ) -> Result<(), ProgramError> { + let remove_signatory_instruction = remove_signatory( + &proposal_cookie.address, + &token_owner_record_cookie.address, + &token_owner_record_cookie.token_owner.pubkey(), + &signatory_record_cookie.account.signatory, + &token_owner_record_cookie.token_owner.pubkey(), + ); + + self.process_transaction( + &[remove_signatory_instruction], + Some(&[&token_owner_record_cookie.token_owner]), + ) + .await?; + + Ok(()) + } + + #[allow(dead_code)] + pub async fn sign_off_proposal( + &mut self, + proposal_cookie: &ProposalCookie, + signatory_record_cookie: &SignatoryRecordCookie, + ) -> Result<(), ProgramError> { + let sign_off_proposal_instruction = sign_off_proposal( + &proposal_cookie.address, + &signatory_record_cookie.signatory.pubkey(), + ); + + self.process_transaction( + &[sign_off_proposal_instruction], + Some(&[&signatory_record_cookie.signatory]), + ) + .await?; + + Ok(()) + } + + #[allow(dead_code)] + pub async fn get_token_owner_record_account(&mut self, address: &Pubkey) -> TokenOwnerRecord { + self.get_borsh_account::(address).await } #[allow(dead_code)] @@ -659,6 +851,20 @@ impl GovernanceProgramTest { .await } + #[allow(dead_code)] + pub async fn get_proposal_account(&mut self, proposal_address: &Pubkey) -> Proposal { + self.get_borsh_account::(proposal_address).await + } + + #[allow(dead_code)] + pub async fn get_signatory_record_account( + &mut self, + proposal_address: &Pubkey, + ) -> SignatoryRecord { + self.get_borsh_account::(proposal_address) + .await + } + #[allow(dead_code)] async fn get_packed_account(&mut self, address: &Pubkey) -> T { self.banks_client @@ -695,7 +901,7 @@ impl GovernanceProgramTest { .await .unwrap() .map(|a| try_from_slice_unchecked(&a.data).unwrap()) - .expect(format!("GET-TEST-ACCOUNT-ERROR: Account {}", address).as_str()) + .expect(format!("GET-TEST-ACCOUNT-ERROR: Account {} not found", address).as_str()) } #[allow(dead_code)] @@ -729,54 +935,6 @@ impl GovernanceProgramTest { .unwrap(); } - #[allow(dead_code)] - pub async fn create_token_account( - &mut self, - token_account_keypair: &Keypair, - token_mint: &Pubkey, - token_mint_authority: &Keypair, - amount: u64, - owner: &Pubkey, - ) { - let create_account_instruction = system_instruction::create_account( - &self.payer.pubkey(), - &token_account_keypair.pubkey(), - self.rent - .minimum_balance(spl_token::state::Account::get_packed_len()), - spl_token::state::Account::get_packed_len() as u64, - &spl_token::id(), - ); - - let initialize_account_instruction = spl_token::instruction::initialize_account( - &spl_token::id(), - &token_account_keypair.pubkey(), - token_mint, - &owner, - ) - .unwrap(); - - let mint_instruction = spl_token::instruction::mint_to( - &spl_token::id(), - token_mint, - &token_account_keypair.pubkey(), - &token_mint_authority.pubkey(), - &[], - amount, - ) - .unwrap(); - - self.process_transaction( - &[ - create_account_instruction, - initialize_account_instruction, - mint_instruction, - ], - Some(&[&token_account_keypair, &token_mint_authority]), - ) - .await - .unwrap(); - } - #[allow(dead_code)] pub async fn create_token_account_with_transfer_authority( &mut self, diff --git a/governance/program/tests/program_test/tools.rs b/governance/program/tests/program_test/tools.rs index b9044cb964f..c1fe0d0d638 100644 --- a/governance/program/tests/program_test/tools.rs +++ b/governance/program/tests/program_test/tools.rs @@ -1,7 +1,7 @@ use std::convert::TryFrom; use solana_program::{instruction::InstructionError, program_error::ProgramError}; -use solana_sdk::{transaction::TransactionError, transport::TransportError}; +use solana_sdk::{signature::Keypair, transaction::TransactionError, transport::TransportError}; /// TODO: Add to SDK /// Instruction errors not mapped in the sdk @@ -35,3 +35,11 @@ pub fn map_transaction_error(transport_error: TransportError) -> ProgramError { _ => panic!("TEST-TRANSPORT-ERROR: {:?}", transport_error), } } + +pub fn clone_keypair(source: &Keypair) -> Keypair { + Keypair::from_bytes(&source.to_bytes()).unwrap() +} + +/// NOP (No Operation) Override function +#[allow(non_snake_case)] +pub fn NopOverride(_: &mut T) {}