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

Commit ef72e56

Browse files
Governance: Use max_voting_time to determine proposal final state (#2529)
* feat: do not allow withdrawing votes before proposal is finalised * feat: do not allow proposal cancellation before proposal is finalised * chore: update comments and add test Co-authored-by: Jon Cinque <[email protected]>
1 parent 4ab60e6 commit ef72e56

File tree

7 files changed

+237
-24
lines changed

7 files changed

+237
-24
lines changed

governance/program/src/instruction.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ pub enum GovernanceInstruction {
253253
/// 1. `[writable]` TokenOwnerRecord account of the Proposal owner
254254
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
255255
/// 3. `[]` Clock sysvar
256+
/// 4. `[]` Governance account
256257
CancelProposal,
257258

258259
/// Signs off Proposal indicating the Signatory approves the Proposal
@@ -1064,12 +1065,14 @@ pub fn cancel_proposal(
10641065
proposal: &Pubkey,
10651066
proposal_owner_record: &Pubkey,
10661067
governance_authority: &Pubkey,
1068+
governance: &Pubkey,
10671069
) -> Instruction {
10681070
let accounts = vec![
10691071
AccountMeta::new(*proposal, false),
10701072
AccountMeta::new(*proposal_owner_record, false),
10711073
AccountMeta::new_readonly(*governance_authority, true),
10721074
AccountMeta::new_readonly(sysvar::clock::id(), false),
1075+
AccountMeta::new_readonly(*governance, false),
10731076
];
10741077

10751078
let instruction = GovernanceInstruction::CancelProposal {};

governance/program/src/processor/process_cancel_proposal.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use solana_program::{
1010
};
1111

1212
use crate::state::{
13-
enums::ProposalState, proposal::get_proposal_data,
13+
enums::ProposalState, governance::get_governance_data,
14+
proposal::get_proposal_data_for_governance,
1415
token_owner_record::get_token_owner_record_data_for_proposal_owner,
1516
};
1617

@@ -25,8 +26,13 @@ pub fn process_cancel_proposal(program_id: &Pubkey, accounts: &[AccountInfo]) ->
2526
let clock_info = next_account_info(account_info_iter)?; // 3
2627
let clock = Clock::from_account_info(clock_info)?;
2728

28-
let mut proposal_data = get_proposal_data(program_id, proposal_info)?;
29-
proposal_data.assert_can_cancel()?;
29+
let governance_info = next_account_info(account_info_iter)?; // 4
30+
31+
let governance_data = get_governance_data(program_id, governance_info)?;
32+
33+
let mut proposal_data =
34+
get_proposal_data_for_governance(program_id, proposal_info, governance_info.key)?;
35+
proposal_data.assert_can_cancel(&governance_data.config, clock.unix_timestamp)?;
3036

3137
let mut proposal_owner_record_data = get_token_owner_record_data_for_proposal_owner(
3238
program_id,

governance/program/src/processor/process_relinquish_vote.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
33
use solana_program::{
44
account_info::{next_account_info, AccountInfo},
5+
clock::Clock,
56
entrypoint::ProgramResult,
67
pubkey::Pubkey,
8+
sysvar::Sysvar,
79
};
810
use spl_governance_tools::account::dispose_account;
911

@@ -52,8 +54,14 @@ pub fn process_relinquish_vote(program_id: &Pubkey, accounts: &[AccountInfo]) ->
5254
)?;
5355
vote_record_data.assert_can_relinquish_vote()?;
5456

55-
// If the Proposal is still being voted on then the token owner vote won't count towards the outcome
56-
if proposal_data.state == ProposalState::Voting {
57+
let clock = Clock::get()?;
58+
59+
// If the Proposal is still being voted on then the token owner vote will be withdrawn and it won't count towards the vote outcome
60+
// Note: If there is no tipping point the proposal can be still in Voting state but already past the configured max_voting_time
61+
// It means it awaits manual finalization (FinalizeVote) and it should no longer be possible to withdraw the vote and we only release the tokens
62+
if proposal_data.state == ProposalState::Voting
63+
&& !proposal_data.has_vote_time_ended(&governance_data.config, clock.unix_timestamp)
64+
{
5765
let governance_authority_info = next_account_info(account_info_iter)?; // 5
5866
let beneficiary_info = next_account_info(account_info_iter)?; // 6
5967

governance/program/src/state/proposal.rs

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -168,19 +168,27 @@ impl Proposal {
168168
.map_err(|_| GovernanceError::InvalidStateCannotVote)?;
169169

170170
// Check if we are still within the configured max_voting_time period
171-
if self
172-
.voting_at
173-
.unwrap()
174-
.checked_add(config.max_voting_time as i64)
175-
.unwrap()
176-
< current_unix_timestamp
177-
{
171+
if self.has_vote_time_ended(config, current_unix_timestamp) {
178172
return Err(GovernanceError::ProposalVotingTimeExpired.into());
179173
}
180174

181175
Ok(())
182176
}
183177

178+
/// Checks whether the voting time has ended for the proposal
179+
pub fn has_vote_time_ended(
180+
&self,
181+
config: &GovernanceConfig,
182+
current_unix_timestamp: UnixTimestamp,
183+
) -> bool {
184+
// Check if we passed vote_end_time determined by the configured max_voting_time period
185+
self.voting_at
186+
.unwrap()
187+
.checked_add(config.max_voting_time as i64)
188+
.unwrap()
189+
< current_unix_timestamp
190+
}
191+
184192
/// Checks if Proposal can be finalized
185193
pub fn assert_can_finalize_vote(
186194
&self,
@@ -190,14 +198,8 @@ impl Proposal {
190198
self.assert_is_voting_state()
191199
.map_err(|_| GovernanceError::InvalidStateCannotFinalize)?;
192200

193-
// Check if we passed the configured max_voting_time period yet
194-
if self
195-
.voting_at
196-
.unwrap()
197-
.checked_add(config.max_voting_time as i64)
198-
.unwrap()
199-
>= current_unix_timestamp
200-
{
201+
// We can only finalize the vote after the configured max_voting_time has expired and vote time ended
202+
if !self.has_vote_time_ended(config, current_unix_timestamp) {
201203
return Err(GovernanceError::CannotFinalizeVotingInProgress.into());
202204
}
203205

@@ -344,9 +346,21 @@ impl Proposal {
344346
}
345347

346348
/// Checks if Proposal can be canceled in the given state
347-
pub fn assert_can_cancel(&self) -> Result<(), ProgramError> {
349+
pub fn assert_can_cancel(
350+
&self,
351+
config: &GovernanceConfig,
352+
current_unix_timestamp: UnixTimestamp,
353+
) -> Result<(), ProgramError> {
348354
match self.state {
349-
ProposalState::Draft | ProposalState::SigningOff | ProposalState::Voting => Ok(()),
355+
ProposalState::Draft | ProposalState::SigningOff => Ok(()),
356+
ProposalState::Voting => {
357+
// Note: If there is no tipping point the proposal can be still in Voting state but already past the configured max_voting_time
358+
// In that case we treat the proposal as finalized and it's no longer allowed to be canceled
359+
if self.has_vote_time_ended(config, current_unix_timestamp) {
360+
return Err(GovernanceError::ProposalVotingTimeExpired.into());
361+
}
362+
Ok(())
363+
}
350364
ProposalState::Executing
351365
| ProposalState::ExecutingWithErrors
352366
| ProposalState::Completed
@@ -700,9 +714,15 @@ mod test {
700714
#[test]
701715
fn test_assert_can_cancel(state in cancellable_states()) {
702716

717+
// Arrange
703718
let mut proposal = create_test_proposal();
719+
let governance_config = create_test_governance_config();
720+
721+
// Act
704722
proposal.state = state;
705-
proposal.assert_can_cancel().unwrap();
723+
724+
// Assert
725+
proposal.assert_can_cancel(&governance_config,1).unwrap();
706726

707727
}
708728

@@ -726,8 +746,10 @@ mod test {
726746
let mut proposal = create_test_proposal();
727747
proposal.state = state;
728748

749+
let governance_config = create_test_governance_config();
750+
729751
// Act
730-
let err = proposal.assert_can_cancel().err().unwrap();
752+
let err = proposal.assert_can_cancel(&governance_config,1).err().unwrap();
731753

732754
// Assert
733755
assert_eq!(err, GovernanceError::InvalidStateCannotCancelProposal.into());

governance/program/tests/process_cancel_proposal.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,97 @@ async fn test_cancel_proposal_with_owner_or_delegate_must_sign_error() {
152152
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
153153
);
154154
}
155+
156+
#[tokio::test]
157+
async fn test_cancel_proposal_with_vote_time_expired_error() {
158+
// Arrange
159+
let mut governance_test = GovernanceProgramTest::start_new().await;
160+
161+
let realm_cookie = governance_test.with_realm().await;
162+
let governed_account_cookie = governance_test.with_governed_account().await;
163+
164+
let token_owner_record_cookie = governance_test
165+
.with_community_token_deposit(&realm_cookie)
166+
.await
167+
.unwrap();
168+
169+
let mut account_governance_cookie = governance_test
170+
.with_account_governance(
171+
&realm_cookie,
172+
&governed_account_cookie,
173+
&token_owner_record_cookie,
174+
)
175+
.await
176+
.unwrap();
177+
178+
let clock = governance_test.bench.get_clock().await;
179+
180+
let proposal_cookie = governance_test
181+
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
182+
.await
183+
.unwrap();
184+
185+
// Advance timestamp past max_voting_time
186+
governance_test
187+
.advance_clock_past_timestamp(
188+
account_governance_cookie.account.config.max_voting_time as i64 + clock.unix_timestamp,
189+
)
190+
.await;
191+
192+
// Act
193+
194+
let err = governance_test
195+
.cancel_proposal(&proposal_cookie, &token_owner_record_cookie)
196+
.await
197+
.err()
198+
.unwrap();
199+
200+
// Assert
201+
202+
assert_eq!(err, GovernanceError::ProposalVotingTimeExpired.into());
203+
}
204+
205+
#[tokio::test]
206+
async fn test_cancel_proposal_in_voting_state() {
207+
// Arrange
208+
let mut governance_test = GovernanceProgramTest::start_new().await;
209+
210+
let realm_cookie = governance_test.with_realm().await;
211+
let governed_account_cookie = governance_test.with_governed_account().await;
212+
213+
let token_owner_record_cookie = governance_test
214+
.with_community_token_deposit(&realm_cookie)
215+
.await
216+
.unwrap();
217+
218+
let mut account_governance_cookie = governance_test
219+
.with_account_governance(
220+
&realm_cookie,
221+
&governed_account_cookie,
222+
&token_owner_record_cookie,
223+
)
224+
.await
225+
.unwrap();
226+
227+
let proposal_cookie = governance_test
228+
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
229+
.await
230+
.unwrap();
231+
232+
governance_test.advance_clock().await;
233+
234+
// Act
235+
236+
governance_test
237+
.cancel_proposal(&proposal_cookie, &token_owner_record_cookie)
238+
.await
239+
.unwrap();
240+
241+
// Assert
242+
243+
let proposal_account = governance_test
244+
.get_proposal_account(&proposal_cookie.address)
245+
.await;
246+
247+
assert_eq!(ProposalState::Cancelled, proposal_account.state);
248+
}

governance/program/tests/process_relinquish_vote.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,3 +439,81 @@ async fn test_relinquish_vote_with_already_relinquished_error() {
439439

440440
assert_eq!(err, GovernanceError::VoteAlreadyRelinquished.into());
441441
}
442+
443+
#[tokio::test]
444+
async fn test_relinquish_proposal_in_voting_state_after_vote_time_ended() {
445+
// Arrange
446+
let mut governance_test = GovernanceProgramTest::start_new().await;
447+
448+
let realm_cookie = governance_test.with_realm().await;
449+
let governed_account_cookie = governance_test.with_governed_account().await;
450+
451+
// Deposit 100 tokens
452+
let token_owner_record_cookie = governance_test
453+
.with_community_token_deposit(&realm_cookie)
454+
.await
455+
.unwrap();
456+
457+
// Add 200 tokens (total 300) to prevent the vote being tipped
458+
governance_test
459+
.mint_community_tokens(&realm_cookie, 200)
460+
.await;
461+
462+
let mut account_governance_cookie = governance_test
463+
.with_account_governance(
464+
&realm_cookie,
465+
&governed_account_cookie,
466+
&token_owner_record_cookie,
467+
)
468+
.await
469+
.unwrap();
470+
471+
let proposal_cookie = governance_test
472+
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
473+
.await
474+
.unwrap();
475+
476+
let clock = governance_test.bench.get_clock().await;
477+
478+
let mut vote_record_cookie = governance_test
479+
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
480+
.await
481+
.unwrap();
482+
483+
// Advance timestamp past max_voting_time
484+
governance_test
485+
.advance_clock_past_timestamp(
486+
account_governance_cookie.account.config.max_voting_time as i64 + clock.unix_timestamp,
487+
)
488+
.await;
489+
490+
// Act
491+
governance_test
492+
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
493+
.await
494+
.unwrap();
495+
496+
// Assert
497+
498+
let proposal_account = governance_test
499+
.get_proposal_account(&proposal_cookie.address)
500+
.await;
501+
502+
// Proposal should be still in voting state but the vote count should not change
503+
assert_eq!(100, proposal_account.yes_votes_count);
504+
assert_eq!(ProposalState::Voting, proposal_account.state);
505+
506+
let token_owner_record = governance_test
507+
.get_token_owner_record_account(&token_owner_record_cookie.address)
508+
.await;
509+
510+
assert_eq!(0, token_owner_record.unrelinquished_votes_count);
511+
assert_eq!(1, token_owner_record.total_votes_count);
512+
513+
let vote_record_account = governance_test
514+
.get_vote_record_account(&vote_record_cookie.address)
515+
.await;
516+
517+
vote_record_cookie.account.is_relinquished = true;
518+
assert_eq!(vote_record_cookie.account, vote_record_account);
519+
}

governance/program/tests/program_test/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ impl GovernanceProgramTest {
354354
}
355355
}
356356

357+
// Creates TokenOwner which owns 100 community tokens and deposits them into the given Realm
357358
#[allow(dead_code)]
358359
pub async fn with_community_token_deposit(
359360
&mut self,
@@ -1668,6 +1669,7 @@ impl GovernanceProgramTest {
16681669
&proposal_cookie.address,
16691670
&token_owner_record_cookie.address,
16701671
&token_owner_record_cookie.token_owner.pubkey(),
1672+
&proposal_cookie.account.governance,
16711673
);
16721674

16731675
self.bench

0 commit comments

Comments
 (0)