All audits
FailedEthereumIndependent

Beanstalk Governance (2022)

Smart contract security audit by TierZero · 2023-04-15

Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.

Findings summary

1
Critical
2
High
2
Medium
1
Low
1
Informational

Independent research — not a commissioned audit.

What the protocol did

Beanstalk was a credit-based stablecoin protocol on Ethereum. Its token, BEAN, targeted a $1 peg not through overcollateralized vaults but through an algorithmic incentive system: users deposited whitelisted assets (BEAN, BEAN:ETH and BEAN:3CRV LP tokens, and others) into a shared pool called the Silo and received Stalk and Seeds in return. Stalk was Beanstalk's governance weight — it grew the longer a deposit sat in the Silo — and it doubled as voting power over Beanstalk Improvement Proposals (BIPs), the mechanism used to change protocol parameters, whitelist assets, or move treasury funds.

Governance was on-chain and self-executing. A BIP needed a supermajority of outstanding Stalk to pass, and the contract exposed an emergencyCommit path that let a proposal execute the moment it crossed a two-thirds threshold, rather than waiting out the normal voting window. That design decision — voting weight that can be acquired instantly, wired directly to an execution path with no delay — is the entire postmortem in one sentence.

The vulnerability

The bug class is flash-loan-financed governance takeover combined with a timelock-free execution path. Two properties have to both be true for this to be catastrophic:

  1. Voting power is a function of a spot token balance (or a balance with no vesting/lock beyond a single block), so it can be rented for the duration of one transaction.
  2. The contract allows a proposal to execute as soon as a quorum threshold is met, without a mandatory delay between "vote passes" and "vote executes."

Illustrative pattern (simplified from Beanstalk's actual flow):

function emergencyCommit(uint256 bipId) external {
    uint256 votes = getStalkVotedFor(bipId);
    uint256 totalStalk = totalStalkSupply();

    require(votes * 3 >= totalStalk * 2, "quorum not met"); // 2/3 supermajority
    _execute(bipId); // executes immediately, same transaction
}

Nothing here checks when the voting Stalk was acquired or requires it to have existed before the proposal was created. A snapshot-based design (voting power fixed at proposal-creation block) would have neutralized this; a same-transaction balance check does not. This is the same root cause we flag whenever governance weight, staking rewards, or reward-distribution logic reads balanceOf() or an equivalent live balance instead of a checkpointed one — it's structurally identical to flash-loan price-oracle manipulation, just applied to a voting oracle instead of a price oracle.

How the exploit worked

  1. The attacker used flash loans from Aave, Uniswap, and SushiSwap to borrow roughly $1 billion in stablecoins (DAI, USDC, USDT) in a single transaction.
  2. That capital was swapped into the Silo's whitelisted deposit assets and deposited into Beanstalk, instantly minting a supermajority of Stalk — the attacker briefly controlled roughly two-thirds of all governance weight.
  3. In the same transaction, the attacker submitted a BIP that had been pre-staged to look like a charitable donation proposal (funding Ukraine relief) but contained an additional rider transferring Silo-held collateral to a contract the attacker controlled.
  4. emergencyCommit fired immediately because quorum was met, executing the malicious payload with no timelock and no human review window.
  5. The attacker's contract swept roughly $182 million in deposited assets, swapped them back through the same DEX pools, repaid the flash loans, and kept the remainder — net profit was in the neighborhood of $80 million, with a small fraction (roughly $250K) routed to an actual Ukraine donation address, apparently to complicate the narrative.
  6. The Silo withdrew liquidity, and the entire sequence completed atomically in one block; there was no intervention window because none existed by design.

How an audit catches this

This is a governance-model review, and it's one of the first things we check on any protocol where voting weight derives from a token balance. Concretely:

  • Trace every path that reads voting power and confirm it uses a checkpoint/snapshot (block-number- or timestamp-indexed) rather than a live balanceOf()-style read. If it doesn't, that's a finding regardless of whether an emergencyCommit-style fast path exists.
  • Enumerate every execution path from "proposal passes" to "proposal executes" and require a minimum timelock on all of them, including any "emergency" or fast-track branch — fast paths are exactly where this bug hides because they're added later and reviewed less.
  • Model the cost of acquiring supermajority voting weight via flash loan against the value the DAO/treasury controls; if borrowing cost is near zero and the payoff exceeds it, the governance design is unsound independent of any other bug.
  • Write an invariant test that mints flash-loan-scale capital, deposits it, and attempts to pass and execute a proposal within one transaction — this is the kind of scenario we build into the test suite during a full engagement, alongside the fuzzing and invariant work we describe in our write-up on formal verification and property-based testing for trading program security.
  • Review any allow/approve-style delegation the same way — the underlying failure mode (unlimited authority granted on a live, spoofable balance) is the same one we cover when discussing approval-based attack surface in EVM contracts.

Remediation

The fix is structural, not a one-line patch:

  • Snapshot voting power at proposal creation (Compound-style checkpoints), so capital acquired after a proposal exists cannot vote on it.
  • Enforce a mandatory timelock between quorum being reached and execution, with no bypass — if an emergency path is required for genuine crises, gate it behind a separate, slower-moving multisig or council rather than raw token-weighted voting.
  • Cap how much of total voting supply a single transaction can newly mint or acquire, or require voting tokens to be locked for a minimum duration before they count toward quorum.
  • Add circuit breakers on Silo withdrawals so a single-block, single-transaction full drain isn't mechanically possible even if governance is compromised.

Governance logic gets far less scrutiny than pricing and accounting code, and that's exactly why it's a recurring source of nine-figure losses. If your protocol has any on-chain governance path — even one gated behind "emergency" naming — it belongs in scope for a full smart contract audit, not a quick read-through.

Need your contracts audited?

Manual review + tooling across TON, Solana and EVM — certificate and full report included.

Get an audit