Flash-Loan Governance Attacks: How Beanstalk Lost $182M in One Block

An attacker borrowed $1 billion, voted with it, drained the treasury, and repaid the loan — all in 13 seconds. Beanstalk's governance had no timelock, no snapshot, no voting delay. Just instant execution on whoever held the most tokens at block.timestamp. The fix is three lines of code. Most DAOs…

A flash-loan governance attack exploits DAOs that use spot token balances for voting power. An attacker borrows millions in governance tokens via a flash loan, submits and executes a malicious proposal (often draining the treasury), then repays the loan — all atomically in a single transaction. The fix is checkpoint-based voting (snapshot at proposal creation), mandatory voting delays, and timelocks on execution.
··7 min read

The bug: voting power measured at the wrong moment

Flash-loan governance attacks happen because DAOs measure voting power at `block.timestamp` of the vote, not at a snapshot taken before the proposal existed. If your governance token is liquid on Aave, Balancer, or any flash-loan provider, an attacker can borrow more tokens than your entire active voter base in one transaction, vote, execute, and repay — all atomically.

The attack only works when three conditions line up:

1. Voting power is read from current `balanceOf` (or current delegate weight) instead of a historical checkpoint.
2. There is no voting delay between proposal submission and the start of voting.
3. There is no timelock between proposal passing and execution.

Beanstalk had all three. So did several smaller forks that followed.

Vulnerable pattern

Here is the canonical broken governance contract. Read it and weep:

```solidity
contract NaiveGovernance {
IERC20 public immutable token;
uint256 public proposalCount;

struct Proposal {
address target;
bytes data;
uint256 yesVotes;
bool executed;
}
mapping(uint256 => Proposal) public proposals;

function propose(address target, bytes calldata data) external returns (uint256 id) {
id = ++proposalCount;
proposals[id] = Proposal(target, data, 0, false);
}

function vote(uint256 id) external {
// BUG: uses live balance, not a checkpoint
proposals[id].yesVotes += token.balanceOf(msg.sender);
}

function execute(uint256 id) external {
Proposal storage p = proposals[id];
require(!p.executed, "done");
require(p.yesVotes > token.totalSupply() / 2, "no quorum");
p.executed = true;
(bool ok,) = p.target.call(p.data);
require(ok, "call failed");
}
}
```

An attacker writes a single function that calls `propose`, `vote`, and `execute` in sequence, wrapped in a flash loan. Game over.

Real incidents

**Beanstalk Farms — April 17, 2022 — $182 million.** The attacker took a flash loan of roughly $1 billion across Aave and Uniswap V3, swapped into BEAN, 3CRV, and LP tokens to acquire 79% of Stalk (the governance asset), then executed a proposal they had submitted 24 hours earlier. The proposal sent $182M of protocol assets to their wallet (with $250k laundered through Ukraine donations for plausible deniability — a nice touch). The flash loan was repaid in the same transaction. Total profit relative to gas: extraordinary.

Beanstalk's mistake was that while proposals required a 24-hour wait under normal conditions, an "emergencyCommit" function allowed immediate execution if a supermajority voted yes. The flash loan provided that supermajority instantly.

**Mango Markets — October 11, 2022 — $114 million.** Avraham Eisenberg manipulated the MNGO oracle to inflate his collateral, borrowed everything in the treasury, then — and this is the part people forget — used his stolen MNGO position to vote on a governance proposal to keep $47M as a "bug bounty" and prevent prosecution. That last part didn't work; he was convicted in 2024. But the structural lesson is identical: governance tokens that can be acquired instantly should not have instant voting power.

The fix: checkpoints, delays, timelocks

OpenZeppelin's `ERC20Votes` and `Governor` contracts solve this correctly. The core idea: snapshot voting power at the block the proposal was created, then enforce a delay before voting opens and a timelock before execution.

