Reproduced Exploit
Beanstalk Farms Exploit — Flash-Loan Governance Self-Pass
Beanstalk's governance let any Bean depositor vote on Beanstalk Improvement Proposals (BIPs) with voting weight equal to their Stalk (deposit). Critically:
Loss
~$182M (non-Bean assets: USDC, USDT, DAI, 3Crv, etc.) drained after the attacker passed a malicious BIP
Chain
Ethereum
Category
governance
Date
Apr 2022
Source
DeFiHackLabs
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Exploit reproduction, trace data, and analysis adapted from DeFiHackLabs by SunWeb3Sec — an open registry of reproduced on-chain exploits. Standalone Foundry PoC and full write-up: 2022-04-Beanstalk_exp in the
evm-hack-registrymirror. Upstream DeFiHackLabs PoC:src/test/…/Beanstalk_exp.sol.
Vulnerability classes: vuln/governance/flash-loan-voting · vuln/governance/timelock-bypass
Reproduction: the PoC compiles in an isolated Foundry project at this project folder. Full verbose trace: output.txt. Verified vulnerable source: Beanstalk Diamond, Bean, Bean/3Crv metapool (Vyper).
Key info#
| Loss | ~$182M (non-Bean assets: USDC, USDT, DAI, 3Crv, etc.) drained after the attacker passed a malicious BIP |
| Vulnerable contract | Beanstalk Diamond — 0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5 |
| Attacker EOA | 0x1da5… (Aave flash-loan funded) |
| Attack contract | 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 (the malicious _init / proposal contract in the trace) |
| Attack tx | 0xcd314668aaa9bb442bf1d273f1d1c8a7a47ef0f1d01d6c2030b0ff4af2da1198 |
| Chain / block / date | Ethereum mainnet / 14,595,905 / Apr 17, 2022 |
| Compiler | 0.7.6 (from Diamond source) |
| Bug class | Governance hijack via flash-loan instant Stalk + unrestricted Diamond _init delegatecall |
TL;DR#
Beanstalk's governance let any Bean depositor vote on Beanstalk Improvement Proposals (BIPs) with voting weight equal to their Stalk (deposit). Critically:
- Stalk was granted instantly on deposit with no lockup or vesting.
- BIPs could be emergency-committed in the same transaction once the Stalk threshold was met (no timelock for "emergency" BIPs).
- A BIP with empty diamondCut but a non-zero _init + _calldata caused the Diamond to perform
_init.delegatecall(_calldata)in the Diamond's own context (full control over all token balances held by the protocol).
The attacker took a ~$1B Aave flash loan, converted it into Bean/3Crv LP tokens via Curve, deposited the LP to mint a majority of Stalk in one block, proposed BIP #18 pointing at their own contract as the _init with calldata to a sweep() that drained the reserves, emergency-committed it, and walked away with ~$182M.
Background — what Beanstalk does#
Beanstalk is a decentralized stablecoin protocol on Ethereum. Its core "Bean" token is stabilized against a basket of stablecoins through a complex Silo deposit / Stalk / Seed governance and incentive system.
- Users deposit Beans or LP tokens (Bean/3Crv) into the Silo.
- Deposits mint Stalk (governance voting power + yield share) and Seeds (future Stalk growth).
- Stalk is used to vote on Beanstalk Improvement Proposals (BIPs).
- The protocol is implemented as a Diamond (EIP-2533) proxy at 0xC1E088fC... with multiple facets. Governance BIPs can add/replace facets or execute arbitrary init code.
The Diamond's fallback delegates calls to facets. Governance has a special path that allows a passed BIP to execute _init.delegatecall(_calldata) without going through the normal owner-only DiamondCut.
The vulnerable code#
From the PoC header comment (exact root cause description):
// VULNERABILITY: Governance Hijack via Flash-Loan Instant Stalk + Unrestricted Diamond Init Delegatecall
// Root Cause:
// Beanstalk uses a Diamond proxy (EIP-2533) for its core logic at 0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5.
// Governance is controlled by "Stalk" (and roots) which are credited INSTANTLY when a user calls
// depositBeans() or deposit(token, amount) on the SiloV2Facet.
// There is no lockup, no vesting period, no minimum deposit time — Stalk is available for voting the same block.
// A passed BIP is executed via:
// beanstalkgov.emergencyCommit(bip) --> internally invokes a diamondCut-like path with the BIP's
// stored (diamondCut, _init, _calldata)
// When _diamondCut is empty and _init + _calldata are provided, the Diamond's fallback + LibDiamond.initializeDiamondCut
// performs: _init.delegatecall(_calldata) IN THE CONTEXT OF THE DIAMOND (so address(this)==Diamond, full storage+balances).
The delegatecall site (verified source):
// sources/Diamond_C1E088/contracts_libraries_LibDiamond.sol:220
(bool success, bytes memory error) = _init.delegatecall(_calldata);
if (success == false) {
if (error.length > 0) {
revert(string(error));
} else {
revert("LibDiamondCut: _init function reverted");
}
}
Diamond fallback (verified):
The fallback in Diamond.sol delegates msg.sig to the registered facet. Governance BIPs bypass normal access control by using the stored BIP data directly.
Silo deposit (instant Stalk):
depositBeans / deposit immediately updates the caller's s.stalk and roots in AppStorage with no time delay.
Root cause#
- Instant Stalk on deposit —
depositBeansmutates governance weight synchronously. - Flash-loanable voting power — no lockup/vesting on Stalk.
- Emergency commit with no timelock — once Stalk threshold met,
emergencyCommitexecutes immediately. - Unrestricted
_initdelegatecall in the BIP execution path — the Diamond runs attacker code withaddress(this) == Diamond, giving it full access to every ERC20 balance the protocol holds. - No sanity checks on the BIP's
_initor_calldatafor governance-initiated cuts.
Preconditions#
- Permissionless deposit into the Silo.
- A flash-loan provider with sufficient stablecoin liquidity (Aave).
- A Curve metapool that accepts the flash-loaned assets and emits an accepted Silo deposit token (Bean/3Crv LP).
- Governance threshold for emergencyCommit reachable with flash-loan capital in one block.
Attack walkthrough (with on-chain numbers from the trace)#
From output.txt (the canonical offline trace):
-
Initial seed (75 ETH → Bean):
uniswapv2.swapExactETHForTokens{value: 75 ether}→ receives 217860949888 Bean (~217.86k Bean).- Logs: "After initial ETH -> BEAN swap, Bean balance of attacker: 217860"
-
Small deposit for initial Stalk:
Bean.approve(Silo, max)siloV2Facet.depositBeans(217860949888)- Emits BeanDeposit, updates stalk/roots.
- Logs: "After BEAN deposit to SiloV2Facet, Bean balance of attacker: 0"
-
Propose malicious BIP #18:
_diamondCut = []_init = address(this)(the PoC contract)_calldata = 0x35faa416(selector forsweep())_pauseOrUnpause = 3beanstalkgov.propose(...)- Storage writes record the BIP at index 18, proposer, etc.
- Emits relevant events.
-
Warp (in original PoC for timing, not strictly needed for emergency):
cheat.warp(block.timestamp + 24*60*60)
-
Massive approvals for flash + Curve + Silo.
-
Aave flashLoan:
- 350M DAI + 500M USDC + 150M USDT (exact from trace).
- Calls
executeOperationon the contract.
-
Inside executeOperation (the real power acquisition):
- Add liquidity to 3Crv pool.
- Add liquidity to Bean/3Crv metapool → mint crvbean LP.
siloV2Facet.deposit(crvbean, balance)→ massive Stalk mint.beanstalkgov.emergencyCommit(18)- This triggers the stored BIP's delegatecall.
-
The delegatecall drain:
- Inside the Diamond context, the attacker's
sweep()(or the real sweep in the tx) transfers the protocol's USDC, DAI, USDT, 3Crv, etc. - Logs show balance movements.
- Attacker ends with huge balances, repays flash + premium, keeps the profit.
- Inside the Diamond context, the attacker's
From logs in trace:
- Multiple
log_named_uintfor balances before/after. - The flashLoan call is huge (350e24 etc in the call data).
Profit accounting (from real exploit): ~$182M net after repaying the flash loan.
Diagrams#
Remediation#
- Add a mandatory timelock between BIP proposal passing and execution.
- Introduce lockup/vesting or time-weighted Stalk for governance power.
- Restrict the
_init/ calldata that BIPs can execute (whitelist or require diamondCut only). - Require higher quorum or supermajority for governance actions that move funds or change code.
- Use TWAP or other manipulation-resistant metrics for voting power instead of instantaneous balance.
How to reproduce#
The PoC runs fully OFFLINE via the shared anvil harness from the committed anvil_state.json:
_shared/run_poc.sh 2022-04-Beanstalk_exp -vvvvv
- Forks mainnet at block 14,595,905.
- Expected: runs the initial swap, deposit, propose, flashLoan setup, Curve ops, deposit for Stalk, emergencyCommit, and the delegatecall path.
- Note: the local run reverts at the end (as documented in the PoC header and output.txt header) because the isolated fork does not perfectly reproduce every side effect of the original Aave/Curve/Diamond state at that exact moment. The exploit logic and root cause are fully visible in the sources and the on-chain transaction.
The real attack succeeded on mainnet for ~$182M.
Reference: Beanstalk Farms flash-loan governance attack, Apr 17 2022 (~$182M).
Sources (registry as source of truth)
- PoC:
test/Beanstalk_exp.sol(detailed comments) and cleanedtest/2022-04-Beanstalk.sol - Trace:
output.txt - Verified contracts:
sources/Diamond_C1E088/,sources/Bean_DC59ac/,sources/Vyper_contract_3a70Df/ - LibDiamond delegatecall at line 220.
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 2022-04-Beanstalk_exp (evm-hack-registry mirror).
- Upstream DeFiHackLabs PoC:
Beanstalk_exp.sol. - Attack transaction: view on explorer.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Beanstalk Farms Exploit".
- Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.