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

Commit 5b5e5be

Browse files
Governance: FlagInstructionError instruction (#2099)
* feat: implement FlagInstructionError * fix: ensure instruction can still be executed after being flagged with error * chore: review cleanup * chore: bump version * chore: code review updates Co-authored-by: Jon Cinque <[email protected]> Co-authored-by: Jon Cinque <[email protected]>
1 parent 2a92fac commit 5b5e5be

File tree

12 files changed

+590
-12
lines changed

12 files changed

+590
-12
lines changed

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.1"
3+
version = "1.0.3"
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,10 @@ pub enum GovernanceError {
275275
/// Governance PDA must sign
276276
#[error("Governance PDA must sign")]
277277
GovernancePdaMustSign,
278+
279+
/// Instruction already flagged with error
280+
#[error("Instruction already flagged with error")]
281+
InstructionAlreadyFlaggedWithError,
278282
}
279283

280284
impl PrintProgramError for GovernanceError {

governance/program/src/instruction.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,18 @@ pub enum GovernanceInstruction {
347347
/// New governance config
348348
config: GovernanceConfig,
349349
},
350+
351+
/// Flags an instruction and its parent Proposal with error status
352+
/// It can be used by Proposal owner in case the instruction is permanently broken and can't be executed
353+
/// Note: This instruction is a workaround because currently it's not possible to catch errors from CPI calls
354+
/// and the Governance program has no way to know when instruction failed and flag it automatically
355+
///
356+
/// 0. `[writable]` Proposal account
357+
/// 1. `[]` TokenOwnerRecord account for Proposal owner
358+
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
359+
/// 3. `[writable]` ProposalInstruction account to flag
360+
/// 4. `[]` Clock sysvar
361+
FlagInstructionError,
350362
}
351363

352364
/// Creates CreateRealm instruction
@@ -1031,3 +1043,29 @@ pub fn set_governance_config(
10311043
data: instruction.try_to_vec().unwrap(),
10321044
}
10331045
}
1046+
1047+
/// Creates FlagInstructionError instruction
1048+
pub fn flag_instruction_error(
1049+
program_id: &Pubkey,
1050+
// Accounts
1051+
proposal: &Pubkey,
1052+
token_owner_record: &Pubkey,
1053+
governance_authority: &Pubkey,
1054+
proposal_instruction: &Pubkey,
1055+
) -> Instruction {
1056+
let accounts = vec![
1057+
AccountMeta::new(*proposal, false),
1058+
AccountMeta::new_readonly(*token_owner_record, false),
1059+
AccountMeta::new_readonly(*governance_authority, true),
1060+
AccountMeta::new(*proposal_instruction, false),
1061+
AccountMeta::new_readonly(sysvar::clock::id(), false),
1062+
];
1063+
1064+
let instruction = GovernanceInstruction::FlagInstructionError {};
1065+
1066+
Instruction {
1067+
program_id: *program_id,
1068+
accounts,
1069+
data: instruction.try_to_vec().unwrap(),
1070+
}
1071+
}

governance/program/src/processor/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ mod process_create_token_governance;
1212
mod process_deposit_governing_tokens;
1313
mod process_execute_instruction;
1414
mod process_finalize_vote;
15+
mod process_flag_instruction_error;
1516
mod process_insert_instruction;
1617
mod process_relinquish_vote;
1718
mod process_remove_instruction;
@@ -36,6 +37,7 @@ use process_create_token_governance::*;
3637
use process_deposit_governing_tokens::*;
3738
use process_execute_instruction::*;
3839
use process_finalize_vote::*;
40+
use process_flag_instruction_error::*;
3941
use process_insert_instruction::*;
4042
use process_relinquish_vote::*;
4143
use process_remove_instruction::*;
@@ -160,5 +162,9 @@ pub fn process_instruction(
160162
GovernanceInstruction::SetGovernanceConfig { config } => {
161163
process_set_governance_config(program_id, accounts, config)
162164
}
165+
166+
GovernanceInstruction::FlagInstructionError {} => {
167+
process_flag_instruction_error(program_id, accounts)
168+
}
163169
}
164170
}

governance/program/src/processor/process_execute_instruction.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ pub fn process_execute_instruction(program_id: &Pubkey, accounts: &[AccountInfo]
7070
.checked_add(1)
7171
.unwrap();
7272

73-
if proposal_data.state == ProposalState::Executing
73+
// Checking for Executing and ExecutingWithErrors states because instruction can still be executed after being flagged with error
74+
// The check for instructions_executed_count ensures Proposal can't be transitioned to Completed state from ExecutingWithErrors
75+
if (proposal_data.state == ProposalState::Executing
76+
|| proposal_data.state == ProposalState::ExecutingWithErrors)
7477
&& proposal_data.instructions_executed_count == proposal_data.instructions_count
7578
{
7679
proposal_data.closed_at = Some(clock.unix_timestamp);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//! Program state processor
2+
3+
use borsh::BorshSerialize;
4+
use solana_program::{
5+
account_info::{next_account_info, AccountInfo},
6+
clock::Clock,
7+
entrypoint::ProgramResult,
8+
pubkey::Pubkey,
9+
sysvar::Sysvar,
10+
};
11+
12+
use crate::state::{
13+
enums::{InstructionExecutionStatus, ProposalState},
14+
proposal::get_proposal_data,
15+
proposal_instruction::get_proposal_instruction_data_for_proposal,
16+
token_owner_record::get_token_owner_record_data_for_proposal_owner,
17+
};
18+
19+
/// Processes FlagInstructionError instruction
20+
pub fn process_flag_instruction_error(
21+
program_id: &Pubkey,
22+
accounts: &[AccountInfo],
23+
) -> ProgramResult {
24+
let account_info_iter = &mut accounts.iter();
25+
26+
let proposal_info = next_account_info(account_info_iter)?; // 0
27+
let token_owner_record_info = next_account_info(account_info_iter)?; // 1
28+
let governance_authority_info = next_account_info(account_info_iter)?; // 2
29+
30+
let proposal_instruction_info = next_account_info(account_info_iter)?; // 3
31+
32+
let clock_info = next_account_info(account_info_iter)?; // 4
33+
let clock = Clock::from_account_info(clock_info)?;
34+
35+
let mut proposal_data = get_proposal_data(program_id, proposal_info)?;
36+
37+
let mut proposal_instruction_data = get_proposal_instruction_data_for_proposal(
38+
program_id,
39+
proposal_instruction_info,
40+
proposal_info.key,
41+
)?;
42+
43+
proposal_data
44+
.assert_can_flag_instruction_error(&proposal_instruction_data, clock.unix_timestamp)?;
45+
46+
let token_owner_record_data = get_token_owner_record_data_for_proposal_owner(
47+
program_id,
48+
token_owner_record_info,
49+
&proposal_data.token_owner_record,
50+
)?;
51+
52+
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
53+
54+
// If this is the first instruction to be executed then set executing_at timestamp
55+
// It indicates when we started executing instructions for the Proposal and the fact we only flag it as error is irrelevant here
56+
if proposal_data.state == ProposalState::Succeeded {
57+
proposal_data.executing_at = Some(clock.unix_timestamp);
58+
}
59+
60+
proposal_data.state = ProposalState::ExecutingWithErrors;
61+
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
62+
63+
proposal_instruction_data.execution_status = InstructionExecutionStatus::Error;
64+
proposal_instruction_data.serialize(&mut *proposal_instruction_info.data.borrow_mut())?;
65+
66+
Ok(())
67+
}

governance/program/src/state/enums.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ pub enum ProposalState {
7474
/// Voting ended with success
7575
Succeeded,
7676

77-
/// Voting completed and now instructions are being execute. Proposal enter this state when first instruction is executed and leaves when the last instruction is executed
77+
/// Voting on Proposal succeeded and now instructions are being executed
78+
/// Proposal enter this state when first instruction is executed and leaves when the last instruction is executed
7879
Executing,
7980

8081
/// Completed
@@ -85,6 +86,10 @@ pub enum ProposalState {
8586

8687
/// Defeated
8788
Defeated,
89+
90+
/// Same as Executing but indicates some instructions failed to execute
91+
/// Proposal can't be transitioned from ExecutingWithErrors to Completed state
92+
ExecutingWithErrors,
8893
}
8994

9095
impl Default for ProposalState {
@@ -133,8 +138,6 @@ pub enum InstructionExecutionStatus {
133138
Success,
134139

135140
/// Instruction execution failed
136-
/// Note: Error status is not supported yet because when CPI call fails it always terminates parent instruction
137-
/// We either have to make it possible to change that behavior or add an instruction to manually set the status
138141
Error,
139142
}
140143

governance/program/src/state/proposal.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use crate::{
1010
error::GovernanceError,
1111
state::{
1212
enums::{
13-
GovernanceAccountType, InstructionExecutionFlags, ProposalState,
14-
VoteThresholdPercentage,
13+
GovernanceAccountType, InstructionExecutionFlags, InstructionExecutionStatus,
14+
ProposalState, VoteThresholdPercentage,
1515
},
1616
governance::GovernanceConfig,
1717
proposal_instruction::ProposalInstruction,
@@ -119,6 +119,7 @@ impl Proposal {
119119
match self.state {
120120
ProposalState::Draft | ProposalState::SigningOff => Ok(()),
121121
ProposalState::Executing
122+
| ProposalState::ExecutingWithErrors
122123
| ProposalState::Completed
123124
| ProposalState::Cancelled
124125
| ProposalState::Voting
@@ -279,6 +280,7 @@ impl Proposal {
279280
match self.state {
280281
ProposalState::Draft | ProposalState::SigningOff | ProposalState::Voting => Ok(()),
281282
ProposalState::Executing
283+
| ProposalState::ExecutingWithErrors
282284
| ProposalState::Completed
283285
| ProposalState::Cancelled
284286
| ProposalState::Succeeded
@@ -301,7 +303,9 @@ impl Proposal {
301303
current_unix_timestamp: UnixTimestamp,
302304
) -> Result<(), ProgramError> {
303305
match self.state {
304-
ProposalState::Succeeded | ProposalState::Executing => {}
306+
ProposalState::Succeeded
307+
| ProposalState::Executing
308+
| ProposalState::ExecutingWithErrors => {}
305309
ProposalState::Draft
306310
| ProposalState::SigningOff
307311
| ProposalState::Completed
@@ -328,6 +332,22 @@ impl Proposal {
328332

329333
Ok(())
330334
}
335+
336+
/// Checks if the instruction can be flagged with error for the Proposal in the given state
337+
pub fn assert_can_flag_instruction_error(
338+
&self,
339+
proposal_instruction_data: &ProposalInstruction,
340+
current_unix_timestamp: UnixTimestamp,
341+
) -> Result<(), ProgramError> {
342+
// Instruction can be flagged for error only when it's eligible for execution
343+
self.assert_can_execute_instruction(proposal_instruction_data, current_unix_timestamp)?;
344+
345+
if proposal_instruction_data.execution_status == InstructionExecutionStatus::Error {
346+
return Err(GovernanceError::InstructionAlreadyFlaggedWithError.into());
347+
}
348+
349+
Ok(())
350+
}
331351
}
332352

333353
/// Converts threshold in percentages to actual vote count
@@ -511,6 +531,7 @@ mod test {
511531
Just(ProposalState::Voting),
512532
Just(ProposalState::Succeeded),
513533
Just(ProposalState::Executing),
534+
Just(ProposalState::ExecutingWithErrors),
514535
Just(ProposalState::Completed),
515536
Just(ProposalState::Cancelled),
516537
Just(ProposalState::Defeated),
@@ -551,6 +572,7 @@ mod test {
551572
Just(ProposalState::Voting),
552573
Just(ProposalState::Succeeded),
553574
Just(ProposalState::Executing),
575+
Just(ProposalState::ExecutingWithErrors),
554576
Just(ProposalState::Completed),
555577
Just(ProposalState::Cancelled),
556578
Just(ProposalState::Defeated),
@@ -596,6 +618,7 @@ mod test {
596618
prop_oneof![
597619
Just(ProposalState::Succeeded),
598620
Just(ProposalState::Executing),
621+
Just(ProposalState::ExecutingWithErrors),
599622
Just(ProposalState::Completed),
600623
Just(ProposalState::Cancelled),
601624
Just(ProposalState::Defeated),

governance/program/src/state/proposal_instruction.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ pub struct ProposalInstruction {
101101
pub executed_at: Option<UnixTimestamp>,
102102

103103
/// Instruction execution status
104-
/// Note: The field is not used in the current version
105104
pub execution_status: InstructionExecutionStatus,
106105
}
107106

0 commit comments

Comments
 (0)