SPL Governance Attack
Detects missing safeguards in governance implementations that enable proposal manipulation and flash governance attacks.
SPL Governance Attack
Overview
Remediation Guide: How to Fix SPL Governance Attack
The SPL governance attack detector identifies Solana programs with governance operations that lack critical safeguards: timelock enforcement, quorum validation, and voting power verification. Without these protections, an attacker can execute governance proposals immediately after creation (flash governance), manipulate voting outcomes with insufficient stake, or bypass the community review period entirely.
The detector targets functions with governance-related semantics and checks for the presence of timelock comparisons, quorum threshold checks, voting power validations, and PDA derivations that indicate governance-aware design.
Why This Is an Issue
Governance mechanisms protect protocol treasuries, parameter changes, and upgrade authority. When governance safeguards are missing:
- Flash governance: An attacker creates a proposal and executes it in the same block, before the community can review or vote against it. This has been used to drain DAO treasuries.
- Quorum bypass: An attacker executes a proposal that received insufficient votes, passing changes that the community did not approve.
- Voting power manipulation: An attacker temporarily acquires voting power (via flash loans or token borrowing), votes on a proposal, and returns the tokens in the same transaction.
- Timelock bypass: Critical changes (parameter updates, treasury withdrawals, upgrade authority transfers) execute without the mandatory delay period.
CWE mapping: CWE-284 (Improper Access Control), CWE-799 (Improper Control of Interaction Frequency).
How to Resolve
// Before: Vulnerable -- governance proposal executed without safeguards
pub fn execute_proposal(accounts: &[AccountInfo]) -> ProgramResult {
let proposal = &accounts[0];
let treasury = &accounts[1];
// No timelock check, no quorum check
let mut data = proposal.try_borrow_mut_data()?;
let mut state = Proposal::try_from_slice(&data[8..])?;
state.status = ProposalStatus::Executed;
// Execute treasury withdrawal directly
**treasury.try_borrow_mut_lamports()? -= state.amount;
**accounts[2].try_borrow_mut_lamports()? += state.amount;
state.serialize(&mut &mut data[8..])?;
Ok(())
}
// After: Enforce timelock and quorum before execution
pub fn execute_proposal(accounts: &[AccountInfo]) -> ProgramResult {
let proposal = &accounts[0];
let treasury = &accounts[1];
let clock = Clock::get()?;
let data = proposal.try_borrow_data()?;
let state = Proposal::try_from_slice(&data[8..])?;
// FIXED: enforce timelock
if clock.unix_timestamp < state.created_at + TIMELOCK_SECONDS {
return Err(ProgramError::InvalidArgument);
}
// FIXED: verify quorum was met
if state.votes_for + state.votes_against < QUORUM_THRESHOLD {
return Err(ProgramError::InvalidArgument);
}
// FIXED: verify proposal passed
if state.votes_for <= state.votes_against {
return Err(ProgramError::InvalidArgument);
}
drop(data);
let mut data = proposal.try_borrow_mut_data()?;
let mut state = Proposal::try_from_slice(&data[8..])?;
state.status = ProposalStatus::Executed;
**treasury.try_borrow_mut_lamports()? -= state.amount;
**accounts[2].try_borrow_mut_lamports()? += state.amount;
state.serialize(&mut &mut data[8..])?;
Ok(())
}
Examples
Vulnerable Code
pub fn cast_vote(accounts: &[AccountInfo], vote: bool) -> ProgramResult {
let proposal = &accounts[0];
let voter = &accounts[1];
// No check that voter has voting power
// No check that voting period is active
let mut data = proposal.try_borrow_mut_data()?;
let mut state = Proposal::try_from_slice(&data[8..])?;
if vote {
state.votes_for += 1; // No voting weight check
} else {
state.votes_against += 1;
}
state.serialize(&mut &mut data[8..])?;
Ok(())
}
Fixed Code
pub fn cast_vote(accounts: &[AccountInfo], vote: bool) -> ProgramResult {
let proposal = &accounts[0];
let voter = &accounts[1];
let token_account = &accounts[2];
let clock = Clock::get()?;
// FIXED: verify voter is signer
if !voter.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let data = proposal.try_borrow_data()?;
let state = Proposal::try_from_slice(&data[8..])?;
// FIXED: verify voting period is active
if clock.unix_timestamp < state.voting_start
|| clock.unix_timestamp > state.voting_end
{
return Err(ProgramError::InvalidArgument);
}
// FIXED: verify voting power from token balance
let token_data = token_account.try_borrow_data()?;
let token_state = spl_token::state::Account::unpack(&token_data)?;
if token_state.owner != *voter.key {
return Err(ProgramError::InvalidAccountData);
}
let voting_weight = token_state.amount;
drop(data);
let mut data = proposal.try_borrow_mut_data()?;
let mut state = Proposal::try_from_slice(&data[8..])?;
if vote {
state.votes_for += voting_weight;
} else {
state.votes_against += voting_weight;
}
state.serialize(&mut &mut data[8..])?;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "spl-governance-attack",
"severity": "critical",
"confidence": 0.75,
"description": "Proposal execution detected without timelock validation. Governance proposals must enforce a timelock delay between approval and execution to allow community review and emergency response. Missing timelocks enable flash governance attacks where malicious proposals are executed immediately.",
"location": { "function": "execute_proposal", "offset": 3 }
}
Detection Methodology
The detector performs a two-pass analysis:
- First pass — Validation scan: Scans the entire function for governance-related validation patterns: timelock comparisons (timestamp checks against proposal creation time), quorum threshold checks (vote count comparisons), voting power validation (token balance reads), and PDA derivations (indicating governance-aware account management).
- Awareness check: If any governance validation is present (including PDA derivation), the developer is considered governance-aware and no findings are emitted. This reduces false positives from programs that implement partial governance with external validation.
- Second pass — Governance operation detection: Identifies
StoreAccountDatastatements targeting governance-related accounts (proposal, vote, treasury). When governance state is modified without any validation patterns in the function, findings are emitted for each missing safeguard. - Category filtering: The detector is categorized as
GovernanceOnlyand is only run on programs identified as governance-related by the detection context.
Limitations
False positives:
- Programs where governance validation is split across multiple instructions (create proposal, vote, execute as separate transactions).
- Programs using an external governance framework where validation occurs in the framework’s code.
False negatives:
- Governance implementations using non-standard naming conventions may not be detected.
- Programs where timelock enforcement happens in the frontend or off-chain rather than on-chain.
Related Detectors
- Missing Signer Check — missing signer validation on governance operations
- Conditional Validation Bypass — governance checks bypassed through conditional paths
- Vault Manipulation — unauthorized treasury operations