🌳 Advanced ⏱️ 40 min

Governance Attack Vectors

On-chain governance is the mechanism by which token holders collectively make decisions about a protocol — upgrading contracts, setting parameters, managing treasuries. It sounds democratic and decentralized. But governance systems introduce their own attack surface: flash loan governance attacks, whale manipulation, malicious proposal execution, and quorum manipulation. In April 2022, the Beanstalk protocol was drained of $182M in a single transaction using flash-borrowed governance tokens. Understanding how governance fails is critical to designing and auditing safe protocols.

📖 How On-Chain Governance Works

A standard governance lifecycle: (1) A token holder with enough tokens creates a proposal. (2) A voting period begins (typically 2-7 days). (3) If quorum is reached and the vote passes, the proposal enters a timelock queue. (4) After the timelock delay (typically 24-72 hours), anyone can execute the queued proposal. The timelock is designed to give users time to exit if they disagree with the outcome.

Flash Loan Governance Attack: Beanstalk ($182M)

Beanstalk used a governance system where votes were counted at the time of proposal execution — not at the time of proposal creation or at a historical snapshot. This meant an attacker could borrow governance tokens, vote, and execute — all within one transaction.

🔍 Beanstalk Flash Loan Governance Attack
// Beanstalk's BIP (Beanstalk Improvement Proposal) execution flaw // Votes were read at execution time, not at snapshot time // Attacker's single transaction (April 17, 2022): // Step 1: Flash borrow $1B worth of stablecoins flashLoan.borrow(350_000_000e6 USDC + more); // Step 2: Deposit into Beanstalk to get massive voting power beanstalk.depositLP(allStablecoins); // Attacker now held 79% of all deposited Stalk (governance tokens) // Step 3: Beanstalk had an "emergency governance" mechanism // that allowed immediate execution of proposals with >2/3 majority // — bypassing the timelock entirely // Step 4: Vote on the attacker's pre-planted malicious proposal // With 79% of votes: automatic supermajority → immediate execution beanstalk.vote(maliciousBIP); // 79% support beanstalk.emergencyCommit(maliciousBIP); // executes immediately // Step 5: Malicious proposal executes: // - Transfers all Beanstalk assets to attacker's address // - ~$182M in BEAN, ETH, USDC, USDT, DAI // Step 6: Repay flash loan (profit ~$80M after loan fees) // Root causes: // 1. Votes counted at execution time → flash loan attack // 2. Emergency mechanism bypassed timelock → no exit window // 3. No veto mechanism → no circuit breaker
🚨 The Emergency Governance Trap

Beanstalk's emergency governance mechanism was designed for good reasons — allowing fast response to bugs. But it became the kill switch for the entire protocol. Any feature that bypasses the timelock under certain conditions must be analyzed extremely carefully: who can trigger it, what vote threshold is required, and can it be abused with flash-borrowed voting power?

Snapshot-Based Voting: The Correct Design

🔍 Governor Bravo: Correct Snapshot Design
// Compound's Governor Bravo (OpenZeppelin basis) // Votes are snapshotted at proposal CREATION time struct Proposal { uint256 id; address proposer; uint256 startBlock; // When voting begins uint256 endBlock; // When voting ends uint256 snapshotBlock; // ← Balances frozen at this block ... } function propose(...) external returns (uint256) { // Snapshot is set to CURRENT block when proposal is created // Anyone who buys tokens AFTER this block cannot vote on this proposal newProposal.snapshotBlock = block.number; ... } function castVote(uint256 proposalId, uint8 support) external { Proposal storage proposal = proposals[proposalId]; // ✅ Read balance at SNAPSHOT block, not current block uint256 votes = token.getPriorVotes( msg.sender, proposal.snapshotBlock // Historical balance — flash loans can't help ); require(votes > 0, "No voting power at snapshot"); ... } // Why this prevents flash loan attacks: // Flash loans exist within a SINGLE block // The snapshot was taken at proposal creation (current block at that time) // Flash-borrowed tokens are acquired in the SAME block as voting // getPriorVotes() reads balance from BEFORE that block → returns 0 for attacker

Timelock: The Last Line of Defense

