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:
parent
631eafbfb4
commit
11ba3fb824
|
@ -10,3 +10,4 @@ hfuzz_workspace
|
|||
**/*.so
|
||||
**/.DS_Store
|
||||
test-ledger
|
||||
docker-target
|
||||
|
|
|
@ -3770,7 +3770,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "spl-governance"
|
||||
version = "1.0.9"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"assert_matches",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
];
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue