Belt Finance StrategyAlpaca balanceSnapshot Undercount / Share-Overmint Near Miss (2021-08-10)
- Name: Belt Finance StrategyAlpaca
balanceSnapshotundercount / share-overmint near miss - Date: 2021-08-10
- Protocol: Belt Finance
- Chain: BNB Chain
- Category: issuance / redemption solvency asymmetry / direct-transfer state desynchronization / share overmint / whitehat near miss
- Loss: no realized theft; Immunefi’s review estimated roughly
$60Mof BNB-denominated value at risk, with about$10Mdrainable per attack iteration before repetition - Summary: Whitehat Alexander Schlindwein found that Belt’s Alpaca strategy contracts could be tricked into understating their own assets and then overminting strategy shares. The bug sat in
StrategyAlpacaImpl.withdraw(): the contract always subtracted the requested withdrawal amount frombalanceSnapshot, even when no strategy position had actually been unwound because enoughwantalready sat idle in the contract. An attacker could first send wrapped BNB directly to the strategy contract sowantLockedInHere()looked artificially high, then withdraw shares to trigger the bad accounting branch, making Belt think the strategy held less than it really did. A later deposit minted too many strategy shares against that understated total,earn()refreshed the snapshot back to reality, and the attacker could then redeem the overminted shares for an unfair share of the vault. - Trigger / entry point:
- The vulnerable logic lived in Belt’s BNB-chain Alpaca strategy implementation,
StrategyAlpacaImpl, in thewithdraw(uint256 _wantAmt)path. - Belt strategy tokens minted shares against
wantLockedTotal(), which depended on both idlewantLockedInHere()balance and the strategy-sidebalanceSnapshot. - The attacker could transfer
wanttokens directly to the strategy contract without going through Belt’s accounting flow. - That direct transfer changed whether
withdraw()thought it needed to call_withdraw(...), but the contract still decrementedbalanceSnapshotas if a strategy unwind had happened.
- The vulnerable logic lived in Belt’s BNB-chain Alpaca strategy implementation,
- Root cause:
- Belt relied on a cached strategy-balance variable,
balanceSnapshot, and assumed withdrawal accounting would only ever be driven by internal asset flows. withdraw()subtracted_wantAmtfrombalanceSnapshotunconditionally, even when the withdrawal had been satisfied entirely from idle balance and_withdraw(...)had not reduced strategy exposure.- A direct token transfer to the strategy contract let an attacker control that branch condition and create a gap between
actual assetsandaccounted assets. - Once total assets were understated, Belt’s share-minting logic sold claims too cheaply, while the redemption side still honored those claims against the strategy’s real assets after
earn()recomputed the snapshot.
- Belt relied on a cached strategy-balance variable,
- Exploit path:
- The attacker deposits BNB / WBNB through Belt normally and receives strategy shares.
- The attacker sends WBNB directly to the strategy contract instead of using Belt’s tracked deposit path, temporarily increasing
wantLockedInHere(). - The attacker withdraws shares. Because the direct transfer makes
wantLockedInHere()large enough, theif (_wantAmt > wantLockedInHere())branch does not call_withdraw(...). - Even though no strategy position was actually unwound, the buggy code still executes
balanceSnapshot = balanceSnapshot.sub(_wantAmt), understating Belt’s tracked assets. - The attacker deposits again while
wantLockedTotal()is falsely low, so the strategy token mints too many shares for the assets supplied. - The attacker calls
earn(), which refreshesbalanceSnapshotfrom the upstream bank and restores Belt’s accounting to the real asset total. - With the corrected snapshot but the overminted share balance still in hand, the attacker withdraws again and cashes out more of the strategy than they fairly funded.
- The sequence can then be repeated until the vault is largely drained.
- What the attacker needed to be true:
- The strategy had to accept direct token transfers that changed
wantLockedInHere()without updating the tracked strategy snapshot. - Share minting had to key off the understated
wantLockedTotal()value quickly enough that a second deposit could exploit the accounting gap. earn()or another refresh path had to restorebalanceSnapshotto reality before final redemption, so the overminted shares could be cashed out at full value.- The strategy needed enough TVL that the temporary direct-transfer cost and the repeated attack steps still produced a profitable spread.
- The strategy had to accept direct token transfers that changed
- Technical anatomy worth remembering:
- This was not a donation / share-price inflation attack where the attacker made each share look more valuable. It was the mirror image: the attacker made Belt think the vault had fewer assets than it really did, then bought shares at a discount and redeemed them after accounting normalized.
- The direct transfer mattered because it changed control flow, not because Belt intentionally credited the donation as user equity.
balanceSnapshotwas a classiccached truthvariable: safe only if every path that can change economic state also updates the cache under the same assumptions.- The bug only became drainable because Belt’s mint side and redeem side temporarily disagreed about the same reserve base. Once that asymmetry existed, the strategy token itself became the attack rail.
- Representative code and artifacts:
- Vulnerable implementation family:
contracts/bsc/earnV2/strategies/alpaca/StrategyAlpacaImpl.sol - Public vulnerable reference commit cited by Immunefi:
b45069e946dfe19418ce49637b3590b0f3e5a217 - Fix commit:
064282d554a424ca95ab7f87d8ec04c5f92cf343 - Core buggy logic before the fix:
if(_wantAmt > wantLockedInHere()) { _withdraw(_wantAmt.sub(wantLockedInHere())); }balanceSnapshot = balanceSnapshot.sub(_wantAmt);
- Core patched logic moved the snapshot decrement inside the
_withdraw(...)branch so only actually-unwound strategy exposure reducedbalanceSnapshot.
- Vulnerable implementation family:
- Response / mitigation:
- Schlindwein reported the issue through Immunefi on 2021-08-10.
- Belt patched the strategy contracts, with the public fix moving the
balanceSnapshotdecrement so it only applied to the portion actually withdrawn from the upstream strategy position. - Immunefi and BNB Chain’s Priority One program paid a combined
$1.05Mbounty for the report.
- Key transaction hashes:
- No public theft transaction hashes exist because the issue was disclosed and fixed before a live exploit.
- The key public preservation artifacts are the vulnerable source snapshot, the fix commit, and Immunefi’s step-by-step exploit description.
Pattern tags
- issuance-redemption-solvency-asymmetry
- balance-delta-provenance-failure
- The reusable shape is
unexpected inventory changes branch conditions, cached AUM is published under false assumptions, then later share issuance sells reserve claims against the wrong denominator. - Belt is a clean reminder that direct transfers do not need to be credited as new reserves to be dangerous. It was enough that direct-transfer-accessible balance changed which withdrawal path executed and therefore how cached strategy exposure was mutated.
Why the exploit worked
- The exploit worked because Belt split one reserve truth object into two stories: idle
wantLockedInHere()balance and cached upstream exposure inbalanceSnapshot. Direct WBNB transfers could change the first story without going through the same accounting path that was supposed to govern the second. withdraw()then treatedenough idle balance existsas if it also meantstrategy exposure just shrank. In the bugged branch, no Alpaca position was unwound, butbalanceSnapshotwas still decremented as though one had been.- That made the next deposit economically decisive. Share minting trusted the understated
wantLockedTotal()immediately, so the attacker could buy strategy shares off a fake smaller asset base even though the real assets were still there. earn()completed the asymmetry by restoring the snapshot back to actual upstream value before redemption. The attacker did not need Belt to stay wrong forever; they only needed Belt to be wrong during issuance and honest again during exit.- This is why the incident is more useful than a generic
logic errorlabel. The real bug wasbranch-local cached-AUM publication became attacker-steerable through ambient inventory, and the redemption side still honored the resulting overminted claims against real reserves.
Detection and audit pivots
- Inventory every place where direct token transfers, prefunds, or unexpected idle balances can change control flow even if the protocol claims those balances are not recognized deposits. Belt shows that branch selection alone can be enough to corrupt cached solvency state.
- For every cached AUM or
snapshotvariable, prove that decrements and increments correspond only to real upstream position changes, not merely to withdrawals that happened to be satisfied locally. - Differential-test
direct transfer -> withdraw -> deposit -> resync/earn -> redeemsequences. The dangerous question is not only whether a cache can drift, but whether an attacker can forcecheap claim issuance now, honest payout laterin one short cycle. - Assert and monitor
recognized total assets == idle balance + true upstream positionacross every branch that can satisfy withdrawals from local inventory, partial upstream unwind, or mixed sources. - Review helper paths that refresh cached balances after issuance. If a later
earn(),harvest(),sync(), or bank query can silently heal the accounting fiction, it may also complete an issuance/redemption asymmetry rather than mitigate it.
Failure-prevention checklist
- Keep cached strategy exposure subordinate to one explicit upstream-balance adoption rule. A local idle-balance branch should not be able to publish
strategy assets decreasedunless the protocol can prove an upstream unwind actually occurred. - Quarantine unexpected inventory before it affects branch conditions that also mutate reserve-significant caches. Direct transfers may need to be ignored, swept, or held inert until explicit recognition instead of being allowed to steer withdraw logic.
- Reuse one canonical total-assets primitive for share minting, share redemption, and cache publication. If issuance and redemption consult different stories about the same reserves, attackers will search for the timing gap.
- Add invariant tests for
deposit -> direct transfer -> withdraw -> deposit -> earn -> withdrawand related mixed-source flows, including cases where withdrawals are fully satisfied from idle balance, partly from idle balance, and entirely from upstream unwinds. - Treat
earn,harvest,sync, and similar refresh verbs as solvency code. If they can restore true reserves after a temporary understatement, they can also turn a transient accounting bug into a drainable round trip. - Alert on
raw balance changed without matching ledger entryand onsnapshot delta does not match upstream position delta, especially in low-share-supply or high-TVL strategies where one undercount can cheaply overmint many shares.
Audit prompts
- Which exact code paths can change
wantLockedInHere()or other locally observed balances without going through the same journal entry that governs cached upstream exposure? - In every withdrawal branch, what proves that any change to
balanceSnapshotmatches a real reduction in upstream strategy assets rather than merely a local inventory payout? - If a user directly transfers the strategy asset to the contract, which invariants guarantee that share price, total-assets reporting, and later share issuance remain unchanged until explicit recognition?
- Can a later
earn(),harvest(), or cache refresh restore the true asset total after the protocol has already issued discounted claims against the understated total? - Are mint and redeem paths reading the same canonical reserve object, or can one path consult cached or branch-local state while the other eventually pays against refreshed reality?
- What pause, cap, or share-issuance throttle limits damage if cached AUM drifts materially below actual reserves for even one block or one user action sequence?
Lessons that should already be standard
- Direct-transfer handling is not only about donation inflation. Unexpected balances can also steer branch conditions and make cached ledgers lie in the opposite direction.
- Cached AUM variables are solvency state. If a snapshot can change share issuance economics, it deserves the same adversarial review as a vault’s reserve ledger.
- Local-inventory payout and upstream-position accounting must stay fused.
User got paid from idle fundsis not the same fact asthe strategy position shrank. - Temporary accounting fiction is enough to drain a system when issuance and redemption happen on different timestamps or through different code paths. The protocol does not need to stay wrong; it only needs to be wrong at the cheap side and right again at the payout side.
- Refresh helpers are not automatically safety valves. Once a resync function can heal the reserve story after bad claims were already minted, it may actually finish the exploit path.
- Near misses like Belt are especially valuable because they show how a very ordinary-looking optimization — caching upstream exposure — becomes a live custody boundary once share supply is minted against it.
Why this incident matters
- It is a clean near-miss exemplar of
mint side sees fiction, redeem side pays realityin a yield-vault setting rather than a bonding curve or standard-wrapper bug. - It also preserves a subtle but reusable lesson: direct transfers do not only cause balance-accounting bugs; they can also steer branch conditions so cached state is updated under false assumptions.
- The case is a good reminder that
cached AUMvariables are part of the solvency boundary, not just gas optimizations.
Internal linkages
- Core recurring pattern: issuance-redemption-solvency-asymmetry.
- Adjacent accounting-boundary pattern: balance-delta-provenance-failure.
- Positive-design companion: unexpected-balance-quarantine-and-explicit-recognition-discipline — Belt is a useful reminder that even when surprise inventory is not directly overcredited as reserves, direct-transfer-accessible balances still need an explicit recognition boundary before cached ledgers and mint math are allowed to react to them.
- Adjacent valuation family: donation-share-price-manipulation — useful as a contrast case because Belt cheapened share issuance by understating assets, whereas donation attacks usually inflate later share value by overstating assets per share.
Control surface
-
The decisive leverage sat in
StrategyAlpacaImpl.withdraw(), the trust Belt placed inbalanceSnapshotas cached strategy truth, the ability to direct-transfer WBNB into the strategy contract, and the share-minting path that immediately trusted the resulting understatedwantLockedTotal(). -
The scary part was not just
a variable updated in the wrong place. It was that Belt temporarily sold reserve claims off a fake smaller denominator, then later let those same claims redeem against the full denominator. -
Sources:
- https://immunefi.com/blog/bug-fix-reviews/belt-finance-logic-error-bugfix-review-largest-ever-bounty-payout/
- https://github.com/BeltFi/belt-contract/blob/b45069e946dfe19418ce49637b3590b0f3e5a217/contracts/bsc/earnV2/strategies/alpaca/StrategyAlpacaImpl.sol
- https://github.com/BeltFi/belt-contract/commit/064282d554a424ca95ab7f87d8ec04c5f92cf343
-
Last reviewed: 2026-07-19 UTC