The Auditor’s Take is a weekly series by Jon Stephens, CEO of Veridise. This week’s edition looks at how governance systems that measure voting power instantaneously, rather than from a fixed checkpoint, become exploitable the moment flash loan liquidity is large enough to buy a temporary supermajority, even for a single block. Jon publishes one of these every week; follow along at @FormallyJon.
In April 2022, a governance proposal on Beanstalk passed a two-thirds supermajority, and the contract executed it exactly as it was written to, transferring the protocol’s assets to the attacker. The voting power behind that supermajority existed for one transaction, funded by a flash loan of over a billion dollars and unwound before the transaction closed. No contract was broken; the code did precisely what its own logic called for.
TLDR:
emergencyCommitread instantaneous voting power, so flash-loaned capital cleared the supermajority.- With no voting checkpoint, a deposit made seconds before counted like months-old stake.
- Any privileged path that lets voting weight be manipulated carries this exposure.
- Snapshot-based voting fixes weight at one prior block; borrowed capital counts for nothing.
Why Flash Loans Break Governance Voting
A governance system that reads voting power instantaneously, with no fixed checkpoint to compare against, cannot tell a durable holder from someone renting capital for thirty seconds; both look identical at the moment the check runs.
Attacks that manufacture voting power recur through very different mechanisms. Audius lost $6M in July 2022 when a storage collision let an attacker reinitialize governance, assign themselves 10 trillion AUDIO votes, and drain the treasury. In May 2023, the governance of Tornado Cash was taken hostage: a hidden selfDestruct smuggled inside a routine-looking relayer-penalty proposal delivered 1.2 million votes against 700,000 legitimate ones. The mechanisms share nothing, but the problem underneath is the same: in each case the threshold check ran exactly as written, and the wrong people were able to satisfy it.
A Supermajority Assembled Inside One Transaction
At the time of the attack, Beanstalk was a permissionless stablecoin protocol on Ethereum, and its governance executed proposals through two paths: a standard commit for proposals that finished their full voting period, and emergencyCommit for proposals at least twenty-four hours old holding a two-thirds supermajority of staked voting weight.
On April 16, 2022, the attacker deposited BEAN into the Silo, which generated enough voting weight to submit two proposals. One was a visible $250K donation to a Ukraine relief address, which drew attention as the apparent purpose of the activity. Alongside it sat the proposal that actually carried the drain, and until the moment of execution it did not read as hostile. Twenty-four hours later the attacker flash-loaned over a billion dollars in DAI, USDC, and other assets from Aave, converted it through Curve into the whitelisted LP tokens the Silo accepts, and deposited those to reach roughly 79 percent of total staked weight. One emergencyCommit call executed the malicious proposal, which used delegatecall into an attacker-controlled contract to move the Silo’s assets out. Beanstalk lost roughly $182M. The attacker kept about $80M of it; the difference went to flash loan repayments, swap fees, and slippage paid out along the exit route.
A Correct Function Built on a Wrong Assumption
Read top to bottom, nothing in emergencyCommit looks incorrectly written:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function emergencyCommit(uint32 bip) external { require(isNominated(bip), "Governance: Not nominated."); require( block.timestamp >= timestamp(bip).add(C.getGovernanceEmergencyPeriod()), "Governance: Too early."); require(isActive(bip), "Governance: Ended."); require( bipVotePercent(bip).greaterThanOrEqualTo(C.getGovernanceEmergencyThreshold()), "Governance: Must have super majority." ); _execute(msg.sender, bip, false, true); } |
Every require holds: the proposal is nominated, old enough, still active, and past two-thirds. The problem is where the number in that last check comes from. bipVotePercent(bip) is computed from whatever deposits the Silo holds when the function runs, which makes borrowed capital parked there for seconds indistinguishable from stake held for months. If there is a root cause to point at, it is that balance read: the design treats whatever the Silo holds right now as durable stake, and never accounts for someone assembling a dominant position for a single block. The standard commit path avoids this by closing voting before execution, so late deposits carry no weight there.
What a Senior Auditor Verifies Beyond the Code
In any DeFi audit, a governance execution path raises two questions: does a check gate who can trigger execution, and does that check restrict the callers it was meant to restrict? The first is easy; the second is where the bugs live, and for emergencyCommit the answer is anyone who can assemble two-thirds of staked weight for a single block. The thing to hunt for in review is any path that allows voting weight itself to be manipulated. Reading it instantaneously rather than from a checkpoint recorded before the proposal was submitted, as emergencyCommit does, is one way that happens, not the only one.
The flash loan made the attack cheap, not possible. Any sufficiently large holder could have cleared the threshold, and DeFi liquidity means anyone can rent that position for one block. The fix for this half of the problem is snapshot-based voting, where eligible weight comes from a single block fixed at or shortly before the voting period opens, so late-acquired capital counts for nothing. The guarantee depends on that single-block condition; a design that lets more than one snapshot be referenced for the same vote reopens the door to counting the same weight twice.
A second red flag sits next to the first: the proposal delegated execution to an address holding no code at submission time, asking voters to authorize behavior they could not read. That is a process failure as much as a design one. Requiring the target to already be deployed and reviewable closes the most direct version of it, though not completely: a contract can be deployed and readable and still carry a hidden mechanism, like the selfDestruct in the Tornado Cash case above, that changes its behavior after the vote passes.
Why Review Alone Misses It, and What Tooling Adds
Standard tests validate emergencyCommit against a local deployment with sensible balances. In principle a team could write the test where a single depositor obtains a dominant stake in the Silo in the same transaction as the emergencyCommit call. In practice, nobody writes that test without first imagining the attack, and a team that imagined it would have fixed the design instead.
That is exactly what a running check adds. A specification-guided fuzzer like OrCa can watch for the same signal an auditor is reasoning about here, not a formal guarantee but a heuristic that very likely identifies an exploit if it fires after deployment, and test it against the contracts as they are actually deployed rather than a hand-built scenario. Run inside the 24-hour window between the proposal’s submission and its execution, a check like that would have flagged the exploit while it was still in progress, and possibly stopped it before it executed; that is the difference between continuous security and the one-time audits most of the industry still relies on.
What This Means for Other Protocols
Governance protocols that execute arbitrary code are high-value targets by design: they control how protocol behaviors change and typically have access to the protocol’s assets. Any protocol that determines voting power instantaneously rather than from a prior snapshot inherits the threat of a large instantaneous stakeholder, similar to Beanstalk. The blockchain security question is whether the authorization on that execution path actually holds.
The pattern Veridise keeps running into here is not a mistyped line but a design that never priced in a hostile whale. Beanstalk’s contracts did what they were written to do; what the design missed is that with enough liquidity available to borrow, anyone can be the dominant stakeholder for exactly one block. Findings like that come from asking who can actually satisfy the checks on every privileged path, not just whether the checks exist, and a careful audit would hope to catch it. Beanstalk launched, though, when flash-loan-funded governance takeovers specifically were still a new and largely undocumented pattern, distinct from the price-oracle flash loan attacks the industry already knew well by 2022. A running check built on a simple heuristic does not need that foresight: it flags a balance collapsing inside one transaction whether or not anyone had ever considered this exact attack, catching it either way, without needing the benefit of hindsight.
Working on a similar protocol?
If your protocol executes proposals via delegatecall with voting power measured instantaneously rather than from a checkpoint, you have the same attack surface. It is easy to miss in review because nothing is wrong line by line: the checks are correct, and the problem sits in which population they actually gate.
If you want a second pair of eyes before your next deploy, talk to us.
The Takeaway
A flash loan governance vulnerability lets an attacker borrow enough capital to pass a vote and repay the loan inside a single transaction. It exists whenever voting power is determined instantaneously rather than via a prior snapshot; Beanstalk lost $182M to it in 2022. Catching it means checking who a threshold gates, not just that it runs.