bEarn Fi bVaults BUSD/ibBUSD Denomination Mismatch / Leftover-Yield Inflation Drain (2021-05-16)
- Name: bEarn Fi bVaults BUSD/ibBUSD denomination mismatch / leftover-yield inflation drain
- Date: 2021-05-16
- Protocol: bEarn Fi
- Chain: BNB Smart Chain
- Category: cross-module asset-denomination mismatch / internal accounting inflation / vault drain
- Loss: about
10.86M BUSDdirectly to the attacker; about$17.9M-$18Mtotal vault damage including opportunistic user withdrawals and emergency exits before the system was fully contained - Summary: bEarn Fi’s BUSD single-stake bVault routed deposits through
BvaultsBankinto an Alpaca-basedBvaultsStrategythat treated the same numeric amount as two different assets in two different modules. The bank asked to withdraw_wantAmtin BUSD terms, while the strategy withdrew_wantAmtinibBUSDterms from Alpaca’s farm, even though eachibBUSDwas worth more than1 BUSD. The strategy then returned only the requested BUSD amount to the bank and left the extra BUSD behind inside the strategy contract. On the next deposit,_farm()treated that leftover balance as fresh strategy assets and added it intowantLockedTotal, so the vault’s internal accounting started recognizing phantom yield. By looping large flash-loan-funded deposits and withdrawals, the attacker kept inflatingwantLockedTotaland then withdrawing more value than had really been deposited, eventually draining the BUSD vault. - Trigger / entry point:
- The affected user-facing vault was bEarn Fi’s BUSD bVault bank at
0xB390B07fcF76678089cb12d8E615d5Fe494b01Fb. - The key strategy module was the BUSD Alpaca strategy at
0x21125d94Cfe886e7179c8D2fE8c1EA8D57C73E0e. - The strategy invested into Alpaca’s ibBUSD vault
0x7C9e73d4C71dae564d41F78d56439bB4ba87592fand staked via Alpaca FairLaunch0xA625AB01B08ce023B2a342Dbb12a16f2C8489A8F. - Public analyses trace the exploit through repeated flash-loan-funded deposit / withdraw loops, including transactions
0x603b2bbe2a7d0877b22531735ff686a7caad866f6c0435c37b7b49e4bfd9a36cand0x6bf610ecaf2f89f41bcad7aca4646199430839e7cf979fbcafa896e5126361d1.
- The affected user-facing vault was bEarn Fi’s BUSD bVault bank at
- Root cause:
BvaultsBankandBvaultsStrategydid not agree on the unit domain of_wantAmtduring withdrawal. The bank used BUSD-denominated amounts, while the strategy passed the same integer onward as if it were denominated inibBUSD.- Because
ibBUSDwas an interest-bearing receipt token worth more than1 BUSD, withdrawing_wantAmtibBUSD from Alpaca yielded more than_wantAmtBUSD. - The strategy’s
withdraw(address, uint256 _wantAmt)then transferred only_wantAmtBUSD back to the bank and silently kept the surplus BUSD balance locally. - On later deposits,
_farm()swept the full local BUSD balance into Alpaca and increasedwantLockedTotalby that full amount, causing bookkeeping to recognize the leftover surplus as legitimate vault yield. - Shortfall-tolerant accounting in
withdraw()masked inconsistencies instead of reverting: if the strategy did not have the exact amount its own accounting implied, it clipped_wantAmtto available balance rather than forcing the mismatch to surface.
- Exploit path:
- The attacker, using EOA
0x47f341d896b08daacb344d9021f955247e50d089and exploit contract0xef39F14213714001456E2E89EDdBDF8c850C3BE6, borrowed roughly7.8M BUSDfrom C.R.E.A.M.’s BUSD market0x2Bc4eb013DDee29D37920938B96d353171289B7C. - They deposited the flash-loaned BUSD into
BvaultsBank, which forwarded the assets into the Alpaca-backed strategy. - The strategy deposited BUSD into Alpaca, received fewer ibBUSD tokens than the raw BUSD number because ibBUSD had appreciated, and staked those ibBUSD shares in FairLaunch.
- The attacker then withdrew roughly the same numeric amount from the bVault. The bank interpreted that number as BUSD, but the strategy withdrew the same number of
ibBUSDshares from Alpaca. - Burning those
ibBUSDshares yielded more BUSD than the attacker had asked the bank to withdraw. The strategy returned only the requested BUSD amount and left the extra BUSD sitting in the strategy contract. - On the next deposit,
_farm()swept that leftover BUSD back into Alpaca and added it towantLockedTotal, so the bank’s share accounting started treating the leftover as earned yield attributable across shares. - Because
BvaultsBankusedwantLockedTotalto price user share value, the attacker could withdraw slightly more BUSD than they had really put in on each loop. - Repeating the loop dozens or hundreds of times with flash-loaned size turned the per-loop phantom yield into a full vault drain.
- The attacker, using EOA
- What the attacker needed to be true:
- The strategy had to use a receipt token with a non-1:1 exchange rate against the bank’s asset, here
ibBUSD > 1 BUSD. - The bank and strategy had to pass the same integer amount across their interface without converting units.
- The strategy had to return only the requested BUSD amount upstream while leaving the excess BUSD in its own balance instead of reconciling it back into the bank’s accounting.
_farm()andwantLockedTotalhad to treat leftover local balance as legitimate strategy principal or yield.- The attacker needed a large temporary BUSD balance so small percentage mismatches per loop became material profit.
- The strategy had to use a receipt token with a non-1:1 exchange rate against the bank’s asset, here
- Technical anatomy worth remembering:
- This was not an oracle attack and not a classic reentrancy bug. The core failure was dimensional: one module meant
BUSD, another meantibBUSD, and both accepted the same integer as if the unit were obvious. - The strategy’s accounting variable
wantLockedTotalwas not recomputed from live underlying value in Alpaca; it was incrementally updated from what the strategy thought it had deposited and withdrawn. That made leftover balance look like real earned yield. - The masking behavior in strategy withdrawal mattered. A design that reverted on accounting mismatch would have broken loudly; instead, clipping
_wantAmtto available balance let the vault continue operating while its internal state drifted further from reality. - The exploit demonstrates how vault stacks can fail at module boundaries even when each component appears locally reasonable. The dangerous edge was the interface between bank accounting, strategy accounting, and receipt-token semantics.
- This was not an oracle attack and not a classic reentrancy bug. The core failure was dimensional: one module meant
- Representative transactions and addresses:
- Attacker EOA:
0x47f341d896b08daacb344d9021f955247e50d089 - Attacker contract:
0xef39F14213714001456E2E89EDdBDF8c850C3BE6 - Representative exploit tx:
0x603b2bbe2a7d0877b22531735ff686a7caad866f6c0435c37b7b49e4bfd9a36c - Representative exploit tx:
0x6bf610ecaf2f89f41bcad7aca4646199430839e7cf979fbcafa896e5126361d1 - bVault bank:
0xB390B07fcF76678089cb12d8E615d5Fe494b01Fb - BUSD Alpaca strategy:
0x21125d94Cfe886e7179c8D2fE8c1EA8D57C73E0e - Alpaca ibBUSD vault:
0x7C9e73d4C71dae564d41F78d56439bB4ba87592f - Alpaca FairLaunch:
0xA625AB01B08ce023B2a342Dbb12a16f2C8489A8F - C.R.E.A.M. BUSD market used for flash liquidity:
0x2Bc4eb013DDee29D37920938B96d353171289B7C
- Attacker EOA:
- Response / mitigation:
- bEarn disabled or contained the affected vault and later said only the single-stake BUSD bVault using the Alpaca strategy was impacted.
- The team said future new bVaults would launch with deposit caps until the corresponding strategy had passed a full audit.
- bEarn later published a compensation plan using remaining funds, DAO funds, and other project resources, and public summaries note that affected users eventually received staged compensation claims.
- The incident also highlighted secondary loss amplification: after the exploit began, other users withdrew at the distorted rate or used
emergencyWithdraw, which increased realized damage beyond the attacker’s direct take.
Why this incident matters
- bEarn is a clean example of how a vault can manufacture fake yield without any external price manipulation: the bug lived entirely inside protocol-controlled accounting.
- It preserves a recurring lesson that smart-contract interfaces need explicit unit semantics. Passing
amountacross module boundaries is unsafe when one side means underlying and the other means receipt shares. - It also shows why internal totals such as
wantLockedTotalshould be derivable from live positions rather than only from increment/decrement bookkeeping. If the bookkeeping goes wrong once, the entire strategy can start compounding its own error. - The sharper healthy-side boundary is that
the strategy contract holds more BUSD than beforeshould not have meantexisting vault shares legitimately earned more backing. bEarn needed a reconciliation boundary between seam-generated leftovers and canonical vault yield.
Three seam-authority promotions this incident collapsed
-
A seam-crossing amount existed between
BvaultsBankandBvaultsStrategy- The bank passed
_wantAmtacross the withdrawal boundary expecting a BUSD-denominated request. - At this stage the safest truthful statement should only have been
the caller wants this much BUSD out. That did not yet mean the strategy was free to spend the same raw integer asibBUSDshares.
- The bank passed
-
bEarn treated that raw amount as canonical cross-module value truth
BvaultsStrategyreused_wantAmtas if it already meant the same economic object inside Alpaca’sibBUSDdomain.- Once the strategy redeemed
_wantAmtibBUSDshares but the bank still settled_wantAmtBUSD, the protocol effectively canonized one seam mismatch into local accounting truth instead of forcing reconciliation.
-
That accepted seam-authored surplus still inherited vault-yield and withdrawal authority
- The extra BUSD left behind in the strategy did not stay quarantined as evidence of accounting disagreement.
_farm()swept it back in,wantLockedTotalrecognized it as real backing, and vault share pricing plus later withdrawals spent against that fiction as if it were earned yield.
Pattern tags:
- Underlying-versus-receipt mismatch:
BvaultsBankrequested withdrawal in BUSD terms whileBvaultsStrategyredeemed the same numeric amount asibBUSDshares from Alpaca. - Leftover-balance recycling: excess BUSD created by the seam stayed inside the strategy, then
_farm()treated that residue as fresh depositable assets and increasedwantLockedTotal. - Incremental-bookkeeping drift: the vault trusted strategy-local counters instead of proving live underlying value after each deposit / withdrawal round trip.
- Silent-shortfall masking: withdrawal logic clipped to available balance instead of reverting on accounting disagreement, so the mismatch stayed latent and compounding.
- Design companions: cross-module-asset-denomination-mismatch, reserve-backed-claim-truth-security-object, and unexpected-balance-quarantine-and-explicit-recognition-discipline.
Why the exploit worked
- The bank-strategy seam had no explicit dimensional contract. A raw
uint256 _wantAmtcrossed the boundary without proving whether it meantBUSD assetsoribBUSD shares, so one side could honestly ask for one thing while the other honestly executed another. - Alpaca’s
ibBUSDexchange rate had already moved above 1, which turned a semantic mismatch into real economic surplus on every large withdrawal. - That surplus did not stop at the seam. The strategy kept the extra BUSD locally, and later
_farm()reintroduced the residue as if it were legitimate principal or earned yield, effectively canonizing interface error into vault PPS. - In other words, bEarn collapsed three different approvals into one flow:
the bank requested assets,the strategy interpreted the same integer as shares, andthe resulting leftover still deserved canonical yield and withdrawal authority. wantLockedTotalwas maintained incrementally rather than continuously reconciled against live underlying value, so once the first mismatch landed, subsequent loops amplified the accounting fiction instead of washing it out.- The strategy’s shortfall-tolerant withdrawal behavior prevented the system from failing loudly. Clipping and continuing preserved liveness while letting the attacker compound the same seam mistake dozens of times under flash-loan size.
- The attacker did not need market manipulation or privileged access. They only needed large temporary BUSD inventory and a repeatable route through a vault stack that reclassified seam-created leftovers as real backing.
Detection and audit pivots
- Annotate every bank / strategy / adapter interface with units and review whether each
amountmeans underlying assets, receipt shares, staked shares, or scaled accounting units on both sides of the call. - Replay deposit-then-withdraw round trips after the receipt token has appreciated meaningfully above 1 and check whether unexplained residue remains in the strategy contract.
- Reconcile
wantLockedTotalor equivalent internal totals against live underlying value in the external vault after every withdrawal path, not only at happy-path deposit checkpoints. - Treat intermediate-contract leftovers as hostile accounting evidence. If strategy-local balance increases after a supposed exact withdrawal, ask whether that residue came from earned yield or from a unit-domain mistake.
- Search for
min(balance, requestedAmount)and similar shortfall-clipping patterns on solvency boundaries. Those branches often hide the first visible symptom of denomination drift. - Fuzz large-loop behavior with flash-loan-sized capital. Small per-loop surpluses that look harmless in unit tests can become full drains once the protocol lets the attacker recycle them through price-per-share accounting.
- Verify that emergency withdrawal, user withdrawal, and strategy unwind paths all consume the same valuation and denomination semantics. If one path settles in assets and another in receipt shares, the seam needs explicit conversion or it is already unsafe.
Failure-prevention checklist
- Make the bank / strategy seam unit-explicit in code.
amountshould never cross as an untyped integer when one side meansBUSD assetsand the other can act onibBUSD shares. - Convert exactly once at the boundary using the live wrapper exchange rate. If the bank requests assets, the strategy should compute the necessary share burn explicitly instead of reusing the same raw integer downstream.
- Recompute strategy solvency from live positions after deposit and withdrawal paths.
wantLockedTotal-style counters should be reconciliation outputs, not the only source of truth for PPS. - Quarantine unexpected strategy-local balances. Leftover BUSD after an ostensibly exact withdraw should enter a reconciliation bucket or revert the call, not get auto-swept into
_farm()as presumptive earned yield. - Revert on denomination disagreement or shortfall instead of clipping to current balance. Silent
min(balance, requestedAmount)behavior turns a loud seam failure into compounding accounting drift. - Add round-trip tests after receipt-token appreciation. Deposit / withdraw loops should be exercised when
ibBUSDis materially above 1, with large sizes and repeated cycles, not only at a fresh 1:1 bootstrap state. - Keep emergency, normal withdrawal, and harvest paths on one valuation story. Any path that settles in assets while another path reasons in receipt shares needs the same explicit conversion and reconciliation boundary.
- Treat wrapper integrations as balance-sheet migrations, not routing glue. Any adapter using interest-bearing or appreciating receipt tokens should get dimensional-analysis review before production and after wrapper changes.
Audit prompts
- What exact unit does
_wantAmtrepresent on each side of theBvaultsBank -> BvaultsStrategyinterface, and where is that meaning enforced in code rather than assumed socially? - If the bank asks for
XBUSD, what exact object is the strategy redeeming downstream:XBUSD of underlying,XibBUSDshares, or whatever balance happens to be locally available? - After a full deposit / withdraw round trip, can any unexplained BUSD residue remain inside the strategy contract, and what code path decides whether that residue is reconciliation evidence or yield?
- Is
wantLockedTotalderived from live Alpaca position value plus local balances, or merely incremented and decremented from prior assumptions that can preserve one old seam mistake indefinitely? - Which branches clip, floor, or tolerate shortfall instead of reverting on accounting disagreement, and could those branches hide the first visible symptom of denomination drift?
- If
ibBUSDappreciates further, does the same loop become more profitable automatically, and has the protocol tested that exchange-rate sensitivity under flash-loan-sized repetition? - Can emergency withdrawal, strategy unwind, or harvest paths reuse the same ambiguous
amountsemantics under different code paths? - If a reviewer looked only at transfer logs, would they still notice that the strategy redeemed and re-staked more economic value than the bank believed it was moving?
Lessons that should already be standard
- Internal module seams can mint fake yield without any oracle manipulation, governance abuse, or reentrancy. A dimensional-analysis mistake inside protocol-authored accounting is enough.
- A strategy-local leftover is not free alpha. Unexpected residue after an exact withdrawal is evidence that one solvency-critical interpretation of value has already diverged from another.
- Incremental totals over wrapper positions are brittle unless live reconciliation stays close. Once the protocol starts compounding its own bookkeeping fiction, every later honest action helps crystalize the theft.
- Wrapper and receipt-token integrations are valuation boundaries, not just routing convenience.
Underlying amountandreceipt-share amountmust be treated as different security objects until explicitly converted. - Silent clipping on solvency boundaries is often worse than a revert. It preserves apparent liveness by hiding the mismatch the attacker most needs to repeat.
- Flash loans matter here not because they create the bug, but because they let attackers industrialize a small per-loop accounting seam until it becomes treasury-scale loss.
The strategy holds more tokens nowwas never the same thing asexisting vault shares earned more backing. Recognition and attribution needed a stronger boundary than raw local balance growth.
Internal linkages
- Core recurring pattern: cross-module-asset-denomination-mismatch.
- Adjacent pattern: unit-normalization-failure — same high-level dimensional-analysis lesson, but that note is oracle-focused while bEarn shows the same class inside vault / strategy accounting.
- Adjacent pattern: donation-share-price-manipulation — both families turn accounting inflation into withdrawable value, but here the printed value came from cross-module denomination confusion and leftover-balance recycling rather than an explicit donation path.
- Adjacent incident: spartan-protocol-current-balance-liquidity-share-inflation-drain-2021-05-01 — another case where vault / pool share accounting treated manipulated balance state as legitimate withdrawable value.
- Adjacent incident: meta-pool-mpeth-erc4626-mint-without-asset-transfer-unbacked-mint-pool-drain-2025-06-17 — a different path to the same broader lesson that issuance and accounting primitives cannot safely assume receipt semantics stayed intact across module boundaries.
Control surface
-
The decisive leverage sat in the
BvaultsBank -> BvaultsStrategyinterface, the meaning of_wantAmt, the strategy’swantLockedTotalbookkeeping, and the decision to recycle strategy-local leftovers through_farm()as if they were earned assets. -
The sharp review split is three-layered here too:
a BUSD-denominated request crossed the seam,the strategy treated the same integer as canonical ibBUSD-side value truth, andthe resulting surplus still inherited vault-yield and withdrawal authority. -
The protocol did not fail because Alpaca mispriced anything. It failed because bEarn’s own modules disagreed about what asset an
amountreferred to, then compounded that mistake into share value. -
Sources:
- https://peckshield.medium.com/bearn-fi-incident-inconsistent-asset-denomination-between-vault-strategy-9b24b68ab1c0
- https://inspexco.medium.com/bearn-fi-incident-analysis-bvaults-improper-withdrawal-amount-c074ec8c29e7
- https://raw.githubusercontent.com/OriginProtocol/security/master/incidents/2021-05-16-BearnFi.md
- https://github.com/bearn-defi/bvaults-smartcontracts
- https://www.quadrigainitiative.com/casestudy/bearnfibvaultsexploited.php
- https://bscscan.com/tx/0x603b2bbe2a7d0877b22531735ff686a7caad866f6c0435c37b7b49e4bfd9a36c
- https://bscscan.com/tx/0x6bf610ecaf2f89f41bcad7aca4646199430839e7cf979fbcafa896e5126361d1
-
Last reviewed: 2026-07-31 UTC