Beanstalk Farms Hack: $182M Stolen in One Governance Block

On April 17, 2022, an attacker borrowed a billion dollars in stablecoins, used it to pass a malicious emergency BIP, and drained Beanstalk Farms of $182M — all in a single block. The bug wasn't in the code. It was in the governance design.

The Beanstalk Farms hack was a flash-loan-funded governance attack. The attacker borrowed ~$1B from Aave, deposited it into Beanstalk's stablecoin pools to obtain voting stalk, then invoked emergencyCommit on a pre-proposed malicious BIP that transferred all protocol assets to their wallet — repaying the flash loan and netting $182M profit. Beanstalk's mistake was allowing voting power to be acquired and used within a single transaction, and permitting emergency execution without a snapshot delay. The fix is timelocks, snapshot-based voting, and proposal review windows.
··7 min read

The Bug: Voting Power You Can Rent for One Block

Beanstalk Farms had a governance system where stalk (voting weight) was earned by depositing assets into the Silo. Reasonable. The problem: stalk balance was checked at the moment of vote, not at a snapshot before the proposal existed. And emergency BIPs (Beanstalk Improvement Proposals) could be executed with a 2/3 supermajority — immediately, no timelock.

If you can acquire voting power and use it in the same transaction, your governance is theater. A flash loan is exactly that capability, available to anyone with gas money.

The Attack, Block by Block

On April 17, 2022, the attacker:

1. Two days earlier, submitted BIP-18 and BIP-19. BIP-18 looked like a normal donation proposal. BIP-19 was the real payload — it called `transfer` on Beanstalk's holdings to the attacker's address (disguised as an Aave-Ukraine donation).
2. On attack day, took a flash loan of ~$350M DAI, $500M USDC, $150M USDT, and ~32M BEAN from Aave and Uniswap.
3. Converted that into LP tokens and deposited into the Silo, instantly receiving a supermajority of stalk.
4. Called `emergencyCommit` on BIP-18, which had been live just long enough to satisfy the 24-hour emergency window.
5. BIP-18 executed `init` logic that drained ~$182M in protocol-controlled value.
6. Withdrew, repaid the flash loans, walked away with $76M net (after slippage) and donated $250k to Ukraine on the way out. Dry humor, attacker side.

Vulnerable Pattern

Here's the shape of the bug — voting power read live from balanceOf, no snapshot:

```solidity
function emergencyCommit(uint256 bipId) external {
BIP storage bip = bips[bipId];
require(block.timestamp > bip.start + EMERGENCY_PERIOD, "too early");
// BUG: reads current stalk balances, which an attacker just inflated via flash loan
uint256 forVotes = totalStalkVotingFor(bipId);
uint256 totalStalk = silo.totalStalk();
require(forVotes * 3 >= totalStalk * 2, "no supermajority");
_execute(bip.target, bip.callData);
}
```

The `totalStalkVotingFor` call iterates over voters and pulls their *current* stalk. Deposit a billion dollars one tx earlier, you are the supermajority.

The Fix: Snapshot + Timelock

Two independent mitigations. You want both.

```solidity
function propose(address target, bytes calldata data) external returns (uint256 id) {
id = ++proposalCount;
proposals[id] = Proposal({
target: target,
callData: data,
snapshotBlock: block.number - 1, // voting power frozen BEFORE proposal
start: block.timestamp,
eta: 0
});
}

function vote(uint256 id, bool support) external {
Proposal storage p = proposals[id];
uint256 weight = silo.stalkAt(msg.sender, p.snapshotBlock); // historical read
_recordVote(id, msg.sender, weight, support);
}

function queue(uint256 id) external {
Proposal storage p = proposals[id];
require(_passed(id), "not passed");
p.eta = block.timestamp + TIMELOCK; // minimum 24-48h delay
}

function execute(uint256 id) external {
Proposal storage p = proposals[id];
require(p.eta != 0 && block.timestamp >= p.eta, "timelock");
(bool ok, ) = p.target.call(p.callData);
require(ok, "exec failed");
}
```

Key properties:

