diff --git a/.gitignore b/.gitignore index 7a244d9b..90bc25d3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ hfuzz_workspace **/*.so **/.DS_Store test-ledger +docker-target diff --git a/Cargo.lock b/Cargo.lock index 7862ceee..a36cc293 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3770,7 +3770,7 @@ dependencies = [ [[package]] name = "spl-governance" -version = "1.0.9" +version = "1.1.0" dependencies = [ "arrayref", "assert_matches", diff --git a/governance/program/Cargo.toml b/governance/program/Cargo.toml index 7c3185f0..58646160 100644 --- a/governance/program/Cargo.toml +++ b/governance/program/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spl-governance" -version = "1.0.9" +version = "1.1.0" description = "Solana Program Library Governance Program" authors = ["Solana Maintainers "] repository = "https://github.com/solana-labs/solana-program-library" diff --git a/governance/program/src/error.rs b/governance/program/src/error.rs index 18ce4d55..7c9d9a40 100644 --- a/governance/program/src/error.rs +++ b/governance/program/src/error.rs @@ -315,6 +315,14 @@ pub enum GovernanceError { /// Owner doesn't have enough governing tokens to create Governance #[error("Owner doesn't have enough governing tokens to create Governance")] NotEnoughTokensToCreateGovernance, + + /// Too many outstanding proposals + #[error("Too many outstanding proposals")] + TooManyOutstandingProposals, + + /// All proposals must be finalized to withdraw governing tokens + #[error("All proposals must be finalized to withdraw governing tokens")] + AllProposalsMustBeFinalisedToWithdrawGoverningTokens, } impl PrintProgramError for GovernanceError { diff --git a/governance/program/src/instruction.rs b/governance/program/src/instruction.rs index 7e78dc81..08cb5143 100644 --- a/governance/program/src/instruction.rs +++ b/governance/program/src/instruction.rs @@ -149,7 +149,7 @@ pub enum GovernanceInstruction { /// 0. `[]` Realm account the created Proposal belongs to /// 1. `[writable]` Proposal account. PDA seeds ['governance',governance, governing_token_mint, proposal_index] /// 2. `[writable]` Governance account - /// 3. `[]` TokenOwnerRecord account for Proposal owner + /// 3. `[writable]` TokenOwnerRecord account of the Proposal owner /// 4. `[signer]` Governance Authority (Token Owner or Governance Delegate) /// 5. `[signer]` Payer /// 6. `[]` System program @@ -172,7 +172,7 @@ pub enum GovernanceInstruction { /// Adds a signatory to the Proposal which means this Proposal can't leave Draft state until yet another Signatory signs /// /// 0. `[writable]` Proposal account - /// 1. `[]` TokenOwnerRecord account for Proposal owner + /// 1. `[]` TokenOwnerRecord account of the Proposal owner /// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate) /// 3. `[writable]` Signatory Record Account /// 4. `[signer]` Payer @@ -187,7 +187,7 @@ pub enum GovernanceInstruction { /// Removes a Signatory from the Proposal /// /// 0. `[writable]` Proposal account - /// 1. `[]` TokenOwnerRecord account for Proposal owner + /// 1. `[]` TokenOwnerRecord account of the Proposal owner /// 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 @@ -203,7 +203,7 @@ pub enum GovernanceInstruction { /// 0. `[]` Governance account /// 1. `[writable]` Proposal account - /// 2. `[]` TokenOwnerRecord account for Proposal owner + /// 2. `[]` TokenOwnerRecord account of the Proposal owner /// 3. `[signer]` Governance Authority (Token Owner or Governance Delegate) /// 4. `[writable]` ProposalInstruction account. PDA seeds: ['governance',proposal,index] /// 5. `[signer]` Payer @@ -225,7 +225,7 @@ pub enum GovernanceInstruction { /// Removes instruction from the Proposal /// /// 0. `[writable]` Proposal account - /// 1. `[]` TokenOwnerRecord account for Proposal owner + /// 1. `[]` TokenOwnerRecord account of the Proposal owner /// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate) /// 3. `[writable]` ProposalInstruction account /// 4. `[writable]` Beneficiary Account which would receive lamports from the disposed ProposalInstruction account @@ -234,7 +234,7 @@ pub enum GovernanceInstruction { /// Cancels Proposal by changing its state to Canceled /// /// 0. `[writable]` Proposal account - /// 1. `[]` TokenOwnerRecord account for Proposal owner + /// 1. `[writable]` TokenOwnerRecord account of the Proposal owner /// 2 `[signer]` Governance Authority (Token Owner or Governance Delegate) /// 3. `[]` Clock sysvar CancelProposal, @@ -255,7 +255,8 @@ pub enum GovernanceInstruction { /// 0. `[]` Realm account /// 1. `[]` Governance account /// 2. `[writable]` Proposal account - /// 3. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] + /// 4. `[writable]` TokenOwnerRecord of the Proposal owner + /// 3. `[writable]` TokenOwnerRecord of the voter. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] /// 4. `[signer]` Governance Authority (Token Owner or Governance Delegate) /// 5. `[writable]` Proposal VoteRecord account. PDA seeds: ['governance',proposal,governing_token_owner_record] /// 6. `[]` Governing Token Mint @@ -274,8 +275,9 @@ pub enum GovernanceInstruction { /// 0. `[]` Realm account /// 1. `[]` Governance account /// 2. `[writable]` Proposal account - /// 3. `[]` Governing Token Mint - /// 4. `[]` Clock sysvar + /// 3. `[writable]` TokenOwnerRecord of the Proposal owner + /// 4. `[]` Governing Token Mint + /// 5. `[]` Clock sysvar FinalizeVote {}, /// Relinquish Vote removes voter weight from a Proposal and removes it from voter's active votes @@ -368,7 +370,7 @@ pub enum GovernanceInstruction { /// and the Governance program has no way to know when instruction failed and flag it automatically /// /// 0. `[writable]` Proposal account - /// 1. `[]` TokenOwnerRecord account for Proposal owner + /// 1. `[]` TokenOwnerRecord account of the Proposal owner /// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate) /// 3. `[writable]` ProposalInstruction account to flag /// 4. `[]` Clock sysvar @@ -735,7 +737,7 @@ pub fn create_proposal( program_id: &Pubkey, // Accounts governance: &Pubkey, - governing_token_owner_record: &Pubkey, + proposal_owner_record: &Pubkey, governance_authority: &Pubkey, payer: &Pubkey, // Args @@ -756,7 +758,7 @@ pub fn create_proposal( AccountMeta::new_readonly(*realm, false), AccountMeta::new(proposal_address, false), AccountMeta::new(*governance, false), - AccountMeta::new_readonly(*governing_token_owner_record, false), + AccountMeta::new(*proposal_owner_record, false), AccountMeta::new_readonly(*governance_authority, true), AccountMeta::new_readonly(*payer, true), AccountMeta::new_readonly(system_program::id(), false), @@ -875,20 +877,23 @@ pub fn cast_vote( realm: &Pubkey, governance: &Pubkey, proposal: &Pubkey, - token_owner_record: &Pubkey, + proposal_owner_record: &Pubkey, + voter_token_owner_record: &Pubkey, governance_authority: &Pubkey, governing_token_mint: &Pubkey, payer: &Pubkey, // Args vote: Vote, ) -> Instruction { - let vote_record_address = get_vote_record_address(program_id, proposal, token_owner_record); + let vote_record_address = + get_vote_record_address(program_id, proposal, voter_token_owner_record); let accounts = vec![ AccountMeta::new_readonly(*realm, false), AccountMeta::new_readonly(*governance, false), AccountMeta::new(*proposal, false), - AccountMeta::new(*token_owner_record, false), + AccountMeta::new(*proposal_owner_record, false), + AccountMeta::new(*voter_token_owner_record, false), AccountMeta::new_readonly(*governance_authority, true), AccountMeta::new(vote_record_address, false), AccountMeta::new_readonly(*governing_token_mint, false), @@ -914,12 +919,14 @@ pub fn finalize_vote( realm: &Pubkey, governance: &Pubkey, proposal: &Pubkey, + proposal_owner_record: &Pubkey, governing_token_mint: &Pubkey, ) -> Instruction { let accounts = vec![ AccountMeta::new_readonly(*realm, false), AccountMeta::new_readonly(*governance, false), AccountMeta::new(*proposal, false), + AccountMeta::new(*proposal_owner_record, false), AccountMeta::new_readonly(*governing_token_mint, false), AccountMeta::new_readonly(sysvar::clock::id(), false), ]; @@ -973,12 +980,12 @@ pub fn cancel_proposal( program_id: &Pubkey, // Accounts proposal: &Pubkey, - token_owner_record: &Pubkey, + proposal_owner_record: &Pubkey, governance_authority: &Pubkey, ) -> Instruction { let accounts = vec![ AccountMeta::new(*proposal, false), - AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new(*proposal_owner_record, false), AccountMeta::new_readonly(*governance_authority, true), AccountMeta::new_readonly(sysvar::clock::id(), false), ]; diff --git a/governance/program/src/processor/process_cancel_proposal.rs b/governance/program/src/processor/process_cancel_proposal.rs index f4aca9ed..77926513 100644 --- a/governance/program/src/processor/process_cancel_proposal.rs +++ b/governance/program/src/processor/process_cancel_proposal.rs @@ -19,7 +19,7 @@ pub fn process_cancel_proposal(program_id: &Pubkey, accounts: &[AccountInfo]) -> 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 proposal_owner_record_info = next_account_info(account_info_iter)?; // 1 let governance_authority_info = next_account_info(account_info_iter)?; // 2 let clock_info = next_account_info(account_info_iter)?; // 3 @@ -28,13 +28,17 @@ pub fn process_cancel_proposal(program_id: &Pubkey, accounts: &[AccountInfo]) -> let mut proposal_data = get_proposal_data(program_id, proposal_info)?; proposal_data.assert_can_cancel()?; - let token_owner_record_data = get_token_owner_record_data_for_proposal_owner( + let mut proposal_owner_record_data = get_token_owner_record_data_for_proposal_owner( program_id, - token_owner_record_info, + proposal_owner_record_info, &proposal_data.token_owner_record, )?; - token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + proposal_owner_record_data + .assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + + proposal_owner_record_data.decrease_outstanding_proposal_count(); + proposal_owner_record_data.serialize(&mut *proposal_owner_record_info.data.borrow_mut())?; proposal_data.state = ProposalState::Cancelled; proposal_data.closed_at = Some(clock.unix_timestamp); diff --git a/governance/program/src/processor/process_cast_vote.rs b/governance/program/src/processor/process_cast_vote.rs index c1691f50..36eb81af 100644 --- a/governance/program/src/processor/process_cast_vote.rs +++ b/governance/program/src/processor/process_cast_vote.rs @@ -17,7 +17,10 @@ use crate::{ governance::get_governance_data_for_realm, proposal::get_proposal_data_for_governance_and_governing_mint, realm::get_realm_data_for_governing_token_mint, - token_owner_record::get_token_owner_record_data_for_realm_and_governing_mint, + token_owner_record::{ + get_token_owner_record_data_for_proposal_owner, + get_token_owner_record_data_for_realm_and_governing_mint, + }, vote_record::{get_vote_record_address_seeds, VoteRecord}, }, tools::{account::create_and_serialize_account_signed, spl_token::get_spl_token_mint_supply}, @@ -35,20 +38,23 @@ pub fn process_cast_vote( let realm_info = next_account_info(account_info_iter)?; // 0 let governance_info = next_account_info(account_info_iter)?; // 1 + let proposal_info = next_account_info(account_info_iter)?; // 2 - let token_owner_record_info = next_account_info(account_info_iter)?; // 3 - let governance_authority_info = next_account_info(account_info_iter)?; // 4 + let proposal_owner_record_info = next_account_info(account_info_iter)?; // 3 - let vote_record_info = next_account_info(account_info_iter)?; // 5 - let governing_token_mint_info = next_account_info(account_info_iter)?; // 6 + let voter_token_owner_record_info = next_account_info(account_info_iter)?; // 4 + let governance_authority_info = next_account_info(account_info_iter)?; // 5 - let payer_info = next_account_info(account_info_iter)?; // 7 - let system_info = next_account_info(account_info_iter)?; // 8 + let vote_record_info = next_account_info(account_info_iter)?; // 6 + let governing_token_mint_info = next_account_info(account_info_iter)?; // 7 - let rent_sysvar_info = next_account_info(account_info_iter)?; // 9 + let payer_info = next_account_info(account_info_iter)?; // 8 + let system_info = next_account_info(account_info_iter)?; // 9 + + let rent_sysvar_info = next_account_info(account_info_iter)?; // 10 let rent = &Rent::from_account_info(rent_sysvar_info)?; - let clock_info = next_account_info(account_info_iter)?; // 10 + let clock_info = next_account_info(account_info_iter)?; // 11 let clock = Clock::from_account_info(clock_info)?; if !vote_record_info.data_is_empty() { @@ -71,28 +77,28 @@ pub fn process_cast_vote( )?; proposal_data.assert_can_cast_vote(&governance_data.config, clock.unix_timestamp)?; - let mut token_owner_record_data = get_token_owner_record_data_for_realm_and_governing_mint( - program_id, - token_owner_record_info, - &governance_data.realm, - governing_token_mint_info.key, - )?; - token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + let mut voter_token_owner_record_data = + get_token_owner_record_data_for_realm_and_governing_mint( + program_id, + voter_token_owner_record_info, + &governance_data.realm, + governing_token_mint_info.key, + )?; + voter_token_owner_record_data + .assert_token_owner_or_delegate_is_signer(governance_authority_info)?; // Update TokenOwnerRecord vote counts - token_owner_record_data.unrelinquished_votes_count = token_owner_record_data + voter_token_owner_record_data.unrelinquished_votes_count = voter_token_owner_record_data .unrelinquished_votes_count .checked_add(1) .unwrap(); - token_owner_record_data.total_votes_count = token_owner_record_data + voter_token_owner_record_data.total_votes_count = voter_token_owner_record_data .total_votes_count .checked_add(1) .unwrap(); - token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?; - - let vote_amount = token_owner_record_data.governing_token_deposit_amount; + let vote_amount = voter_token_owner_record_data.governing_token_deposit_amount; // Calculate Proposal voting weights let vote_weight = match vote { @@ -113,12 +119,28 @@ pub fn process_cast_vote( }; let governing_token_mint_supply = get_spl_token_mint_supply(governing_token_mint_info)?; - proposal_data.try_tip_vote( + if proposal_data.try_tip_vote( governing_token_mint_supply, &governance_data.config, &realm_data, clock.unix_timestamp, - )?; + )? { + if proposal_owner_record_info.key == voter_token_owner_record_info.key { + voter_token_owner_record_data.decrease_outstanding_proposal_count(); + } else { + let mut proposal_owner_record_data = get_token_owner_record_data_for_proposal_owner( + program_id, + proposal_owner_record_info, + &proposal_data.token_owner_record, + )?; + proposal_owner_record_data.decrease_outstanding_proposal_count(); + proposal_owner_record_data + .serialize(&mut *proposal_owner_record_info.data.borrow_mut())?; + }; + } + + voter_token_owner_record_data + .serialize(&mut *voter_token_owner_record_info.data.borrow_mut())?; proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?; @@ -126,7 +148,7 @@ pub fn process_cast_vote( let vote_record_data = VoteRecord { account_type: GovernanceAccountType::VoteRecord, proposal: *proposal_info.key, - governing_token_owner: token_owner_record_data.governing_token_owner, + governing_token_owner: voter_token_owner_record_data.governing_token_owner, vote_weight, is_relinquished: false, }; @@ -135,7 +157,7 @@ pub fn process_cast_vote( payer_info, vote_record_info, &vote_record_data, - &get_vote_record_address_seeds(proposal_info.key, token_owner_record_info.key), + &get_vote_record_address_seeds(proposal_info.key, voter_token_owner_record_info.key), program_id, system_info, rent, diff --git a/governance/program/src/processor/process_create_proposal.rs b/governance/program/src/processor/process_create_proposal.rs index 08588683..9a52af58 100644 --- a/governance/program/src/processor/process_create_proposal.rs +++ b/governance/program/src/processor/process_create_proposal.rs @@ -36,7 +36,7 @@ pub fn process_create_proposal( let proposal_info = next_account_info(account_info_iter)?; // 1 let governance_info = next_account_info(account_info_iter)?; // 2 - let token_owner_record_info = next_account_info(account_info_iter)?; // 3 + let proposal_owner_record_info = next_account_info(account_info_iter)?; // 3 let governance_authority_info = next_account_info(account_info_iter)?; // 4 let payer_info = next_account_info(account_info_iter)?; // 5 @@ -58,21 +58,31 @@ pub fn process_create_proposal( let mut governance_data = get_governance_data_for_realm(program_id, governance_info, realm_info.key)?; - let token_owner_record_data = - get_token_owner_record_data_for_realm(program_id, token_owner_record_info, realm_info.key)?; + let mut proposal_owner_record_data = get_token_owner_record_data_for_realm( + program_id, + proposal_owner_record_info, + realm_info.key, + )?; // Proposal owner (TokenOwner) or its governance_delegate must sign this transaction - token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + proposal_owner_record_data + .assert_token_owner_or_delegate_is_signer(governance_authority_info)?; - // Ensure proposal owner (TokenOwner) has enough tokens to create proposal - token_owner_record_data.assert_can_create_proposal(&realm_data, &governance_data.config)?; + // Ensure proposal owner (TokenOwner) has enough tokens to create proposal and no outstanding proposals + proposal_owner_record_data.assert_can_create_proposal(&realm_data, &governance_data.config)?; + + proposal_owner_record_data.outstanding_proposal_count = proposal_owner_record_data + .outstanding_proposal_count + .checked_add(1) + .unwrap(); + proposal_owner_record_data.serialize(&mut *proposal_owner_record_info.data.borrow_mut())?; 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, + token_owner_record: *proposal_owner_record_info.key, signatories_count: 0, signatories_signed_off_count: 0, diff --git a/governance/program/src/processor/process_deposit_governing_tokens.rs b/governance/program/src/processor/process_deposit_governing_tokens.rs index c70875d4..a65c6b9d 100644 --- a/governance/program/src/processor/process_deposit_governing_tokens.rs +++ b/governance/program/src/processor/process_deposit_governing_tokens.rs @@ -92,7 +92,8 @@ pub fn process_deposit_governing_tokens( governance_delegate: None, unrelinquished_votes_count: 0, total_votes_count: 0, - reserved: [0; 8], + outstanding_proposal_count: 0, + reserved: [0; 7], }; create_and_serialize_account_signed( diff --git a/governance/program/src/processor/process_finalize_vote.rs b/governance/program/src/processor/process_finalize_vote.rs index 9dbceb52..6ede6c28 100644 --- a/governance/program/src/processor/process_finalize_vote.rs +++ b/governance/program/src/processor/process_finalize_vote.rs @@ -13,6 +13,7 @@ use crate::{ governance::get_governance_data_for_realm, proposal::get_proposal_data_for_governance_and_governing_mint, realm::get_realm_data_for_governing_token_mint, + token_owner_record::get_token_owner_record_data_for_proposal_owner, }, tools::spl_token::get_spl_token_mint_supply, }; @@ -26,10 +27,11 @@ pub fn process_finalize_vote(program_id: &Pubkey, accounts: &[AccountInfo]) -> P let realm_info = next_account_info(account_info_iter)?; // 0 let governance_info = next_account_info(account_info_iter)?; // 1 let proposal_info = next_account_info(account_info_iter)?; // 2 + let proposal_owner_record_info = next_account_info(account_info_iter)?; // 3 - let governing_token_mint_info = next_account_info(account_info_iter)?; // 3 + let governing_token_mint_info = next_account_info(account_info_iter)?; // 4 - let clock_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 realm_data = get_realm_data_for_governing_token_mint( @@ -58,5 +60,13 @@ pub fn process_finalize_vote(program_id: &Pubkey, accounts: &[AccountInfo]) -> P proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?; + let mut proposal_owner_record_data = get_token_owner_record_data_for_proposal_owner( + program_id, + proposal_owner_record_info, + &proposal_data.token_owner_record, + )?; + proposal_owner_record_data.decrease_outstanding_proposal_count(); + proposal_owner_record_data.serialize(&mut *proposal_owner_record_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 dd4171ee..dbdd6d0e 100644 --- a/governance/program/src/processor/process_withdraw_governing_tokens.rs +++ b/governance/program/src/processor/process_withdraw_governing_tokens.rs @@ -62,6 +62,10 @@ pub fn process_withdraw_governing_tokens( return Err(GovernanceError::AllVotesMustBeRelinquishedToWithdrawGoverningTokens.into()); } + if token_owner_record_data.outstanding_proposal_count > 0 { + return Err(GovernanceError::AllProposalsMustBeFinalisedToWithdrawGoverningTokens.into()); + } + transfer_spl_tokens_signed( governing_token_holding_info, governing_token_destination_info, diff --git a/governance/program/src/state/proposal.rs b/governance/program/src/state/proposal.rs index 44361cba..505e9527 100644 --- a/governance/program/src/state/proposal.rs +++ b/governance/program/src/state/proposal.rs @@ -292,7 +292,7 @@ impl Proposal { config: &GovernanceConfig, realm_data: &Realm, current_unix_timestamp: UnixTimestamp, - ) -> Result<(), ProgramError> { + ) -> Result { let max_vote_weight = self.get_max_vote_weight(realm_data, governing_token_mint_supply)?; if let Some(tipped_state) = self.try_get_tipped_vote_state(max_vote_weight, config) { @@ -302,9 +302,11 @@ impl Proposal { // Capture vote params to correctly display historical results self.max_vote_weight = Some(max_vote_weight); self.vote_threshold_percentage = Some(config.vote_threshold_percentage.clone()); - } - Ok(()) + Ok(true) + } else { + Ok(false) + } } /// Checks if vote can be tipped and automatically transitioned to Succeeded or Defeated state diff --git a/governance/program/src/state/token_owner_record.rs b/governance/program/src/state/token_owner_record.rs index 558ccb5d..5ef3f708 100644 --- a/governance/program/src/state/token_owner_record.rs +++ b/governance/program/src/state/token_owner_record.rs @@ -43,8 +43,14 @@ pub struct TokenOwnerRecord { /// If TokenOwner withdraws vote while voting is still in progress total_votes_count is decreased and the vote doesn't count towards the total pub total_votes_count: u32, + /// The number of outstanding proposals the TokenOwner currently owns + /// The count is increased when TokenOwner creates a proposal + /// and decreased once it's either voted on (Succeeded or Defeated) or Cancelled + /// By default it's restricted to 1 outstanding Proposal per token owner + pub outstanding_proposal_count: u8, + /// Reserved space for future versions - pub reserved: [u8; 8], + pub reserved: [u8; 7], /// A single account that is allowed to operate governance with the deposited governing tokens /// It can be delegated to by the governing_token_owner or current governance_delegate @@ -84,7 +90,7 @@ impl TokenOwnerRecord { Err(GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()) } - /// Asserts TokenOwner has enough tokens to be allowed to create proposal + /// Asserts TokenOwner has enough tokens to be allowed to create proposal and doesn't have any outstanding proposals pub fn assert_can_create_proposal( &self, realm_data: &Realm, @@ -103,6 +109,12 @@ impl TokenOwnerRecord { return Err(GovernanceError::NotEnoughTokensToCreateProposal.into()); } + // The number of outstanding proposals is currently restricted to 1 + // If there is a need to change it in the future then it should be added to realm or governance config + if self.outstanding_proposal_count > 0 { + return Err(GovernanceError::TooManyOutstandingProposals.into()); + } + Ok(()) } @@ -124,6 +136,16 @@ impl TokenOwnerRecord { Ok(()) } + + /// Decreases outstanding_proposal_count + pub fn decrease_outstanding_proposal_count(&mut self) { + // Previous versions didn't use the count and it can be already 0 + // TODO: Remove this check once all outstanding proposals on mainnet are resolved + if self.outstanding_proposal_count != 0 { + self.outstanding_proposal_count = + self.outstanding_proposal_count.checked_sub(1).unwrap(); + } + } } /// Returns TokenOwnerRecord PDA address @@ -225,7 +247,7 @@ pub fn get_token_owner_record_data_for_proposal_owner( #[cfg(test)] mod test { - use solana_program::borsh::get_packed_len; + use solana_program::borsh::{get_packed_len, try_from_slice_unchecked}; use super::*; @@ -240,11 +262,68 @@ mod test { governance_delegate: Some(Pubkey::new_unique()), unrelinquished_votes_count: 1, total_votes_count: 1, - reserved: [0; 8], + outstanding_proposal_count: 1, + reserved: [0; 7], }; let size = get_packed_len::(); assert_eq!(token_owner_record.get_max_size(), Some(size)); } + + #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] + pub struct TokenOwnerRecordV1 { + pub account_type: GovernanceAccountType, + + pub realm: Pubkey, + + pub governing_token_mint: Pubkey, + + pub governing_token_owner: Pubkey, + + pub governing_token_deposit_amount: u64, + + pub unrelinquished_votes_count: u32, + + pub total_votes_count: u32, + + pub reserved: [u8; 8], + + pub governance_delegate: Option, + } + + #[test] + fn test_deserialize_v1_0_account() { + let token_owner_record_v1_0 = TokenOwnerRecordV1 { + 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, + unrelinquished_votes_count: 2, + total_votes_count: 5, + reserved: [0; 8], + governance_delegate: Some(Pubkey::new_unique()), + }; + + let mut token_owner_record_v1_0_data = vec![]; + token_owner_record_v1_0 + .serialize(&mut token_owner_record_v1_0_data) + .unwrap(); + + let token_owner_record_v1_1_data: TokenOwnerRecord = + try_from_slice_unchecked(&token_owner_record_v1_0_data).unwrap(); + + assert_eq!( + token_owner_record_v1_0.account_type, + token_owner_record_v1_1_data.account_type + ); + + assert_eq!(0, token_owner_record_v1_1_data.outstanding_proposal_count); + + assert_eq!( + token_owner_record_v1_0.governance_delegate, + token_owner_record_v1_1_data.governance_delegate + ); + } } diff --git a/governance/program/tests/process_cancel_proposal.rs b/governance/program/tests/process_cancel_proposal.rs index 4ba37ad1..adb91462 100644 --- a/governance/program/tests/process_cancel_proposal.rs +++ b/governance/program/tests/process_cancel_proposal.rs @@ -48,6 +48,12 @@ async fn test_cancel_proposal() { assert_eq!(ProposalState::Cancelled, proposal_account.state); assert_eq!(Some(clock.unix_timestamp), proposal_account.closed_at); + + let token_owner_record_account = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(0, token_owner_record_account.outstanding_proposal_count); } #[tokio::test] diff --git a/governance/program/tests/process_cast_vote.rs b/governance/program/tests/process_cast_vote.rs index c68af782..ca9bb94d 100644 --- a/governance/program/tests/process_cast_vote.rs +++ b/governance/program/tests/process_cast_vote.rs @@ -394,6 +394,12 @@ async fn test_cast_vote_with_vote_tipped_to_succeeded() { .await; assert_eq!(ProposalState::Succeeded, proposal_account.state); + + let proposal_owner_record = governance_test + .get_token_owner_record_account(&proposal_cookie.account.token_owner_record) + .await; + + assert_eq!(0, proposal_owner_record.outstanding_proposal_count); } #[tokio::test] @@ -479,6 +485,12 @@ async fn test_cast_vote_with_vote_tipped_to_defeated() { .await; assert_eq!(ProposalState::Defeated, proposal_account.state); + + let proposal_owner_record = governance_test + .get_token_owner_record_account(&proposal_cookie.account.token_owner_record) + .await; + + assert_eq!(0, proposal_owner_record.outstanding_proposal_count); } #[tokio::test] @@ -530,6 +542,12 @@ async fn test_cast_vote_with_threshold_below_50_and_vote_not_tipped() { .await; assert_eq!(ProposalState::Voting, proposal_account.state); + + let proposal_owner_record = governance_test + .get_token_owner_record_account(&proposal_cookie.account.token_owner_record) + .await; + + assert_eq!(1, proposal_owner_record.outstanding_proposal_count); } #[tokio::test] diff --git a/governance/program/tests/process_create_proposal.rs b/governance/program/tests/process_create_proposal.rs index 3ac5dc6b..a6457504 100644 --- a/governance/program/tests/process_create_proposal.rs +++ b/governance/program/tests/process_create_proposal.rs @@ -48,6 +48,12 @@ async fn test_create_community_proposal() { .await; assert_eq!(1, account_governance_account.proposals_count); + + let token_owner_record_account = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(1, token_owner_record_account.outstanding_proposal_count); } #[tokio::test] diff --git a/governance/program/tests/process_execute_instruction.rs b/governance/program/tests/process_execute_instruction.rs index 92a0da0c..d4c89590 100644 --- a/governance/program/tests/process_execute_instruction.rs +++ b/governance/program/tests/process_execute_instruction.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "test-bpf")] +#![cfg(feature = "test-bpf-all")] mod program_test; @@ -551,8 +551,12 @@ async fn test_execute_instruction_for_other_proposal_error() { .advance_clock_by_min_timespan(proposal_instruction_cookie.account.hold_up_time as u64) .await; + let token_owner_record_cookie2 = governance_test + .with_community_token_deposit(&realm_cookie) + .await; + let proposal_cookie2 = governance_test - .with_proposal(&token_owner_record_cookie, &mut mint_governance_cookie) + .with_proposal(&token_owner_record_cookie2, &mut mint_governance_cookie) .await .unwrap(); diff --git a/governance/program/tests/process_finalize_vote.rs b/governance/program/tests/process_finalize_vote.rs index dc4a0ce4..93799335 100644 --- a/governance/program/tests/process_finalize_vote.rs +++ b/governance/program/tests/process_finalize_vote.rs @@ -99,6 +99,12 @@ async fn test_finalize_vote_to_succeeded() { ), proposal_account.vote_threshold_percentage ); + + let proposal_owner_record = governance_test + .get_token_owner_record_account(&proposal_cookie.account.token_owner_record) + .await; + + assert_eq!(0, proposal_owner_record.outstanding_proposal_count); } #[tokio::test] diff --git a/governance/program/tests/process_remove_instruction.rs b/governance/program/tests/process_remove_instruction.rs index 36299372..59cf3ded 100644 --- a/governance/program/tests/process_remove_instruction.rs +++ b/governance/program/tests/process_remove_instruction.rs @@ -338,13 +338,17 @@ async fn test_remove_instruction_with_instruction_from_other_proposal_error() { .await .unwrap(); + let token_owner_record_cookie2 = governance_test + .with_community_token_deposit(&realm_cookie) + .await; + let mut proposal_cookie2 = governance_test - .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .with_proposal(&token_owner_record_cookie2, &mut account_governance_cookie) .await .unwrap(); let proposal_instruction_cookie2 = governance_test - .with_nop_instruction(&mut proposal_cookie2, &token_owner_record_cookie, None) + .with_nop_instruction(&mut proposal_cookie2, &token_owner_record_cookie2, None) .await .unwrap(); diff --git a/governance/program/tests/process_withdraw_governing_tokens.rs b/governance/program/tests/process_withdraw_governing_tokens.rs index 1b3a7d3f..07a8826e 100644 --- a/governance/program/tests/process_withdraw_governing_tokens.rs +++ b/governance/program/tests/process_withdraw_governing_tokens.rs @@ -318,3 +318,89 @@ async fn test_withdraw_tokens_with_malicious_holding_account_error() { GovernanceError::InvalidGoverningTokenHoldingAccount.into() ); } + +#[tokio::test] +async fn test_withdraw_governing_tokens_with_outstanding_proposals_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await; + let mut account_governance_cookie = governance_test + .with_account_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + // Act + let err = governance_test + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::AllProposalsMustBeFinalisedToWithdrawGoverningTokens.into() + ); +} + +#[tokio::test] +async fn test_withdraw_governing_tokens_after_proposal_cancelled() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await; + let mut account_governance_cookie = governance_test + .with_account_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let proposal_cookie = governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + governance_test + .cancel_proposal(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + // Act + governance_test + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + // Assert + let source_account = governance_test + .get_token_account(&token_owner_record_cookie.token_source) + .await; + + assert_eq!( + token_owner_record_cookie.token_source_amount, + source_account.amount + ); +} diff --git a/governance/program/tests/program_test/mod.rs b/governance/program/tests/program_test/mod.rs index b30beead..8d284a7e 100644 --- a/governance/program/tests/program_test/mod.rs +++ b/governance/program/tests/program_test/mod.rs @@ -465,7 +465,8 @@ impl GovernanceProgramTest { governance_delegate: None, unrelinquished_votes_count: 0, total_votes_count: 0, - reserved: [0; 8], + outstanding_proposal_count: 0, + reserved: [0; 7], }; let governance_delegate = Keypair::from_base58_string(&token_owner.to_base58_string()); @@ -1397,6 +1398,7 @@ impl GovernanceProgramTest { &realm_cookie.address, &proposal_cookie.account.governance, &proposal_cookie.address, + &proposal_cookie.account.token_owner_record, &proposal_cookie.account.governing_token_mint, ); @@ -1482,6 +1484,7 @@ impl GovernanceProgramTest { &token_owner_record_cookie.account.realm, &proposal_cookie.account.governance, &proposal_cookie.address, + &proposal_cookie.account.token_owner_record, &token_owner_record_cookie.address, &token_owner_record_cookie.token_owner.pubkey(), &proposal_cookie.account.governing_token_mint,