Reproduced Exploit
Beanstalk BIP-39 upgrade omits a modified facet, inflating grown Stalk 1,000,000x
Chain
Other
Category
dependency
Date
Dec 2023
Source
AuditVault
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. Reproduction of a public audit finding curated by AuditVault — the original finding: 31275-failure-to-add-modified-facets-and-facets-with-modified-depe. Standalone Foundry PoC and full write-up: 31275-failure-to-add-modified-facets-and-facets-with-modified-depe_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/dependency/upgradeable-contract · vuln/logic/upgrade-safety · misassumption/external-call-is-safe
Reproduction: the test deploys the REAL Beanstalk EIP-2535 diamond (real
Diamond/LibDiamond/DiamondCutFacet/DiamondLoupeFacet/OwnershipFacet), the REALAppStoragestorage layout, and the REAL stem/stalk library math from both audited commits, then performs the BIP-39diamondCutthat omits re-cuttingSiloFacet. A pre-upgrade deposit's claimable grown Stalk jumps from 20 to 20,000,000 — an exact 1e6x over-issuance.
Root cause#
Beanstalk is an EIP-2535 Diamond. At upgrade time, modified facets are re-cut by listing them in the relevant function of protocol/scripts/bips.js. BIP-39 (Seed Gauge) changed the stem-tip scale convention inside LibTokenSilo, but the bipSeedGauge upgrade script fails to re-cut SiloFacet, FieldFacet, BDVFacet, ConvertFacet, WhitelistFacet, and every facet whose library dependencies changed.
Because Solidity internal libraries are inlined into each facet's bytecode, a facet that is not re-cut keeps executing the old library code even after the diamond's storage is rescaled by the newly-cut facets. The two conventions are irreconcilable:
- Pre-upgrade (
LibTokenSilo.stemTipForToken@ 76066733) addss.ss[token].milestoneStemun-divided (truncated scale), and the milestone writer storesmilestoneStemtruncated. - BIP-39 (
LibTokenSilo@ dfb418d) introducesstemTipForTokenUntruncated, storesmilestoneStemuntruncated (~1e6x larger), and divides the whole sum by1e6.
After the buggy upgrade the diamond still routes stemTipForToken / balanceOfGrownStalk to the stale V1 SiloFacet. Its inlined V1 math reads the freshly rescaled (untruncated) milestoneStem and adds it without dividing, so the returned stem tip — and therefore the grown Stalk of every pre-upgrade deposit — is inflated by ~1e6x. Stalk is Beanstalk's governance-and-yield token; over-issuing it breaks protocol accounting and lets pre-upgrade depositors Mow far more Stalk than intended.
The vulnerable, un-divided add is at SiloExit.stemTipForToken → LibTokenSilo.stemTipForToken; the milestone-writer divergence is in LibWhitelist.updateStalkPerBdvPerSeasonForToken.
What is real in this reproduction#
Deployed unmodified from the audited commits:
- Real EIP-2535 diamond:
Diamond.sol,LibDiamond.sol,DiamondCutFacet.sol,DiamondLoupeFacet.sol,OwnershipFacet.sol(src/beanstalk,src/libraries,src/interfaces). - Real
AppStorage.sol/ReentrancyGuard.sol/LibAppStorage.sol— the actual Beanstalk storage layout, sos.ss[token].milestoneStem,s.season.currentands.a[account].mowStatuses[token]sit at their real slots. - Real library math, byte-identical function bodies (library symbols suffixed
V1/V2only so both commit versions coexist in one compilation unit):LibTokenSilo.stemTipForToken(Untruncated),LibSilo.stalkReward/_balanceOfGrownStalk,LibWhitelist.updateStalkPerBdvPerSeasonForToken. - Real
SiloExit.stemTipForToken/balanceOfGrownStalkbodies, exposed viaSiloViewFacetV1/SiloViewFacetV2.
SetupFacet (whitelist + pre-upgrade deposit + season) reproduces the state that InitBipNewSilo and a real deposit/mow leave; it contains no vulnerability logic.
Exploit walkthrough (with numbers)#
Whitelisted token BEAN, stalkEarnedPerSeason = 2e6, deposit bdv = 1000e6, milestone at season 100, current season 200.
- Pre-upgrade (correct): V1
stemTipForToken = 0 + (2e6·100)/1e6 = 200. Grown Stalk= (200 − 0)·1000e6 = 2e11→ 20 Stalk. - BIP-39 buggy upgrade:
diamondCutreplacesWhitelistFacetwith V2 but does not re-cutSiloFacet. - Post-upgrade milestone write: V2
updateStalkPerBdvPerSeasonForTokenstoresmilestoneStem = stemTipForTokenUntruncated = 2e6·100 = 2e8(untruncated). - Harm: the stale V1
balanceOfGrownStalkreadsmilestoneStem = 2e8and adds it un-divided →stemTip = 2e8. Grown Stalk= (2e8 − 0)·1000e6 = 2e17→ 20,000,000 Stalk.
Backward-compatibility invariant broken by exactly 1e6x. The control run — which re-cuts SiloFacet to V2 — returns 2e11 (20 Stalk), confirming the omission is the sole cause.
Reproduction#
_shared/run-poc/run_poc.sh 31275-failure-to-add-modified-facets-and-facets-with-modified-depe_exp -vvvvv
Expected result: 1 passed. The assertions in test/31275-failure-to-add-modified-facets-and-facets-with-modified-depe_exp.sol verify the pre-upgrade baseline (2e11), the untruncated milestone rescale (2e8), the inflated post-upgrade value (2e17 = grownStalkBefore·1e6), and that the control upgrade which re-cuts SiloFacet stays backward-compatible (2e11).
Sources#
- AuditVault finding #31275
- Cyfrin Beanstalk BIP-39 report
- Pre-upgrade Beanstalk source
76066733 - BIP-39 Beanstalk source
dfb418d
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 31275-failure-to-add-modified-facets-and-facets-with-modified-depe_exp (evm-hack-registry mirror).
- AuditVault finding: 31275-failure-to-add-modified-facets-and-facets-with-modified-depe.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Beanstalk BIP-39 upgrade omits a modified facet, inflating grown Stalk 1,000,000x".
- 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.