Web3 & smart contracts
Partial15 minGovernance Attacks
On-chain governance lets token holders propose and enact changes to a protocol — often including control of its treasury and its own upgradeable code. An attack converts temporary or illegitimate voting power into a permanent, privileged action: borrowing a voting majority for one block, passing a malicious proposal, bypassing a timelock, or exploiting weak quorum, delegation, or proposal-execution checks to seize what the vote was meant to protect.
Classification
- Domain
- Web3 & smart contracts
- ROOTVIEL support
- Partial — Core is built; parts are not yet wired end-to-end.
- Analysis family
- RootRaven
- CWE
- CWE-284, CWE-863, CWE-840
- OWASP
- SC01, SC03
Definition
A governance attack is the subversion of a protocol’s on-chain decision-making so that an adversary causes the system to execute an action the token holders did not legitimately approve. Because governance commonly controls the treasury, the parameters, and the contract code itself (via an upgrade or a privileged executor), a successful governance attack is often total: the attacker does not break one function, they become the protocol.
The attacks cluster around four weaknesses: voting power that can be acquired temporarily (bought, borrowed, or flash-financed) rather than reflecting durable stake; proposal execution that is not properly gated (anyone can execute, or execution does not re-validate state); quorum and delegation accounting that can be gamed; and timelock/queueing controls that can be bypassed or front-run. Each converts a transient or unauthorized condition into an irreversible privileged call.
Context
Governance attacks apply to any system where privileged actions are gated by a token-weighted or role-based vote.
- ▸Token-voting DAOs: proposal → vote → queue → execute pipelines (Governor-style) controlling treasury and parameters.
- ▸Upgradeable protocols where governance owns the proxy admin and can replace implementation code.
- ▸Vote accounting based on current balance or a same-block snapshot, which flash capital can inflate.
- ▸Delegation systems where voting power is assigned separately from token custody.
- ▸Timelocks and multisig executors meant to delay or constrain execution, and the exact moment a proposal becomes executable.
Solidity & EVM fundamentals
A governance system measures voting power at some reference point and acts on a tally. The safe pattern anchors power to a historical snapshot — ERC-20 Votes / ERC-6372 checkpoints read balance "as of" a past block (the proposal’s snapshot block), so tokens acquired after that block carry no weight. The unsafe pattern reads current balance (or a snapshot taken inside the attacking transaction), which lets an attacker acquire power, vote, and return the tokens atomically.
The second fundamental is the execution pipeline. A Governor typically separates propose, castVote, queue (into a Timelock), and execute. Security depends on each stage checking the right thing: that the proposer met the threshold, that the tally met quorum against the snapshot supply, that the timelock delay elapsed, and — critically — that execute re-validates authorization and does not let an arbitrary caller run a proposal that never passed. A missing or mis-ordered check at any stage is the opening.
- Snapshot / checkpoint voting
- Voting power read as of a historical block (ERC-20 Votes, ERC-6372), so power reflects stake held before the proposal, not tokens acquired to vote.
- Quorum
- The minimum participation (as a fraction of snapshot supply) a proposal needs to be valid; too low a quorum makes capture cheap.
- Delegation
- Assigning voting power to an address distinct from the token holder; mis-accounted delegation can double-count or be re-pointed to game a tally.
- Timelock
- A mandatory delay between a proposal passing and its execution, giving the community time to react; bypassing it removes the last safeguard.
- Proposal threshold
- The minimum voting power required to submit a proposal; prevents spam but, if low with current-balance voting, lets flash capital open a proposal.
Root cause
The core root cause is deriving authority from a quantity that is not bound to durable, pre-committed stake — current token balance, a same-block snapshot, or re-pointable delegation — so that power can be acquired and relinquished within the attack itself. This is the governance-specific form of trusting a within-transaction state reading, the same family of mistake as spot-price oracles.
A second root cause is incomplete authorization in the execution pipeline: an execute path that does not verify the proposal actually passed and cleared its timelock, a queue step that can be skipped, or a privileged executor callable by the wrong party. Here the defect is classic access control — a consequential action reachable without the complete chain of checks that was supposed to guard it. Either root alone suffices; together they make capture trivial.
How the attack works, step by step
The archetype combines temporary voting power with a pipeline that honours it: borrow a majority, vote, and ride the proposal through to a treasury-draining execution. Variants substitute a timelock bypass or an unauthorized execute for the voting step.
- 1Acquire power: the attacker flash-borrows or temporarily buys enough governance tokens to exceed the proposal threshold and sway the vote — only possible if power is read from current balance or a same-block snapshot.
- 2Propose / vote: they submit (or vote on) a proposal whose payload grants them value — transferring the treasury, re-pointing the proxy to malicious code, or changing a critical parameter.
- 3Clear the gates: if quorum is low or the timelock can be bypassed/front-run, the proposal reaches an executable state faster than the community can respond.
- 4Execute: the malicious payload runs with the protocol’s own authority — draining funds or seizing upgrade control.
- 5Return power: the borrowed tokens are repaid in the same flow; the attacker keeps the proceeds, having never held durable stake.
Vulnerable example
A synthetic governor that reads voting power from CURRENT token balance and whose execute path neither enforces a timelock nor restricts who may call it. Flash-borrowed tokens can pass a proposal and immediately execute a treasury transfer.
synthetic — do not use; current-balance votes, no timelock, open execute// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IToken { function balanceOf(address) external view returns (uint256); } contract NaiveGovernor { IToken public token; uint256 public quorum = 1000e18; // low quorum struct Proposal { address target; bytes data; uint256 forVotes; bool executed; } mapping(uint256 => Proposal) public proposals; mapping(uint256 => mapping(address => bool)) public voted; function propose(address target, bytes calldata data) external returns (uint256 id) { id = uint256(keccak256(abi.encode(target, data, block.number))); proposals[id] = Proposal(target, data, 0, false); } // UNSAFE: voting weight = CURRENT balance, so flash-borrowed tokens vote. function castVote(uint256 id) external { require(!voted[id][msg.sender], "voted"); voted[id][msg.sender] = true; proposals[id].forVotes += token.balanceOf(msg.sender); // live balance } // UNSAFE: no timelock, no snapshot check, anyone can execute the instant // quorum is (momentarily) met -- all in one flash-loan transaction. function execute(uint256 id) external { Proposal storage p = proposals[id]; require(!p.executed && p.forVotes >= quorum, "not passed"); p.executed = true; (bool ok, ) = p.target.call(p.data); // runs with this contract's authority require(ok, "exec failed"); } }
Impact
- ▸Treasury theft: a passed proposal transfers protocol funds to the attacker.
- ▸Code seizure: in an upgradeable system, governance can re-point the proxy to attacker-controlled implementation, taking permanent control.
- ▸Parameter sabotage: critical parameters (fees, collateral factors, oracle addresses) changed to enable further extraction.
- ▸Irreversibility: because the action executes with the protocol’s own authority, there is usually no higher power to undo it — the attack is often final.
Governance is a privileged sink: it controls the very mechanisms meant to protect the protocol. That makes governance capture among the highest-impact outcomes, which is precisely why its authority must be anchored to durable, pre-committed stake and a complete execution-gate chain.
Detection — static, dynamic, symbolic, fuzzing, formal
Detection targets both the power source (is voting weight snapshot-anchored or live?) and the pipeline (are quorum, timelock, and execute authorization complete?), then confirms capture by executing an end-to-end acquire→vote→execute run.
- Static analysis
- Flags voting weight read from current balance (not a historical checkpoint), execute paths with no timelock or open callers, and low/absent quorum. Strong SIGNALS of capturability, not proof.
- Dynamic (local-fork) testing
- Reconstruct the full attack on a local chain/fork: acquire power (flash loan or temporary transfer), propose/vote, and attempt execute; observe whether a malicious payload runs. A reproduced treasury transfer is concrete evidence.
- Symbolic execution
- Can confirm an execute path is reachable without a passed-and-timelocked proposal, or that a tally threshold is satisfiable with attacker-controlled power; a model is a counterexample. Cross-contract scope often forces UNKNOWN.
- Fuzzing
- Invariant fuzzing asserts governance safety properties (e.g. "no proposal executes unless it met snapshot-quorum and cleared the timelock"); a violating sequence is reproducible.
- Formal verification
- Prove an authorization invariant over the pipeline — execution implies a valid, passed, timelocked proposal — under a modelled token and timelock. Holds only within those assumptions.
A reproduced capture in a lab is strong evidence that the pipeline is exploitable as configured; it is not a prediction that a specific actor will do it on-chain. SIGNAL (current-balance voting) and FINDING (a reproduced malicious execution) are different strengths of evidence.
How autonomous agents investigate it
An autonomous investigator models the governance pipeline end to end and seeks a concrete, replayable sequence that executes an unauthorized payload — proven by running it, not by reasoning about the thresholds.
- 1Observe: identify the power source (checkpoint vs. live balance), quorum, timelock presence, and who may call execute; mark weaknesses as candidates.
- 2Hypothesise: state the capture claim ("flash-borrow N tokens, propose a treasury transfer, self-vote past the low quorum, and execute in the same transaction because there is no timelock").
- 3Experiment: on a local fork, perform the acquire→propose→vote→execute sequence and observe whether the malicious payload runs and funds move.
- 4Collect evidence: the executed call, the treasury delta, and whether voting power was merely borrowed — all from execution.
- 5Decide: promote to a finding only when a real run executed an unauthorized action; a snapshot-anchored, timelocked pipeline where the run fails is UNKNOWN or informational.
A model is well suited to assembling the multi-step proposal and payload; whether the execution actually succeeds and moves value is a deterministic measurement on the fork, not a conclusion the model should assert.
How RootRaven analyzes it
RootRaven proves a governance finding only by executing the full acquire→propose→vote→execute attack on a disposable LOCAL blockchain or a LOCAL fork of real state — never on a live network — and confirming the unauthorized action with a deterministic, AI-free verifier. The model composes the candidate proposal and payload; the verifier, not the model, decides whether a malicious call executed and whether the power used was legitimately held.
- Observation
- Current-balance voting, low/absent quorum, missing timelock, and open execute are marked as candidate capturability weaknesses — SIGNALS only.
- Hypothesis
- A quantified capture claim: tokens acquired, proposal payload, quorum crossed, and the expected privileged effect.
- Experiment
- On a local fork/chain, acquire power (flash loan or temporary transfer), propose, vote, and attempt execute; record the executed call and state changes. No live-network attack mode.
- Evidence
- The executed malicious call, the treasury/ownership delta, and whether voting power was only borrowed — raw results from the sandbox.
- Verification
- A deterministic checker confirms a privileged action executed without a legitimately-passed, timelocked proposal (or with only transient power) — no model judgement.
- Finding
- Promoted only on a reproduced unauthorized execution; a snapshot-anchored, timelocked pipeline that resisted the run is UNKNOWN, never "safe".
Support is "partial": governance capture depends on token liquidity (to borrow power), timelock timing, and cross-contract wiring, so reproduction needs careful fork setup and is sometimes inconclusive. RootRaven keeps the caveat and reports a non-reproduced weakness as UNKNOWN; a lab capture is scoped to the forked state, block, and parameters used, and is not a claim that an actor will attempt it live.
Mitigation
- ▸Anchor voting power to a historical snapshot (ERC-20 Votes / ERC-6372 checkpoints) read as of the proposal’s snapshot block, so tokens acquired afterward carry no weight — this alone defeats flash-loan voting.
- ▸Require a meaningful proposal threshold and quorum relative to snapshot supply, so capture demands durable, costly stake.
- ▸Enforce a timelock between passing and execution, giving the community time to detect and respond; make the timelock the only path to privileged execution.
- ▸Gate execute so it can run only a proposal that provably passed and cleared its delay; never leave a privileged executor callable without that chain of checks.
- ▸Account delegation carefully (checkpointed, non-double-counting) and consider vote-escrow / lock-ups that bind power to time-committed stake.
- ▸Use a Governor + Timelock framework with a guardian/veto or emergency pause for the transition period, rather than rolling bespoke vote-counting.
- ▸Separate powers: do not let a single proposal both change the rules and move the funds in one uninterruptible step.
Corrected example — Solidity, why, and limits
The corrected governor reads voting power from a historical checkpoint (so borrowed tokens are worthless), enforces quorum against snapshot supply, and routes execution through a timelock whose delay must elapse — removing both the temporary-power and the open-execute roots.
synthetic — snapshot votes + quorum + timelock// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IVotes { // Power as of a PAST block (ERC-20 Votes / ERC-6372). function getPastVotes(address account, uint256 blockNumber) external view returns (uint256); function getPastTotalSupply(uint256 blockNumber) external view returns (uint256); } interface ITimelock { function queue(address target, bytes calldata data, uint256 eta) external returns (bytes32); function execute(address target, bytes calldata data, uint256 eta) external; } contract SnapshotGovernor { IVotes public token; ITimelock public timelock; uint256 public constant DELAY = 2 days; uint256 public constant QUORUM_BPS = 400; // 4% of snapshot supply struct Proposal { address target; bytes data; uint256 snapshot; uint256 forVotes; bool queued; } mapping(uint256 => Proposal) public proposals; mapping(uint256 => mapping(address => bool)) public voted; function propose(address target, bytes calldata data) external returns (uint256 id) { id = uint256(keccak256(abi.encode(target, data, block.number))); // WHY: fix the snapshot to the PREVIOUS block -> future acquisitions don't count. proposals[id] = Proposal(target, data, block.number - 1, 0, false); } // WHY: weight is power AS OF the snapshot block, so flash-borrowed // tokens (acquired now) have zero voting power here. function castVote(uint256 id) external { Proposal storage p = proposals[id]; require(!voted[id][msg.sender], "voted"); voted[id][msg.sender] = true; p.forVotes += token.getPastVotes(msg.sender, p.snapshot); } // WHY: quorum against snapshot supply, then QUEUE into a timelock -- // execution cannot happen in the same transaction as acquiring power. function queue(uint256 id) external { Proposal storage p = proposals[id]; uint256 need = token.getPastTotalSupply(p.snapshot) * QUORUM_BPS / 10000; require(p.forVotes >= need, "quorum not met"); require(!p.queued, "queued"); p.queued = true; timelock.queue(p.target, p.data, block.timestamp + DELAY); // delay must elapse } }
Limits of the fix: snapshot voting defeats flash-loan capture and a timelock restores a reaction window, but neither prevents a patient, well-capitalised attacker from accumulating durable stake and voting legitimately — governance is ultimately a stake contest. A timelock only helps if someone is watching and can veto/pause; quorum set too low is still cheap to meet; and correctness now depends on the token’s checkpointing and the timelock being wired correctly and holding the actual privileges.
Verifying the fix
Verify by attempting the flash-financed capture against the fixed governor and confirming it fails — borrowed tokens carry no snapshot power, and execution cannot occur before the timelock elapses. Conceptual Foundry-style fork tests follow.
conceptual — Foundry fork: flash-loan capture failsfunction test_flashVotingHasNoPower() public { uint256 id = gov.propose(treasury, drainCalldata); // snapshot = block-1 _flashBorrowGovTokens(hugeAmount); // acquired AFTER snapshot gov.castVote(id); // weight counted... _repayFlashLoan(); // WHY it holds: getPastVotes at the snapshot sees zero for the attacker. (, , , uint256 forVotes, ) = gov.proposals(id); assertEq(forVotes, 0); vm.expectRevert(); // quorum not met gov.queue(id); } function test_executeBlockedBeforeTimelock() public { uint256 id = _passLegitimately(); // real snapshot stake gov.queue(id); vm.expectRevert(); // delay not elapsed timelock.execute(treasury, drainCalldata, block.timestamp + 2 days); }
A passing test supports "flash-acquired power contributed zero and execution could not precede the timelock, for this configuration" — a bounded claim. It does not prove the protocol is immune to a legitimately-accumulated majority, nor that quorum and delay are set high/long enough, which are governance-design choices, not properties a test can certify.
Limitations
- ▸Stake contest residual: no code change stops a patient attacker who accumulates durable stake and votes legitimately; governance security is partly economic.
- ▸Timelock only helps if watched: the delay buys reaction time only when a guardian or community can actually veto/pause.
- ▸Parameter policy: tooling can prove snapshot/quorum/timelock are enforced, not that the quorum fraction or delay length are adequate.
- ▸Cross-contract wiring: capture depends on the token’s checkpointing and the timelock holding the real privileges; a mis-wired framework re-opens the hole.
- ▸Fork-scoped and non-predictive: a reproduced capture is bounded to the forked state and is not a claim an actor will attempt it live.
- ▸Maturity caveat: "partial" — RootRaven reproduces some captures in the lab but reports non-reproduced weaknesses as UNKNOWN, never safe.
References
- [1]OpenZeppelin docs — Governor and TimelockController
- [2]OpenZeppelin docs — ERC-20 Votes (checkpointed voting power)
- [3]EIP-6372 — Contract clock for checkpoint-based voting
- [4]OWASP Smart Contract Top 10 — Access Control & Logic Errors
- [5]MITRE CWE-284 (Improper Access Control), CWE-863 (Incorrect Authorization), CWE-840 (Business Logic Errors)
- [6]Foundry Book — fork testing for end-to-end governance attacks
References point to public, well-known technical sources for further study.
Related articles