🔍 Timelock Security Properties
contract Timelock { uint256 public constant MINIMUM_DELAY = 2 days; uint256 public constant MAXIMUM_DELAY = 30 days; mapping(bytes32 => bool) public queuedTransactions; function queueTransaction(..., uint256 eta) external onlyGovernance { // eta = estimated time of execution require(eta >= block.timestamp + delay, "ETA too early"); bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); queuedTransactions[txHash] = true; // Now everyone can see what will execute in 48 hours // Users who disagree can withdraw funds before execution } function executeTransaction(..., uint256 eta) external payable { bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); require(queuedTransactions[txHash], "Not queued"); require(block.timestamp >= eta, "Too early"); require(block.timestamp <= eta + GRACE_PERIOD, "Stale transaction"); queuedTransactions[txHash] = false; // Execute the call (bool success,) = target.call{value: value}(callData); require(success, "Execution failed"); } } // ❌ Common bypass: an admin function that can skip the timelock function emergencyUpgrade(address newImpl) external onlyOwner { // This completely bypasses governance and timelock _upgradeTo(newImpl); // ← HUGE centralization risk }

Build Finance DAO Takeover

⚠️ Build Finance DAO: Governance Taken Over by a Whale

In February 2022, a single address accumulated enough BUILD tokens to unilaterally pass a governance proposal that minted 1,000,000 BUILD tokens to themselves and drained the DAO treasury of ~$470K. The protocol had no quorum requirement and no timelock. Once one whale held majority voting power, they had complete control. This illustrates that governance security requires not just technical mechanisms but also token distribution analysis — a protocol where 51% of tokens can be bought cheaply has zero meaningful governance security.

Governance Parameter Attacks

🔍 Governance Parameter Manipulation
// Governor Bravo has setable parameters — attackers target these // ❌ DANGEROUS governance parameter values: governor.setVotingDelay(1); // 1 block voting delay — no time to react governor.setVotingPeriod(10); // 10 blocks = ~2 minutes to vote governor.setQuorum(1); // 1 token for quorum → anyone can pass proposals // Attack sequence using parameter proposals: // Proposal A (legitimate): "Upgrade the fee parameter" // → Wait for it to pass normally (builds trust) // Proposal B: "Set votingDelay=1, votingPeriod=10, quorum=1" // → If passed, the attacker can now push any future proposal through // → In 10 blocks (~2 min) with 1 token, before anyone reacts // Proposal C: "Transfer treasury to attacker" // → Created immediately after B executes // → Passes in 10 blocks with the attacker's 1 token // ✅ PROTECTION: Governance parameters should themselves require timelock // and have hardcoded minimum values uint256 constant MIN_VOTING_DELAY = 1 days; uint256 constant MIN_VOTING_PERIOD = 3 days; uint256 constant MIN_QUORUM = 1_000_000e18; // 1M tokens minimum

Safe Governance Design

FeatureUnsafe DesignSafe Design
Vote SnapshotCurrent balance at vote/execution timeHistorical balance at proposal creation
TimelockNone, or bypassable by adminMandatory, minimum 48 hours
QuorumVery low or fixed at launchMeaningful % of circulating supply; not changeable to 0
Emergency PowerOne-key admin can do anythingGuardian can only pause, not act; requires own timelock
Parameter ChangesAny parameter changeable via governance immediatelyGovernance parameters have hardcoded minimums
Veto PowerNoneSecurity council can veto within timelock window
Proposal ThresholdAny token holder can proposeHigh threshold prevents spam; delegates allow participation

Auditing Governance: What to Check

🔍 Governance Audit Checklist
// Questions to answer for every governance system: // 1. When is voting power read? // → Must be historical snapshot, never current block token.getPriorVotes(account, proposal.snapshotBlock); // ✅ token.balanceOf(account); // ❌ // 2. Can a proposal drain the treasury? // → What is the maximum value a single proposal can move? // → Is there a per-proposal spending limit? // 3. Can governance change the timelock delay to 0? // → Check minimum delay enforcement require(newDelay >= MINIMUM_DELAY); // ✅ — must exist // 4. Is there a guardian/security council with veto power? // → Who are the members? Are keys hardware-secured? // 5. What can the timelock's admin address do directly? // → Admin should only be the Governor contract itself // 6. Can the Governor be upgraded? // → If proxy, what guards the upgrade? // 7. What happens to queued proposals if governance is upgraded? // → Old proposals should be cancelable by original proposer // 8. Can vote delegation be used to concentrate power? // → Is there a maximum delegation cap?
💡 What a Malicious Passed Proposal Can Do

A governance proposal executes as a call from the Timelock contract. The Timelock typically has admin rights over the entire protocol. This means a passed proposal can: transfer all treasury assets, upgrade smart contracts to malicious implementations, change all protocol parameters (interest rates, collateral factors, fees), add new admin addresses, and disable security mechanisms. Never underestimate what governance can do — treat it as equivalent to root access.

Vote Delegation: Concentrated Power Risks

Most governance systems (ERC20Votes, Compound, OpenZeppelin Governor) allow token holders to delegate their voting power to another address. This is useful — small holders can delegate to active community members. But it creates concentration risk: if a single delegate accumulates enough delegated power, they can unilaterally pass proposals.

🔍 Vote Delegation Attack Vector
// OpenZeppelin ERC20Votes delegation // Any holder can delegate their votes to any address token.delegate(attackerAddress); // attacker receives my voting power // Attack scenario: // 1. Attacker controls 10% of supply (below 40% quorum threshold) // 2. Attacker convinces passive holders (40% of supply) to delegate // 3. Attacker now has 50% voting power → can pass any proposal // 4. Proposal: transfer treasury to attacker // This is a social engineering attack, not a code exploit // It is amplified by passive holders who delegate without reading proposals // Mitigations: // 1. Delegation cap: no single delegate can hold >20% of voting power function delegate(address delegatee) public override { uint256 newPower = getVotes(delegatee) + balanceOf(msg.sender); require(newPower <= totalSupply() * 20 / 100, "Exceeds delegation cap"); super.delegate(delegatee); } // 2. Proposal guardian: security council can veto within timelock window // 3. Spending limits: governance proposals can move max X per 30 days

The Governor Bravo Governance Vulnerability

🔍 Compound's Real Governance Bug
// In 2021, Compound's Governor Bravo had a real bug (not exploited): // The _castVoteInternal() function had an integer overflow in vote counting // under specific conditions with very large vote amounts // More relevant for auditors: the proposal.eta timing bug // Governor Bravo queues proposals with an ETA (Estimated Time of Arrival) // The timelock checks: block.timestamp >= proposal.eta // Bug pattern: what if proposal.eta can be set to 0? function queue(uint256 proposalId) external { Proposal storage proposal = proposals[proposalId]; require(state(proposalId) == ProposalState.Succeeded); uint256 eta = block.timestamp + timelock.delay(); // eta is correctly computed here // But: what if timelock.delay() can return 0? // If delay() is user-controlled or can be set to 0 via another proposal... // eta = block.timestamp + 0 = block.timestamp // Proposal can be executed immediately after queuing — no waiting period proposal.eta = eta; for (uint256 i; i < proposal.targets.length; i++) { timelock.queueTransaction( proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], eta ); } } // Security audit check: can the timelock delay be set to 0? // Is there a minimum delay enforced in the Timelock contract itself? uint256 public constant MINIMUM_DELAY = 2 days; // ✅ Must exist and be enforced require(delay_ >= MINIMUM_DELAY && delay_ <= MAXIMUM_DELAY); // ✅

Emergency Multisig: Circuit Breaker Design

📖 The Guardian Pattern

Many protocols implement a "guardian" or "security council" — a multisig that can pause the protocol or veto governance proposals within the timelock window. The critical constraint: the guardian should only have NEGATIVE powers (block/pause), never POSITIVE powers (execute actions). A guardian that can pause but not act preserves decentralization while adding a human circuit breaker for emergencies. Compound, Aave, and Uniswap all implement some version of this pattern.

🔍 Guardian Veto Implementation
// Guardian: can cancel queued proposals but CANNOT execute them contract GovernorWithGuardian { address public guardian; // 5/9 security council multisig // ✅ Guardian CAN DO: cancel a queued proposal before it executes function guardianCancel(uint256 proposalId) external { require(msg.sender == guardian, "Only guardian"); require(state(proposalId) == ProposalState.Queued, "Not queued"); _cancel(proposalId); emit ProposalCanceled(proposalId); } // ✅ Guardian CAN DO: pause protocol functions in emergency function guardianPause() external { require(msg.sender == guardian, "Only guardian"); _pause(); } // ❌ Guardian CANNOT DO: execute actions directly // ❌ Guardian CANNOT DO: transfer treasury funds // ❌ Guardian CANNOT DO: upgrade contracts // ❌ Guardian CANNOT DO: change governance parameters // These actions require full governance process + timelock // Guardian itself should have a timelock for parameter changes // Multisig composition: known public figures OR security firms // Guardian keys should be geographically distributed + hardware wallets }

Governance Auditing: Full Parameter Check

Governance ParameterDangerous ValueSafe ValueCan Governance Change It?
Voting Delay0 blocks (proposal = instant vote)>= 1 day in blocksYes, but with minimum enforced in code
Voting Period< 1 day (no time for community)3-7 days minimumYes, but with minimum enforced in code
Proposal Threshold0 tokens (anyone can spam proposals)0.1-1% of supplyYes, but cannot be set to 0
Quorum0 (one vote passes anything)4-10% of total supplyYes, but cannot be set to 0
Timelock Delay0 seconds (no exit window)48-72 hours minimumYes, but minimum is hardcoded constant
Guardian Threshold1/1 (single key can veto)3/5 or 5/9 multisigOnly via governance + timelock

Griefing Attacks Against Active Proposals

Even a well-designed governance system can be disrupted by griefing attacks that target the mechanics of the voting and execution cycle. These attacks do not steal funds directly — they prevent legitimate governance from functioning, creating protocol paralysis.

🔍 Proposal Spam and Queue Griefing
// Attack 1: Proposal spam — fill up the proposal queue // If any user can create proposals (threshold = 0): // Attacker creates 100 proposals that all fail → queue is full // Legitimate proposals cannot be created until queue clears // Fix: proposal threshold (e.g., must hold 1% of supply) // Attack 2: Block-based timing manipulation // Some governance uses block.number for voting windows // On L2 chains (Optimism, Arbitrum), block.number != Ethereum block.number // Optimism has 1-second blocks → "1 day delay" = 86,400 L2 blocks // Attacker who knows this can time proposals to coincide with low voter activity // Fix: use block.timestamp instead of block.number for voting windows on L2 // Attack 3: Last-minute voting (governance 51% attack without flash loan) // If quorum is low and a large holder waits until the last block to vote: // Other voters see "proposal is failing" and do not vote // At last moment, whale votes YES and passes quorum // Defense: "optimistic quorum" — if late votes change outcome, extend voting period // Attack 4: Governance griefing via token locking // Some protocols: voting requires tokens to be locked during proposal lifecycle // Attacker votes on every new proposal (even bad ones) to lock competitor tokens // Their tokens are also locked but they don't need liquidity // Result: circulating supply of governance tokens collapses → quorum impossible // Attack 5: ETA manipulation in Timelock queue // executeTransaction() checks: block.timestamp >= eta // If eta is calculated as block.timestamp + delay at queue time, // a miner/validator can subtly adjust timestamps // Not practically exploitable on PoS Ethereum, but worth noting for L1 forks

Real-World Governance Takeovers

🚨 Build Finance DAO Takeover ($470K, February 2022)

Build Finance DAO used a governance system where the founding team held a majority of tokens. A governance attacker simply accumulated enough voting power over time (no flash loan needed) and submitted a proposal to mint themselves additional governance tokens, which passed because the founding team did not vote. This gave them permanent super-majority control. The attacker then transferred the treasury to themselves. The attack took weeks of token accumulation and succeeded because the existing holders were not actively monitoring governance. The lesson: governance apathy is a vulnerability, and large inactive token holders are a liability.

🔍 Auditing Governance Systems: Key Checks
// Governance audit checklist (run for every governance system) // GOV-1: Voting power source // grep: balanceOf( in castVote() → CRITICAL: live balance, flash loan exploitable // grep: getPriorVotes( or getVotes( → OK: historical snapshot // GOV-2: Snapshot block for new proposals // Must be: snapshotBlock = block.number - 1 // NOT: snapshotBlock = block.number (same block allows flash loan snapshot) // GOV-3: Timelock minimum delay // Read: minimumDelay constant — should be >= 48 hours // Check: can governance vote to reduce minimumDelay to 0? // If yes: attacker can first reduce delay, then execute arbitrary proposals instantly // GOV-4: Emergency mechanisms // Any function that bypasses timelock is a backdoor // grep: execute( WITHOUT queueTransaction — check if this bypasses timelock // Acceptable: guardian veto (blocks execution, does not bypass timelock) // Not acceptable: emergency execute that skips timelock delay // GOV-5: Proposal creation threshold // proposalThreshold should be 0.01%-1% of supply, not 0 // Read: proposalThreshold() or PROPOSAL_THRESHOLD constant // GOV-6: Token delegation concentration // Check top delegate addresses: can any single delegate pass quorum alone? // This is a systemic risk finding (usually Medium/High, not Critical)

Key Takeaways

  • Governance voting power must be snapshotted at proposal creation, not at vote or execution time — the Beanstalk exploit ($182M) exploited the latter design.
  • The timelock is the fundamental security guarantee of on-chain governance — any bypass mechanism (emergency execution, admin override) is a critical centralization risk.
  • Governance parameters (quorum, voting delay, period) must have hardcoded minimums that cannot be set to zero through governance itself.
  • Any protocol where governance tokens have low market cap can be attacked by a well-funded actor who simply buys enough tokens to pass proposals.
  • A security council or guardian with veto-only power (not acting power) provides meaningful protection without re-centralizing the protocol.
  • Vote delegation creates concentration risk — auditors should check whether a single delegate can accumulate enough power to unilaterally pass proposals.
  • A passed governance proposal has the full power of the Timelock admin — which is typically root access to the entire protocol.