Expert level
Onchain governance lets token holders propose, vote on and execute arbitrary contract calls, usually through a Governor contract that snapshots voting power, counts votes against a quorum and hands passed proposals to a timelock. Its failures are predictable: votes borrowed for a single transaction, malicious code hidden behind an innocent proposal, low turnout that lets a motivated minority win, and delegation that concentrates power. Good designs price the cost of capture and give users time to leave.
The Governor pipeline
A proposal is a list of contract calls: targets, values and calldata. Whatever those calls can do, a passed proposal can do, which makes governance an admin key with a voting procedure in front of it (the key itself is covered in admin keys and timelocks). The standard pipeline, as implemented by OpenZeppelin's Governor and the Compound Governor Bravo lineage before it:
| Stage | What happens | Typical setting |
|---|---|---|
| Propose | Anyone above the proposal threshold submits calls and a description | A threshold in votes |
| Voting delay | Time to review before voting power is fixed | OpenZeppelin’s example: 7,200 blocks, about a day |
| Snapshot | Voting power is read at a past timepoint, not live | Set when the delay ends |
| Voting period | For, against, abstain | Example: 50,400 blocks, about a week |
| Quorum and majority | Enough participation, more for than against | Example: 4% of supply |
| Queue and timelock | Passed calls wait in a timelock before they can run | Days |
| Execute | Anyone triggers the calls once the delay passes |
// OpenZeppelin Governor, the configuration that matters. The overrides
// Solidity requires to combine these modules are omitted.
contract ProtocolGovernor is
Governor, GovernorCountingSimple, GovernorVotes,
GovernorVotesQuorumFraction, GovernorTimelockControl
{
constructor(IVotes token, TimelockController timelock)
Governor("ProtocolGovernor")
GovernorVotes(token) // ERC-5805 checkpointed voting power
GovernorVotesQuorumFraction(4) // quorum: 4% of supply at the snapshot
GovernorTimelockControl(timelock) // passed proposals queue here
{}
function votingDelay() public pure override returns (uint256) {
return 7200; // about 1 day of blocks, then the snapshot is taken
}
function votingPeriod() public pure override returns (uint256) {
return 50400; // about 1 week of voting
}
function proposalThreshold() public pure override returns (uint256) {
return 100_000e18; // votes needed to propose (illustrative)
}
}The snapshot is the most important safety property in the pipeline. In OpenZeppelin's Governor the snapshot
is the proposal time plus the voting delay, and each vote is weighted by getPastVotes at that
timepoint, served from the checkpoints that
ERC-5805 tokens keep on
every transfer and delegation. ERC-6372 lets the clock be block numbers or timestamps, which matters on layer 2s
with irregular blocks. Tokens only vote once delegated, even to yourself. The Governor should be the only
holder of the timelock’s proposer and canceller roles, the executor role should be open (granted to the zero
address) or tightly held, and the timelock’s admin role should be renounced after setup.
Flash loan governance: Beanstalk
On April 17, 2022, at about 12:24 UTC, Beanstalk lost about $182 million because its voting power had no
snapshot. Power came from deposits in its Silo and counted immediately, and an emergencyCommit path
let a proposal execute with a two thirds supermajority once it had been live for a day.
A day earlier, the attacker had submitted two proposals: BIP-18, which sent the protocol's assets to the
attacker, and BIP-19, which donated $250,000 in BEAN to Ukraine's official crypto donation address. Then, in one
transaction: flash loans of about $1 billion in DAI, USDC and USDT from Aave, plus BEAN and LUSD from Uniswap and
SushiSwap; conversion into Curve LP tokens; a deposit into the Silo that produced about 79% of the vote; a call to
emergencyCommit; execution; repayment of every loan. The protocol lost about $182 million in
assets, of which the attacker kept roughly $76 to $77 million in non Bean assets.
Three lessons generalize. Power measured at a past timepoint cannot be borrowed for one transaction. No path should run from vote to execution in the same block. And emergency paths are the attack surface, because they exist to skip the safeguards.
Snapshots turn a flash loan attack into a rental. If the governance token can be borrowed on a lending market, an attacker can borrow before the snapshot, delegate, and hold through the vote:
The real defense is often market depth, not the formula: if 40 million tokens cannot be borrowed at any rate, the rental does not exist. Governance tokens with deep lending markets deserve longer delays and higher thresholds for treasury actions.
Malicious proposals hidden in code: Tornado Cash
Voters approve a description. Execution runs whatever code sits at the target address when the timelock expires. In May 2023, Tornado Cash governance was taken over through that gap. The attacker submitted a proposal presented as a copy of one voters had already approved. Its contract carried a hidden path to SELFDESTRUCT, and it had been deployed with CREATE from a factory that was itself deployed with CREATE2.
After the vote passed, the attacker destroyed both. Redeploying the factory with CREATE2 put it back at the same address with its nonce back at its starting value (1, under EIP-161), so its next CREATE landed on the approved proposal address with entirely different code. On execution, on May 20, 2023, that code granted attacker addresses 10,000 locked TORN each, 1.2 million votes in total, more than all legitimate votes combined. The attacker withdrew and sold about 483,000 TORN, and later that month passed Proposal 21 restoring governance and zeroing their own votes.
Since Dencun in March 2024, EIP-6780 limits SELFDESTRUCT to deleting code only in the transaction that created the contract, which closes this exact trick on Ethereum. The class remains: upgradeable targets, DELEGATECALL into code that can change, targets with no code yet at proposal time, and calldata that does not match the description. Two defenses cover most of it. Simulate every proposal on a fork and publish the state changes, as Uniswap's Seatbelt, released with ScopeLift in January 2022, began doing for Governor Bravo proposals. And pin the code:
// Pin the code each target had when the proposal was made, and refuse to
// execute if it changed. For a proxy, pin its implementation as well.
mapping(uint256 => bytes32[]) internal pinned;
// Nonexistent accounts report 0; accounts holding only ether report
// keccak256(""). Treat both as "no code", or anyone could send 1 wei to a
// fresh recipient, flip its hash and block the proposal.
function _codeHash(address a) internal view returns (bytes32 h) {
h = a.codehash;
if (h == bytes32(0)) h = keccak256("");
}
function propose(address[] memory targets, uint256[] memory values,
bytes[] memory calldatas, string memory description)
public override returns (uint256 id)
{
id = super.propose(targets, values, calldatas, description);
for (uint256 i; i < targets.length; ++i) pinned[id].push(_codeHash(targets[i]));
}
function _executeOperations(uint256 id, address[] memory targets,
uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
internal override
{
for (uint256 i; i < targets.length; ++i)
require(_codeHash(targets[i]) == pinned[id][i], "target code changed");
super._executeOperations(id, targets, values, calldatas, descriptionHash);
}A target with no code at proposal time pins the empty hash, so a contract deployed there later fails the check,
which blocks the "propose now, deploy later" pattern as well. The normalization in _codeHash matters:
without it, sending 1 wei to a fresh recipient flips its hash from zero to the empty hash and blocks the proposal. An
ordinary account that later sets an EIP-7702 delegation also fails the check, which is usually what you want.
Vote buying, turnout and capture
The cost of capturing a vote is set by turnout, not supply. A token with a billion units and 4% turnout can be captured with a little over 40 million votes, bought, borrowed or bribed. Buying has a hidden cost: the attacker holds tokens that fall when the attack becomes public. Borrowing or hedging removes it, producing votes with no economic exposure, sometimes called empty voting, which breaks the assumption that voters bear the consequences of their votes.
Compound's proposal 289 in July 2024 showed capture by coalition rather than exploit. A group proposed moving 499,000 COMP from the treasury into a yield vault run by the proposers. It passed on July 28, 2024 with about 682,190 votes for and 633,640 against, over a quorum of 400,000, in a token with a 10 million supply. It was canceled on July 30, before execution. Nobody broke a contract; with about 13% of supply voting, a coalition came within a timelock of redirecting treasury funds.
Governance can even vote on its own losses. After the Mango Markets exploit in October 2022, the attacker used governance tokens to put forward a settlement in Mango's DAO, per rekt.news, proposing a large bounty and no criminal pursuit, and voted for it. Vote markets built for emissions, covered in mechanism design, are the legitimate cousin of all this.
Delegation and its concentration
Delegation fixes apathy by letting passive holders lend their votes to people who read proposals. It also concentrates power in a small set of delegates, and tokens delegated to addresses that stopped voting simply vanish from turnout, which lowers the cost of capture computed above.
Useful mitigations are procedural. Proposal thresholds keep spam out without silencing delegates: Arbitrum
requires a proposer to have at least 1,000,000 votable tokens delegated. Late quorum protection stops last minute
swings: if quorum is first reached in the final 2 days of Arbitrum's 14 day voting period, voting stays open
until 2 days after that point, the same idea as OpenZeppelin's GovernorPreventLateQuorum. Public voting records, easy redelegation
and delegate statements let holders fire delegates who stop showing up.
Timelocks, vetoes and the right to exit
A timelock protects users only if they can leave within it. If withdrawing takes seven days, a two day timelock protects nobody. Arbitrum's constitution builds this in: a constitutional proposal takes at least 42 days from temperature check to execution, including a waiting period on Arbitrum One (3 days for treasury actions, 8 for messages to layer 1) and a further 3 day wait on Ethereum, so that withdrawals started in protest can complete first.
Newer designs give users a direct brake. Lido's Dual Governance, live on Ethereum mainnet since July 4, 2025, lets stETH holders escrow their tokens against DAO proposals. At 1% of stETH supply in escrow, veto signaling delays governance by 5 to 45 days depending on the level of opposition. At 10%, rage quit blocks governance until the escrowed stETH has been withdrawn. Optimism, as of September 2026, puts protocol upgrades through a Developer Advisory Board review followed by a 7 day veto period for tokenholders, chains, apps and users. The older Moloch ragequit, where members exit with their share of the treasury before a proposal they oppose runs, is the same principle.
Vote to act
Classic token governance
- Every change is explicitly approved
- Slow, and turnout sets the cost of capture
- Minorities have no recourse except selling
Optimistic, with veto and exit
Proposals pass unless stopped
- Fast by default, with a brake for real opposition
- Affected users get time and a mechanism to leave
- More states and parameters to design and verify
Security councils and emergency powers
Some failures cannot wait six weeks. Arbitrum's answer is a Security Council of 12 elected members in two cohorts of six. Emergency actions, with no delay, need 9 of 12 signatures. Non emergency actions also need 9 of 12 and then pass through the same waiting periods as ordinary proposals. Members can be removed by a vote with at least 10% of votable tokens participating and five sixths in favor, or by 9 fellow members.
A council is a multisig that can do almost anything, so its safety rests on threshold, independence of members and key management (see multisig, MPC and threshold signatures), plus narrowly defined emergencies and regular elections. For rollups, how these powers are bounded is part of how L2BEAT assigns stages, covered in rollup architecture.
Offchain voting, onchain execution
Snapshot voting is free for voters: they sign messages, and a strategy reads balances at a chosen block (the general idea is in the snapshot glossary entry). The result binds nothing onchain. A multisig then executes it, so the real security model is "signers who promise to follow the vote". Bridges that executed Snapshot results optimistically existed; UMA, for example, ended support for its oSnap module on December 15, 2025. Many DAOs use both layers: a free offchain temperature check, then a binding onchain vote. Uniswap's UNIfication proposal, for instance, asked for future fee parameter votes to go straight to Snapshot followed by an onchain vote.
Design recommendations
Snapshot every vote and never execute in the voting block
Checkpointed voting power at a past timepoint, a voting delay long enough to review, and a timelock after every passed proposal, including emergency ones.
Size the timelock to the exit
The delay must exceed the time it takes an affected user to withdraw, bridge out or unbond.
Price capture against turnout
Compute the cost to buy, borrow or bribe a winning vote at realistic turnout, and compare it with what the vote controls. Raise thresholds or delays for treasury and upgrade actions until the cost exceeds the prize.
Make proposals verifiable
Verified source for every target, pinned code hashes, public fork simulations with state diffs, and a canceller role able to stop a proposal whose code changed.
Separate powers
Emissions routing, parameter changes, treasury spending and upgrades need different thresholds. A vote that can be rented cheaply should not move the treasury.
Give minorities a brake or an exit
Veto escrow, ragequit, or at least a delay that lets dissenters leave before a change they oppose takes effect.
Common questions
How did the Beanstalk flash loan governance attack work?
Beanstalk counted voting power from deposits immediately, with no snapshot, and let a proposal execute with a two thirds supermajority through an emergency path after one day. The attacker proposed a transfer to themselves, waited a day, then flash borrowed about $1 billion, deposited it for about 79% of the vote, executed the proposal and repaid the loans in one transaction.
What does a timelock protect against?
It puts time between a decision and its execution, so users can review what passed, cancel it through a guardian if it is malicious, or withdraw before it takes effect. It only protects users if the delay is longer than the time they need to exit.
What is optimistic governance?
A design where proposals take effect after a waiting period unless a defined group objects, instead of needing an affirmative vote to pass. Lido's Dual Governance and Optimism's protocol upgrade veto period are examples: fast by default, with a brake that affected parties can pull.
Is Snapshot voting onchain?
No. Snapshot votes are signed messages stored offchain, counted against balances at a chosen block. They cost voters nothing but bind nothing, so execution usually depends on a multisig that has committed to follow the result.
How can a DAO defend against vote buying?
It cannot stop votes being sold, but it can make capture expensive and slow: snapshots and delays so votes must be held rather than flash borrowed, higher thresholds for treasury actions, separate powers for emissions and upgrades, and veto or exit mechanisms for the minority.
Where to go next
You reached the end of the ladder
Beginner to Expert, every level. The best way to keep this knowledge sharp is to use it: build something, audit something, or teach somebody the level below.