**Snapshot block is set on proposal creation and is in the past.** A flash loan in the future cannot retroactively grant voting weight.
- **Voting power requires historical balance tracking** — ERC20Votes-style checkpoints, or per-deposit timestamps in Beanstalk's case.
- **Timelock between passage and execution.** Even if voting somehow gets compromised, there's a window for emergency response, off-chain alerts, or a guardian veto.
- **No same-block compose.** Flash loans must not be able to chain `propose → vote → execute`.

Why "Emergency" Paths Are Almost Always Wrong

The ostensible reason for `emergencyCommit` was to react fast to bugs. In practice, emergency execution is the attack vector. If you genuinely need fast response, use a multisig guardian with narrow pause-only authority — not a voting shortcut. Voting that can complete in one block is just rent-a-quorum.

Compound, OpenZeppelin Governor, and most modern DAO frameworks all enforce: snapshot at proposal block, voting period of days, then queue, then timelock. Beanstalk has since adopted similar patterns and a multisig guardian.

Detection Checklist

If you're shipping governance, audit for:

Is voting weight read via `balanceOf`, `totalSupply`, or any current-state function? **Red flag.**
- Can a proposal be created, voted on, and executed within `< 1 block`? **Critical.**
- Does the snapshot block precede the proposal's existence on-chain? It must.
- Are LP tokens, staked tokens, or any flash-loanable asset counted toward voting power? Use checkpoints.
- Is there an emergency path with a shortened quorum or window? Justify it or kill it.

We catch these in our [free AI audit](https://www.cryptohawking.com/audit) within minutes — it specifically flags governance functions that read live balances. For projects holding meaningful TVL, our [manual audit](https://www.cryptohawking.com/audit/manual) ($5,000, 3 business days, paid in ETH/SOL/USDT) walks the full proposal lifecycle, including flash-loan composability and guardian role separation.

The Lesson

Beanstalk wasn't broken because of a reentrancy or an integer overflow. It was broken because the governance designers assumed token holders would be the ones voting. In a world with $10B+ of flash loan liquidity available on demand, *anyone* can be a token holder for one block. Build accordingly.

FAQ

Why didn't a timelock save Beanstalk?

Beanstalk had no timelock on emergency BIPs — that's the core failure. BIP-18 had been submitted two days earlier but the `emergencyCommit` function allowed execution as soon as 24 hours passed with a 2/3 stalk supermajority, with no delay between passing the vote and executing the payload. A standard 48-hour timelock between passage and execution would have given the team and community time to spot the malicious init contract and respond. Modern Governor implementations enforce this by default.

Can ERC20Votes prevent flash loan governance attacks?

Mostly yes, when combined with proper snapshots. ERC20Votes tracks historical balances via checkpoints, and Governor reads voting power at the proposal's `snapshotBlock`, which is set to `block.number - 1` at proposal creation. A flash loan taken after the proposal exists cannot grant retroactive voting weight. The remaining attack surface is delegating votes to yourself before proposing — which is why long voting periods and timelocks remain mandatory even with ERC20Votes.

What's the difference between BIP-18 and the malicious payload?

BIPs in Beanstalk pointed to an `init` contract whose code would run via delegatecall on execution. BIP-18 was described publicly as a donation proposal, but its init contract actually contained transfer calls that drained all Silo assets to the attacker. The contract was deployed under an innocuous-looking address. The lesson: governance frontends should display the decoded calldata and target contract bytecode hash, not just the human-readable description submitted by the proposer.

How much did the attacker actually profit?

Approximately $76 million net. The protocol lost $182M in total value, but the attacker had to repay roughly $1 billion in flash loans plus fees, absorb slippage from converting drained assets through Uniswap and Curve, and pay gas. They also routed $250k to a Ukraine donation address — likely for optics or PR cover. The remainder was laundered through Tornado Cash within hours. As of 2024, the funds remain unrecovered and the attacker has not been identified.

Should DAOs ban flash-loanable tokens from voting?

Not necessarily ban, but require checkpoints. If voting power comes from an ERC20 that uses ERC20Votes (or equivalent historical balance tracking), flash loans become useless for governance attacks because the snapshot is taken at a past block. The danger appears when voting power is computed from current LP positions, current staked balances, or any composable DeFi position read at vote time. Always anchor weight to a block that exists before the proposal does.

One Solidity tip + 1 case study per month

Beanstalk Farms Hack: $182M Stolen in One Governance Block | Crypto Hawking