Skip to main content
Sigvex

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:

  1. 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).
  2. 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.
  3. Second pass – Governance operation detection: Identifies StoreAccountData statements 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.
  4. Category filtering: The detector is categorized as GovernanceOnly and 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.

References