Lost
Came back
Attacked
Audit
Beanstalk let a two-thirds supermajority execute a proposal at once through emergencyCommit, one day after it was proposed, and counted voting power (Stalk) from Silo deposits made in the same transaction. The attacker proposed BIP-18 a day early, then flash-borrowed enough to hold about 70% of Stalk, voted, and executed a proposal that sent the protocol's assets to them, all in one transaction.
governanceflash loanvoting powerno timelock
Governance.propose
A day earlier: proposes BIP-18, whose code sends the protocol's assets to the attacker's contract. emergencyCommit becomes possible one day later.
Lenders.flashLoan
Borrows about $1B of DAI, USDC, USDT, BEAN and LUSD for one transaction.
Curve.add_liquidity
Adds them to Curve pools and receives LP tokens.
Silo.deposit
Deposits the LP tokens into the Silo. Stalk, the voting power, is credited at once: about 70% of all Stalk.
Missing check. Voting power counted from a deposit made in this same transaction: no snapshot, no lock.
Governance.vote
Votes for BIP-18 with the borrowed Stalk.
Governance.emergencyCommit
With more than two-thirds of Stalk behind it and a day since the proposal, BIP-18 executes immediately: the protocol's assets move to the attacker.
Missing check. A supermajority could execute at once: no delay between the vote and the execution.
Curve.remove_liquidity
Removes the liquidity.
Lenders.repay
Repays the flash loans and keeps the rest, about $76M, sent on through Tornado Cash.
Fund flow, in order
Voting weight was read live, so it could be borrowed and returned within one transaction; and the emergency path removed the delay that would have let anyone react.
Beanstalk's own response was to remove on-chain governance (the GovernanceFacet) in favour of a multisig.
The invariant that would have failed
The weight a vote counts is the weight its voter held at the proposal's snapshot.
function test_vote_ignoresDepositsAfterTheSnapshot() public { uint32 bip = _propose(); uint256 before = governance.votingPower(alice, bip); _deposit(alice, 1_000e18); // after the proposal's snapshot assertEq(governance.votingPower(alice, bip), before);}