```solidity
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

contract SafeGovernance {
ERC20Votes public immutable token;
uint256 public constant VOTING_DELAY = 1 days;
uint256 public constant VOTING_PERIOD = 3 days;
uint256 public constant TIMELOCK = 2 days;

struct Proposal {
address target;
bytes data;
uint256 snapshotBlock;
uint256 voteStart;
uint256 voteEnd;
uint256 executeAfter;
uint256 yesVotes;
bool executed;
}
mapping(uint256 => Proposal) public proposals;
uint256 public proposalCount;

function propose(address target, bytes calldata data) external returns (uint256 id) {
id = ++proposalCount;
uint256 snap = block.number - 1; // past block — flash loans can't reach back
proposals[id] = Proposal({
target: target,
data: data,
snapshotBlock: snap,
voteStart: block.timestamp + VOTING_DELAY,
voteEnd: block.timestamp + VOTING_DELAY + VOTING_PERIOD,
executeAfter: 0,
yesVotes: 0,
executed: false
});
}

function vote(uint256 id) external {
Proposal storage p = proposals[id];
require(block.timestamp >= p.voteStart && block.timestamp < p.voteEnd, "closed");
// Reads historical voting power — a flash loan in this block can't influence it
uint256 weight = token.getPastVotes(msg.sender, p.snapshotBlock);
p.yesVotes += weight;
}

function queue(uint256 id) external {
Proposal storage p = proposals[id];
require(block.timestamp >= p.voteEnd, "voting open");
require(p.yesVotes > token.getPastTotalSupply(p.snapshotBlock) / 2, "no quorum");
p.executeAfter = block.timestamp + TIMELOCK;
}

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

Three things matter here:

**`getPastVotes(account, pastBlock)`** reads from a checkpoint. A flash loan executed in the current block cannot retroactively appear in block `N-1`.
- **Voting delay** prevents proposing and voting in the same transaction. Even if an attacker accumulates tokens legitimately, the community gets time to notice.
- **Timelock on execution** means even a successful malicious proposal sits in the queue long enough for a guardian multisig to cancel it or for users to exit.

What auditors check

When we review governance code on [/audit/manual](https://www.cryptohawking.com/audit/manual), these are the questions we run through every time:

Does voting power come from `balanceOf` or from a historical checkpoint?
- Is the snapshot block strictly less than the proposal creation block?
- Is there any "emergency" path that bypasses the timelock? (This killed Beanstalk.)
- Can someone propose and vote in the same block?
- Is the governance token itself transferable and lendable on major money markets? If yes, flash-loan exposure is real.
- Does the timelock have a guardian or veto role for emergencies?
- For NFT or staking-based governance: can voting power be acquired and used in the same block?

If you want a fast first pass before paying for a deep review, the free [AI audit](https://www.cryptohawking.com/audit) catches the obvious checkpoint and timelock misses in seconds.

A note on "vote-escrowed" tokens

veTokens (Curve-style) are flash-loan resistant by design — you cannot lock and unlock in the same transaction, so flash loans cannot acquire voting power at all. This is one reason ve-governance has aged better than balance-based governance. If you are designing a new DAO from scratch in 2025 and your protocol has a treasury worth attacking, seriously consider vote-escrow or at least delegation-with-lockup on top of `ERC20Votes`.

TL;DR

If your governance contract reads `balanceOf(msg.sender)` anywhere in the voting path, you are one flash loan away from being a case study. Use `ERC20Votes`, snapshot at a past block, enforce a voting delay, and never, ever ship an `emergencyCommit` function. Beanstalk learned this for $182 million. You can learn it for the price of an OpenZeppelin import.

FAQ

Why doesn't a checkpoint-based snapshot prevent all flash-loan governance attacks?

Checkpoints prevent same-block attacks, but a determined attacker can still accumulate tokens legitimately over time, wait for the snapshot block, and then attack. Snapshots solve atomic flash-loan exploits, not whale capture. The voting delay and timelock are what protect against the slower form of the attack — they give the community time to detect a malicious proposal and respond, either by mobilizing counter-votes or triggering an emergency cancel via a guardian role.

Is OpenZeppelin Governor enough, or do I still need additional defenses?

OpenZeppelin's Governor framework with ERC20Votes and TimelockController covers the core flash-loan vector well. But you still need to configure it correctly: a non-zero voting delay (at least 1 day), a meaningful timelock (2-7 days for treasury-controlling DAOs), and ideally a guardian multisig with veto power over the timelock for emergencies. The framework gives you the primitives; misconfiguration still kills you. Several post-Beanstalk forks shipped Governor with a 0-block voting delay and got drained anyway.

Can NFT-based governance be flash-loan attacked?

Yes, if the NFT is liquid on platforms like NFTfi or instantly purchasable via Sudoswap or Blur with flash loans. Several NFT DAOs have used `balanceOf(IERC721)` at vote time, which is exactly as exploitable as ERC20 balance-based voting. The fix is identical: snapshot ownership at a past block using ERC721Votes (or roll your own checkpoint logic), enforce a voting delay, and timelock execution.

What if my governance token isn't listed on any flash-loan provider?

You still have exposure. Attackers can flash-loan ETH or stablecoins, swap into your governance token through whatever DEX pool exists, vote, execute, swap back, and repay. The slippage cost is the attacker's only friction, and if your treasury exceeds the slippage by an order of magnitude — which it usually does — the attack is still profitable. Beanstalk's attack involved swapping flash-borrowed assets into BEAN and Stalk LPs, not borrowing Stalk directly.

How long should the timelock be?

For a DAO controlling under $1M, 24-48 hours is reasonable. Between $1M and $50M, use 3-5 days. Above $50M, 7 days minimum, and pair it with a guardian multisig that can cancel (but not execute) proposals during the timelock window. The timelock has to be long enough for the community to organize a response, including users withdrawing funds if the proposal turns out to be malicious. Compound and Uniswap both use multi-day timelocks for exactly this reason.

One Solidity tip + 1 case study per month

Flash-Loan Governance Attacks: How Beanstalk Lost $182M in One Block | Crypto Hawking