Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 11ba3fb

Browse files
authored
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
1 parent 631eafb commit 11ba3fb

21 files changed

+351
-70
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ hfuzz_workspace
1010
**/*.so
1111
**/.DS_Store
1212
test-ledger
13+
docker-target

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

governance/program/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "spl-governance"
3-
version = "1.0.9"
3+
version = "1.1.0"
44
description = "Solana Program Library Governance Program"
55
authors = ["Solana Maintainers <[email protected]>"]
66
repository = "https://github.com/solana-labs/solana-program-library"

governance/program/src/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,14 @@ pub enum GovernanceError {
315315
/// Owner doesn't have enough governing tokens to create Governance
316316
#[error("Owner doesn't have enough governing tokens to create Governance")]
317317
NotEnoughTokensToCreateGovernance,
318+
319+
/// Too many outstanding proposals
320+
#[error("Too many outstanding proposals")]
321+
TooManyOutstandingProposals,
322+
323+
/// All proposals must be finalized to withdraw governing tokens
324+
#[error("All proposals must be finalized to withdraw governing tokens")]
325+
AllProposalsMustBeFinalisedToWithdrawGoverningTokens,
318326
}
319327

320328
impl PrintProgramError for GovernanceError {

governance/program/src/instruction.rs

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ pub enum GovernanceInstruction {
149149
/// 0. `[]` Realm account the created Proposal belongs to
150150
/// 1. `[writable]` Proposal account. PDA seeds ['governance',governance, governing_token_mint, proposal_index]
151151
/// 2. `[writable]` Governance account
152-
/// 3. `[]` TokenOwnerRecord account for Proposal owner
152+
/// 3. `[writable]` TokenOwnerRecord account of the Proposal owner
153153
/// 4. `[signer]` Governance Authority (Token Owner or Governance Delegate)
154154
/// 5. `[signer]` Payer
155155
/// 6. `[]` System program
@@ -172,7 +172,7 @@ pub enum GovernanceInstruction {
172172
/// Adds a signatory to the Proposal which means this Proposal can't leave Draft state until yet another Signatory signs
173173
///
174174
/// 0. `[writable]` Proposal account
175-
/// 1. `[]` TokenOwnerRecord account for Proposal owner
175+
/// 1. `[]` TokenOwnerRecord account of the Proposal owner
176176
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
177177
/// 3. `[writable]` Signatory Record Account
178178
/// 4. `[signer]` Payer
@@ -187,7 +187,7 @@ pub enum GovernanceInstruction {
187187
/// Removes a Signatory from the Proposal
188188
///
189189
/// 0. `[writable]` Proposal account
190-
/// 1. `[]` TokenOwnerRecord account for Proposal owner
190+
/// 1. `[]` TokenOwnerRecord account of the Proposal owner
191191
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
192192
/// 3. `[writable]` Signatory Record Account
193193
/// 4. `[writable]` Beneficiary Account which would receive lamports from the disposed Signatory Record Account
@@ -203,7 +203,7 @@ pub enum GovernanceInstruction {
203203
204204
/// 0. `[]` Governance account
205205
/// 1. `[writable]` Proposal account
206-
/// 2. `[]` TokenOwnerRecord account for Proposal owner
206+
/// 2. `[]` TokenOwnerRecord account of the Proposal owner
207207
/// 3. `[signer]` Governance Authority (Token Owner or Governance Delegate)
208208
/// 4. `[writable]` ProposalInstruction account. PDA seeds: ['governance',proposal,index]
209209
/// 5. `[signer]` Payer
@@ -225,7 +225,7 @@ pub enum GovernanceInstruction {
225225
/// Removes instruction from the Proposal
226226
///
227227
/// 0. `[writable]` Proposal account
228-
/// 1. `[]` TokenOwnerRecord account for Proposal owner
228+
/// 1. `[]` TokenOwnerRecord account of the Proposal owner
229229
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
230230
/// 3. `[writable]` ProposalInstruction account
231231
/// 4. `[writable]` Beneficiary Account which would receive lamports from the disposed ProposalInstruction account
@@ -234,7 +234,7 @@ pub enum GovernanceInstruction {
234234
/// Cancels Proposal by changing its state to Canceled
235235
///
236236
/// 0. `[writable]` Proposal account
237-
/// 1. `[]` TokenOwnerRecord account for Proposal owner
237+
/// 1. `[writable]` TokenOwnerRecord account of the Proposal owner
238238
/// 2 `[signer]` Governance Authority (Token Owner or Governance Delegate)
239239
/// 3. `[]` Clock sysvar
240240
CancelProposal,
@@ -255,7 +255,8 @@ pub enum GovernanceInstruction {
255255
/// 0. `[]` Realm account
256256
/// 1. `[]` Governance account
257257
/// 2. `[writable]` Proposal account
258-
/// 3. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
258+
/// 4. `[writable]` TokenOwnerRecord of the Proposal owner
259+
/// 3. `[writable]` TokenOwnerRecord of the voter. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
259260
/// 4. `[signer]` Governance Authority (Token Owner or Governance Delegate)
260261
/// 5. `[writable]` Proposal VoteRecord account. PDA seeds: ['governance',proposal,governing_token_owner_record]
261262
/// 6. `[]` Governing Token Mint
@@ -274,8 +275,9 @@ pub enum GovernanceInstruction {
274275
/// 0. `[]` Realm account
275276
/// 1. `[]` Governance account
276277
/// 2. `[writable]` Proposal account
277-
/// 3. `[]` Governing Token Mint
278-
/// 4. `[]` Clock sysvar
278+
/// 3. `[writable]` TokenOwnerRecord of the Proposal owner
279+
/// 4. `[]` Governing Token Mint
280+
/// 5. `[]` Clock sysvar
279281
FinalizeVote {},
280282

281283
/// Relinquish Vote removes voter weight from a Proposal and removes it from voter's active votes
@@ -368,7 +370,7 @@ pub enum GovernanceInstruction {
368370
/// and the Governance program has no way to know when instruction failed and flag it automatically
369371
///
370372
/// 0. `[writable]` Proposal account
371-
/// 1. `[]` TokenOwnerRecord account for Proposal owner
373+
/// 1. `[]` TokenOwnerRecord account of the Proposal owner
372374
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
373375
/// 3. `[writable]` ProposalInstruction account to flag
374376
/// 4. `[]` Clock sysvar
@@ -735,7 +737,7 @@ pub fn create_proposal(
735737
program_id: &Pubkey,
736738
// Accounts
737739
governance: &Pubkey,
738-
governing_token_owner_record: &Pubkey,
740+
proposal_owner_record: &Pubkey,
739741
governance_authority: &Pubkey,
740742
payer: &Pubkey,
741743
// Args
@@ -756,7 +758,7 @@ pub fn create_proposal(
756758
AccountMeta::new_readonly(*realm, false),
757759
AccountMeta::new(proposal_address, false),
758760
AccountMeta::new(*governance, false),
759-
AccountMeta::new_readonly(*governing_token_owner_record, false),
761+
AccountMeta::new(*proposal_owner_record, false),
760762
AccountMeta::new_readonly(*governance_authority, true),
761763
AccountMeta::new_readonly(*payer, true),
762764
AccountMeta::new_readonly(system_program::id(), false),
@@ -875,20 +877,23 @@ pub fn cast_vote(
875877
realm: &Pubkey,
876878
governance: &Pubkey,
877879
proposal: &Pubkey,
878-
token_owner_record: &Pubkey,
880+
proposal_owner_record: &Pubkey,
881+
voter_token_owner_record: &Pubkey,
879882
governance_authority: &Pubkey,
880883
governing_token_mint: &Pubkey,
881884
payer: &Pubkey,
882885
// Args
883886
vote: Vote,
884887
) -> Instruction {
885-
let vote_record_address = get_vote_record_address(program_id, proposal, token_owner_record);
888+
let vote_record_address =
889+
get_vote_record_address(program_id, proposal, voter_token_owner_record);
886890

887891
let accounts = vec![
888892
AccountMeta::new_readonly(*realm, false),
889893
AccountMeta::new_readonly(*governance, false),
890894
AccountMeta::new(*proposal, false),
891-
AccountMeta::new(*token_owner_record, false),
895+
AccountMeta::new(*proposal_owner_record, false),
896+
AccountMeta::new(*voter_token_owner_record, false),
892897
AccountMeta::new_readonly(*governance_authority, true),
893898
AccountMeta::new(vote_record_address, false),
894899
AccountMeta::new_readonly(*governing_token_mint, false),
@@ -914,12 +919,14 @@ pub fn finalize_vote(
914919
realm: &Pubkey,
915920
governance: &Pubkey,
916921
proposal: &Pubkey,
922+
proposal_owner_record: &Pubkey,
917923
governing_token_mint: &Pubkey,
918924
) -> Instruction {
919925
let accounts = vec![
920926
AccountMeta::new_readonly(*realm, false),
921927
AccountMeta::new_readonly(*governance, false),
922928
AccountMeta::new(*proposal, false),
929+
AccountMeta::new(*proposal_owner_record, false),
923930
AccountMeta::new_readonly(*governing_token_mint, false),
924931
AccountMeta::new_readonly(sysvar::clock::id(), false),
925932
];
@@ -973,12 +980,12 @@ pub fn cancel_proposal(
973980
program_id: &Pubkey,
974981
// Accounts
975982
proposal: &Pubkey,
976-
token_owner_record: &Pubkey,
983+
proposal_owner_record: &Pubkey,
977984
governance_authority: &Pubkey,
978985
) -> Instruction {
979986
let accounts = vec![
980987
AccountMeta::new(*proposal, false),
981-
AccountMeta::new_readonly(*token_owner_record, false),
988+
AccountMeta::new(*proposal_owner_record, false),
982989
AccountMeta::new_readonly(*governance_authority, true),
983990
AccountMeta::new_readonly(sysvar::clock::id(), false),
984991
];

governance/program/src/processor/process_cancel_proposal.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub fn process_cancel_proposal(program_id: &Pubkey, accounts: &[AccountInfo]) ->
1919
let account_info_iter = &mut accounts.iter();
2020

2121
let proposal_info = next_account_info(account_info_iter)?; // 0
22-
let token_owner_record_info = next_account_info(account_info_iter)?; // 1
22+
let proposal_owner_record_info = next_account_info(account_info_iter)?; // 1
2323
let governance_authority_info = next_account_info(account_info_iter)?; // 2
2424

2525
let clock_info = next_account_info(account_info_iter)?; // 3
@@ -28,13 +28,17 @@ pub fn process_cancel_proposal(program_id: &Pubkey, accounts: &[AccountInfo]) ->
2828
let mut proposal_data = get_proposal_data(program_id, proposal_info)?;
2929
proposal_data.assert_can_cancel()?;
3030

31-
let token_owner_record_data = get_token_owner_record_data_for_proposal_owner(
31+
let mut proposal_owner_record_data = get_token_owner_record_data_for_proposal_owner(
3232
program_id,
33-
token_owner_record_info,
33+
proposal_owner_record_info,
3434
&proposal_data.token_owner_record,
3535
)?;
3636

37-
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
37+
proposal_owner_record_data
38+
.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
39+
40+
proposal_owner_record_data.decrease_outstanding_proposal_count();
41+
proposal_owner_record_data.serialize(&mut *proposal_owner_record_info.data.borrow_mut())?;
3842

3943
proposal_data.state = ProposalState::Cancelled;
4044
proposal_data.closed_at = Some(clock.unix_timestamp);

governance/program/src/processor/process_cast_vote.rs

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ use crate::{
1717
governance::get_governance_data_for_realm,
1818
proposal::get_proposal_data_for_governance_and_governing_mint,
1919
realm::get_realm_data_for_governing_token_mint,
20-
token_owner_record::get_token_owner_record_data_for_realm_and_governing_mint,
20+
token_owner_record::{
21+
get_token_owner_record_data_for_proposal_owner,
22+
get_token_owner_record_data_for_realm_and_governing_mint,
23+
},
2124
vote_record::{get_vote_record_address_seeds, VoteRecord},
2225
},
2326
tools::{account::create_and_serialize_account_signed, spl_token::get_spl_token_mint_supply},
@@ -35,20 +38,23 @@ pub fn process_cast_vote(
3538

3639
let realm_info = next_account_info(account_info_iter)?; // 0
3740
let governance_info = next_account_info(account_info_iter)?; // 1
41+
3842
let proposal_info = next_account_info(account_info_iter)?; // 2
39-
let token_owner_record_info = next_account_info(account_info_iter)?; // 3
40-
let governance_authority_info = next_account_info(account_info_iter)?; // 4
43+
let proposal_owner_record_info = next_account_info(account_info_iter)?; // 3
44+
45+
let voter_token_owner_record_info = next_account_info(account_info_iter)?; // 4
46+
let governance_authority_info = next_account_info(account_info_iter)?; // 5
4147

42-
let vote_record_info = next_account_info(account_info_iter)?; // 5
43-
let governing_token_mint_info = next_account_info(account_info_iter)?; // 6
48+
let vote_record_info = next_account_info(account_info_iter)?; // 6
49+
let governing_token_mint_info = next_account_info(account_info_iter)?; // 7
4450

45-
let payer_info = next_account_info(account_info_iter)?; // 7
46-
let system_info = next_account_info(account_info_iter)?; // 8
51+
let payer_info = next_account_info(account_info_iter)?; // 8
52+
let system_info = next_account_info(account_info_iter)?; // 9
4753

48-
let rent_sysvar_info = next_account_info(account_info_iter)?; // 9
54+
let rent_sysvar_info = next_account_info(account_info_iter)?; // 10
4955
let rent = &Rent::from_account_info(rent_sysvar_info)?;
5056

51-
let clock_info = next_account_info(account_info_iter)?; // 10
57+
let clock_info = next_account_info(account_info_iter)?; // 11
5258
let clock = Clock::from_account_info(clock_info)?;
5359

5460
if !vote_record_info.data_is_empty() {
@@ -71,28 +77,28 @@ pub fn process_cast_vote(
7177
)?;
7278
proposal_data.assert_can_cast_vote(&governance_data.config, clock.unix_timestamp)?;
7379

74-
let mut token_owner_record_data = get_token_owner_record_data_for_realm_and_governing_mint(
75-
program_id,
76-
token_owner_record_info,
77-
&governance_data.realm,
78-
governing_token_mint_info.key,
79-
)?;
80-
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
80+
let mut voter_token_owner_record_data =
81+
get_token_owner_record_data_for_realm_and_governing_mint(
82+
program_id,
83+
voter_token_owner_record_info,
84+
&governance_data.realm,
85+
governing_token_mint_info.key,
86+
)?;
87+
voter_token_owner_record_data
88+
.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
8189

8290
// Update TokenOwnerRecord vote counts
83-
token_owner_record_data.unrelinquished_votes_count = token_owner_record_data
91+
voter_token_owner_record_data.unrelinquished_votes_count = voter_token_owner_record_data
8492
.unrelinquished_votes_count
8593
.checked_add(1)
8694
.unwrap();
8795

88-
token_owner_record_data.total_votes_count = token_owner_record_data
96+
voter_token_owner_record_data.total_votes_count = voter_token_owner_record_data
8997
.total_votes_count
9098
.checked_add(1)
9199
.unwrap();
92100

93-
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
94-
95-
let vote_amount = token_owner_record_data.governing_token_deposit_amount;
101+
let vote_amount = voter_token_owner_record_data.governing_token_deposit_amount;
96102

97103
// Calculate Proposal voting weights
98104
let vote_weight = match vote {
@@ -113,20 +119,36 @@ pub fn process_cast_vote(
113119
};
114120

115121
let governing_token_mint_supply = get_spl_token_mint_supply(governing_token_mint_info)?;
116-
proposal_data.try_tip_vote(
122+
if proposal_data.try_tip_vote(
117123
governing_token_mint_supply,
118124
&governance_data.config,
119125
&realm_data,
120126
clock.unix_timestamp,
121-
)?;
127+
)? {
128+
if proposal_owner_record_info.key == voter_token_owner_record_info.key {
129+
voter_token_owner_record_data.decrease_outstanding_proposal_count();
130+
} else {
131+
let mut proposal_owner_record_data = get_token_owner_record_data_for_proposal_owner(
132+
program_id,
133+
proposal_owner_record_info,
134+
&proposal_data.token_owner_record,
135+
)?;
136+
proposal_owner_record_data.decrease_outstanding_proposal_count();
137+
proposal_owner_record_data
138+
.serialize(&mut *proposal_owner_record_info.data.borrow_mut())?;
139+
};
140+
}
141+
142+
voter_token_owner_record_data
143+
.serialize(&mut *voter_token_owner_record_info.data.borrow_mut())?;
122144

123145
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
124146

125147
// Create and serialize VoteRecord
126148
let vote_record_data = VoteRecord {
127149
account_type: GovernanceAccountType::VoteRecord,
128150
proposal: *proposal_info.key,
129-
governing_token_owner: token_owner_record_data.governing_token_owner,
151+
governing_token_owner: voter_token_owner_record_data.governing_token_owner,
130152
vote_weight,
131153
is_relinquished: false,
132154
};
@@ -135,7 +157,7 @@ pub fn process_cast_vote(
135157
payer_info,
136158
vote_record_info,
137159
&vote_record_data,
138-
&get_vote_record_address_seeds(proposal_info.key, token_owner_record_info.key),
160+
&get_vote_record_address_seeds(proposal_info.key, voter_token_owner_record_info.key),
139161
program_id,
140162
system_info,
141163
rent,

0 commit comments

Comments
 (0)