Governance: Constrain active proposals (#2268)

* wip: add unresolved_proposal_count to Proposal

* chore: update create_proposal test

* chore: bump version

* feat: decrease unresolved proposal count for Cancel ix

* chore: rename unresolved to outstanding

* feat: decrease outstanding proposal count for CastVote ix

* feat: decrease outstanding proposal count for FinalizeVote ix

* feat: Prevent withdrawals with outstanding proposals

* chore: fix unit test

* chore: make clippy happy

* chore: update instructions comments

* chore: temp. exclude tests with slots wrapping
This commit is contained in:
Sebastian Bor 2021-08-12 03:05:03 +02:00 committed by GitHub
parent 631eafbfb4
commit 11ba3fb824
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 351 additions and 70 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ hfuzz_workspace
**/*.so
**/.DS_Store
test-ledger
docker-target

2
Cargo.lock generated
View File

@ -3770,7 +3770,7 @@ dependencies = [
[[package]]
name = "spl-governance"
version = "1.0.9"
version = "1.1.0"
dependencies = [
"arrayref",
"assert_matches",

View File

@ -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 <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana-program-library"

View File

@ -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 {

View File

@ -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),
];

View File

@ -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);

View File

@ -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,

View File

@ -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,

View File

@ -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(

View File

@ -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(())
}

View File

@ -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,

View File

@ -292,7 +292,7 @@ impl Proposal {
config: &GovernanceConfig,
realm_data: &Realm,
current_unix_timestamp: UnixTimestamp,
) -> Result<(), ProgramError> {
) -> Result<bool, ProgramError> {
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

View File

@ -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::<TokenOwnerRecord>();
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<Pubkey>,
}
#[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
);
}
}

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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();

View File

@ -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]

View File

@ -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();

View File

@ -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
);
}

View File

@ -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,