Loss: roughly 7.4MofDAIwasdrainedfromtheMultiStablesVaultinthemainexploittransaction,ofwhichabout2M was later returned; Value’s own post-mortem described the net loss as roughly $6M while other analyses preserve the larger gross drain
Summary: The Value DeFi attacker used an 80,000 ETH Aave flash loan plus a second large Uniswap stablecoin leg to first mint about 24,956,075 MultiStablesVault shares (mvUSD) from a 25M DAI deposit, then brutally skew Curve’s 3pool so USDC became artificially scarce. MultiStablesVault withdrawal math did not value the vault only from idle 3Crv. It called balance_to_sell(), which asked the controller to price its other strategy holdings like BCrv and CCrv as if they could be withdrawn to USDC and then converted back into 3Crv at the current manipulated 3pool state. Under that false mark, the vault’s effective 3Crv balance jumped from about 11M to about 14.5M, so burning 24.956MmvUSD redeemed about 33.089M 3Crv instead of the roughly 24.956M that honest pricing would have allowed. The attacker then unwound the pool skew, removed liquidity from Curve, repaid the flash loan, and kept about 5.4M DAI after a later 2M DAI return.
Trigger / entry point:
The exploit centered on transaction 0x46a03488247425f845e444b9c10b52ba3c14927c687d38287c0faddc7471150a at about 2020-11-14 15:36:30 UTC.
The attacker interacted through MultiVaultBank (0x8764f2c305b79680CfCc3398a96aedeA9260f7ff) into MultiStablesVault (0xDdD7df28B1Fb668B77860B473aF819b03DB61101).
Value’s own post-mortem says the new Vault v2 code path had not been audited and that the Bank deposit path did not prevent smart-contract entry, which let the attacker package deposit, pool manipulation, and withdrawal into one atomic flash-loan bundle.
Root cause:
Value treated Curve 3pool’s live state as a trustworthy pricing oracle for cross-strategy vault accounting.
balance_to_sell() did not ask what does the vault already hold in 3Crv?; it asked what would all strategy assets be worth if converted into 3Crv right now?.
The controller’s balanceOf(_want, true) path iterated over strategy assets such as BCrv and CCrv, used ShareConverter.convert_shares_rate() to simulate *_Crv -> USDC -> 3Crv, and therefore inherited Curve 3pool spot-state manipulation directly into withdrawal value.
Because the vault already held and could withdraw real 3Crv from its strategies, the attacker could redeem against that inflated mark without paying the full slippage cost that the hypothetical conversion path implied.
In short, the oracle said the vault could sell non-3Crv strategy assets for far more 3Crv than the market would really sustain, and the withdrawal path honored that fiction.
Exploit path:
The attacker used malicious contract 0x675bd0a0b03096c5ead734cfa00c7620538c7c6f to initiate the exploit.
They flash-borrowed 80,000 ETH from Aave.
They routed part of that capital through Uniswap V2 to obtain about 116M DAI and about 31M USDT of working inventory.
They deposited 25M DAI through MultiVaultBank into MultiStablesVault, which converted the DAI into about 24.956M 3Crv and minted about 24,956,075 mvUSD to the attacker.
They then distorted Curve 3pool in two large legs: about 90M DAI -> 90.285M USDC, then about 31M USDT -> 17.33M USDC, leaving the pool with very little USDC liquidity.
At that point, Value’s own post-mortem says the vault effectively held about 8.5M 3Crv, 2.2M BCrv, and 300k CCrv. The vulnerable convert_rate_xxx_to_3crv() logic priced those non-3Crv strategy assets by first converting them to USDC and then asking Curve how much 3Crv that USDC could mint.
Because USDC had just been made abnormally scarce, the vault’s internally computed total 3Crv balance rose to about 14.5M instead of the roughly 11M honest value.
The attacker burned the 24,956,075 mvUSD they had just minted and withdrew about 33.089M 3Crv, even though honest pricing would have returned only about 24.956M 3Crv.
They reversed the pool distortion by swapping about 17.33M USDC -> 30.94M USDT and about 90.285M USDC -> 90.927M DAI.
They removed liquidity from Curve by burning the inflated 33.089M 3Crv, received about 33.11M DAI, repaid the flash loan stack, and exited with about 5.4M DAI profit.
A later transaction returned 2M DAI to Value’s deployer wallet, but the vault still suffered a multi-million-dollar permanent loss.
What the attacker needed to be true:
The vault had to mint mvUSD against a DAI-to-3Crv deposit path that could be combined atomically with the later withdrawal.
Curve 3pool needed enough depth to absorb the giant DAI/USDT-to-USDC skew trades while still letting the attacker make USDC scarce enough to poison the controller’s pricing path.
balance_to_sell() had to honor controller-side hypothetical sale value rather than a manipulation-resistant or delayed mark.
The vault needed access to real strategy assets or withdrawable 3Crv so that the attacker could redeem against the inflated valuation without having to fully execute the same hypothetical conversions that created the inflated price.
The protocol had to leave deposit and withdrawal callable within the same transaction, so the attacker could mint shares, poison the oracle, redeem, and normalize the pool before anyone could react.
Technical anatomy worth remembering:
This was not just Curve spot price bad. The decisive bug lived in cross-strategy conversion logic: ShareConverter.convert_shares_rate() priced BCrv and CCrv in 3Crv terms by passing through the manipulated USDC side of Curve’s 3pool.
The critical withdrawal formula was _output_amount = (balance_to_sell().mul(_shares)).div(totalSupply()). Once balance_to_sell() was inflated, the vault overpaid every redeemed share.
Aftermath’s walkthrough highlights why this was so dangerous: the oracle imagined liquidating all strategy assets into 3Crv at the manipulated price, but the actual withdrawal path could pay the attacker from already-held strategy balances and based-token inventory. The protocol honored a fake liquidation mark without making the liquidation happen.
Value’s own post-mortem preserved the ugly ratio explicitly: manipulated accounting let the vault read a share price around 14.5M / 11M = 1.32, which turned 24.9M vault shares into about 33M 3Crv.
The incident is a strong sibling to Harvest’s October 2020 exploit, but the mechanics are not identical. Harvest let the attacker mint vault shares too cheaply against a manipulated pool state; Value let the attacker redeem vault shares too richly because cross-strategy liquidation math priced other Curve LP positions through a poisoned USDC path.
Representative transactions and addresses:
Main exploit transaction: 0x46a03488247425f845e444b9c10b52ba3c14927c687d38287c0faddc7471150a
Value halted deposits in the affected MultiStables vault and snapshot user balances for compensation.
The team said future releases would fall back to the older audited Vault v1 code until Vault v2 received heavier review.
Proposed mitigations included a stricter share_converter, a secondary oracle source for comparison, and timelock or same-block anti-reentrancy / anti-flash-loan style restrictions on deposit-withdraw sequences.
The incident reinforced a broader lesson that vaults need manipulation-resistant accounting not only when minting shares, but also when valuing heterogeneous strategy inventory during withdrawal.
The preserved healthy-side lesson is narrower than compare against another oracle: the vault needed one share-truth object binding cross-strategy inventory valuation, hypothetical unwind assumptions, redemption math, and actual payout inventory so a temporary Curve skew could not make imagined BCrv/CCrv -> USDC -> 3Crv sale proceeds immediately spendable as real 3Crv.
Three vault-and-redemption promotions this incident collapsed
1. A cross-strategy unwind quote could still be printed
Once the attacker made USDC scarce inside Curve 3pool, Value’s convert_rate_xxx_to_3crv() path could already print a far richer answer for what BCrv and CCrv supposedly meant in 3Crv terms.
Value’s own post-mortem preserved the local accounting distortion clearly: a vault that should have read about 11M 3Crv of effective value instead read about 14.5M 3Crv because the controller priced BCrv -> USDC -> 3Crv and CCrv -> USDC -> 3Crv through the manipulated scarce-USDC state.
At that stage the attacker had not yet forced the vault to realize those routes. The first dangerous promotion was only a richer hypothetical unwind quote now exists.
2. Value still promoted that hypothetical unwind into canonical share-redemption truth
The decisive promotion was that balance_to_sell() and controller.balanceOf(_want, true) did not leave the quote as controller-side bookkeeping. They published it as the vault’s official 3Crv balance for redemption math.
That is where the exploit stopped being merely Curve state was weird. The protocol crossed from some strategy assets can be described with a poisoned unwind route to that poisoned route still counts as money-good vault inventory for every mvUSD share.
In practice this meant the attacker’s freshly minted 24,956,075 mvUSD did not redeem against the honest ~24.956M 3Crv they had actually put in; it redeemed against a vault story that had been re-authored upward by the manipulated BCrv/CCrv -> USDC -> 3Crv path.
3. That published share truth still inherited immediate 3Crv payout authority
The final promotion was that Value did not merely display the richer accounting story; it still let the attacker burn those shares and receive about 33.089M 3Crv of real withdrawable value in the same economic window.
The vault never forced the exact hypothetical liquidation path whose quote created the inflated mark. It could satisfy the redemption from real strategy assets and withdrawable inventory while the manipulated route remained only a pricing fiction.
That is the reusable design split worth preserving: a route quote exists, that quote becomes canonical vault-share truth, and that accepted truth still deserves immediate reserve-spend authority were three different promotions, and Value collapsed them into one flash-loan bundle.
Value’s balance_to_sell() path never asked the realizability question that mattered: what 3Crv value could the vault actually honor for all strategy inventory after the attacker stopped supporting the manipulated Curve state? Instead, controller.balanceOf(_want, true) priced BCrv and CCrv through a hypothetical *_Crv -> USDC -> 3Crv unwind path while USDC had been made artificially scarce inside 3pool.
That made the vault’s internal mark describe an execution path, not a settled reserve object. The controller published about 14.5M 3Crv of effective value even though the honest economic value was closer to 11M 3Crv, and the withdrawal formula immediately treated the inflated figure as if it were already money-good.
Withdrawal accounting then monetized the dirty mark without forcing the same dirty unwind. The attacker could burn mvUSD and receive real 3Crv from vault / strategy inventory instead of having to execute the exact hypothetical BCrv and CCrv liquidation route whose quote created the inflated share price.
Deposit and withdrawal also remained composable inside one flash-loan bundle. That let the attacker mint shares, distort Curve, redeem against the inflated mark, and normalize the pool before any slower accounting or operator response could challenge the controller’s published value.
The durable lesson is therefore sharper than Curve spot price was manipulable: Value fused hypothetical cross-strategy liquidation value, vault-share redemption value, and payout inventory into one same-window truth object even though only the first piece was attacker-authored and non-realizable.
Detection and audit pivots
Reconstruct the full valuation path behind balance_to_sell() rather than stopping at the vault used Curve. Ask which exact helper functions turn strategy assets into the redeemable unit, and whether those helpers are quoting a hypothetical unwind route or settled reserve inventory.
Compare the controller’s reported 3Crv value with executable unwind value at protocol size. If a strategy mark only holds while one coin is abnormally scarce inside Curve, the vault should treat that mark as adversarial until delayed snapshots, haircuts, or explicit realizability checks clear it.
Simulate the exact exploit order: mint shares from a stablecoin deposit, skew 3pool to make USDC scarce, then burn those same shares before arbitrage normalizes the pool. Reviews that test only deposit-side pricing or only isolated withdrawal logic will miss the decisive same-window composition seam.
Audit whether withdrawal payouts force the valuation path to settle. If the protocol can pay from idle buffers or already-held strategy assets while the quoted unwind route remains hypothetical, then the vault is monetizing an oracle gap instead of proving reserve truth.
Treat heterogeneous strategy inventory as a single solvency object. 3Crv, BCrv, and CCrv should not become redeemable through one share price unless the protocol can defend the conversion semantics, timing, and market depth for the whole basket together.
Why this incident matters
Value DeFi is a clean example of a vault losing money because its internal liquidation model doubled as an oracle.
The protocol did not merely trust one thin token pair. It trusted a whole hypothetical unwind path across strategy tokens, and that path silently collapsed onto one manipulable USDC pocket in Curve 3pool.
It is a useful counterpart to Harvest: together they show that vault-share accounting can fail on both entry and exit, and that the dangerous oracle may live in shareConverter or controller accounting rather than in a dedicated price-feed contract.
The preserved healthy boundary was not just price strategy assets better; it was keep cross-strategy liquidation value, share redemption value, and payout inventory describing one bounded economic object instead of letting a flash-loaned USDC scarcity event promote a hypothetical unwind quote into immediate 3Crv withdrawal truth.
Failure-prevention checklist
Make hypothetical unwind math prove realizability before it becomes share-redemption authority. If BCrv -> USDC -> 3Crv is only an estimate, it should not immediately govern how much real 3Crv the vault will pay out.
Separate internal marks from spendable marks. Controller-side balance_to_sell() style accounting can inform monitoring or slow-moving strategy decisions, but same-block withdrawal rights should rely on delayed, haircut, or reserve-proven values.
Force valuation paths and payout paths to agree on inventory semantics. If a withdrawal can be paid from idle buffers or strategy balances without executing the quoted unwind route, then the quoted route needs an explicit discount or quarantine.
Quarantine same-transaction deposit-and-redeem loops around heterogeneous strategy baskets. A user who just minted shares from one stablecoin path should not be able to reprice a sibling strategy path and immediately cash out the richer story.
Treat cross-strategy conversion helpers like ShareConverter.convert_shares_rate() as solvency-critical oracle code, not just accounting glue. Every intermediate hop, especially through a manipulable pool-side scarcity leg, deserves adversarial review.
Apply depth-aware haircuts to basket components whose valuation depends on a path-sized unwind through a stressed venue. A vault should not credit the last unit of BCrv or CCrv at the same rate as a tiny quote when the whole basket would move the pool.
Reuse one canonical valuation path across mint, redeem, and strategy reporting or else explicitly narrow which path has authority. If deposits and withdrawals lean on different economic assumptions, attackers will route value through the weaker side.
Add automatic degraded-mode behavior for violent pool imbalances: pause withdrawals, freeze new share issuance, clamp share price changes, or fall back to slower snapshots when one route coin becomes abnormally scarce.
Review flash-loan resistance as a chronology problem, not just an EOA only problem. Blocking contract entry at one layer does not fix a vault that still lets fresh manipulated state become immediate payout truth.
Make multi-strategy vaults defend the basket as one balance-sheet object. The vault should be able to name which reserves actually back each share before any hypothetical conversion path is allowed to inflate that answer.
Audit prompts
Which exact function decides redeemable share value, and does it report settled reserve inventory or a hypothetical unwind quote?
If the protocol says a strategy basket is worth more right now, what trade path would have to clear at that price, at vault size, for the payout to be real?
Can the user who just minted shares also choose the pool state, route scarcity, or intermediate asset that the redemption formula will trust a few calls later?
Where does the system distinguish controller mark is readable from controller mark may immediately authorize reserve spending?
Do deposit-side and withdrawal-side share calculations rely on the same valuation semantics, or can an attacker enter through one accounting story and exit through a richer one?
If the quoted unwind route passes through a temporarily scarce asset like USDC in 3pool, what guard prevents that scarcity from instantly inflating the entire basket’s share price?
Does the vault ever pay from existing balances while valuing from a hypothetical route that never actually executes, and if so, what discount or quarantine closes that gap?
What delayed snapshot, TWAP, cap, or fallback path activates when basket valuation moves too far too fast for organic unwinds to justify?
Which strategy-helper contracts, converters, and controller methods are effectively acting as price or reserve oracles even though they are named like bookkeeping code?
If every share redeemed immediately after a stressed valuation had to be honored in the same asset, what concrete reserves would run out first?
Lessons that should already be standard
Internal accounting helpers are often the real oracle. A vault can lose money without a dedicated price-feed contract ever being the obvious bug surface.
Hypothetical unwind value is not reserve truth. If the protocol has not actually executed the conversion, it should hesitate before letting that estimate command withdrawals.
Multi-strategy share systems fail when they tell two stories at once: one story for what the basket is supposedly worth, another for what inventory can actually be paid out.
Same-window composability is dangerous when one actor can mint shares, manipulate the basket’s mark, and redeem before the mark has to survive outside the attacker’s own transaction.
The path through the pool matters as much as the endpoint asset. 3Crv-denominated valuation was not safe here because the route silently hinged on one temporarily scarce USDC leg.
Safer vault design is not only about better pricing inputs; it is about keeping pricing publication, share authority, and payout inventory bound tightly enough that a fake unwind cannot become a real withdrawal.
The right review question is not merely can this strategy asset be quoted higher? but can that higher quote immediately spend stronger reserves the strategy never had to realize?
Sharper vault-accounting lens: vault-strategy-path-valuation-manipulation — Value’s decisive oracle lived in controller-side hypothetical unwind math, not just in a generic Curve spot price bad story.
Positive-design companion: vault-share-truth-security-object — the sharper healthy-side follow-up when the question becomes what one vault should have kept bound together before a hypothetical cross-strategy unwind quote could become immediate share-redemption and payout truth.
Timing-side design companion: delayed-snapshots-and-same-block-quarantine — useful when the sharper follow-up is why a same-transaction Curve distortion should not have become immediate withdrawal authority.
Publication / reserve-authority companion: internal-mark-publication-quarantine-and-reserve-spend-separation — useful when the sharper follow-up is why controller-side hypothetical unwind marks should have been quarantined before they could become spendable 3Crv redemption truth.
The leverage sat in balance_to_sell(), controller.balanceOf(..., true), ShareConverter.convert_shares_rate(), Curve 3pool’s live USDC side, and the protocol’s willingness to honor hypothetical liquidation value as immediate redeemable 3Crv.
Value did not lose funds because Curve was volatile. It lost funds because the vault’s own exit math treated a flash-loaned, non-realizable cross-strategy conversion quote as if it were hard reserve value.
The preserved healthy boundary was that controller-side unwind math and withdrawal-side payout inventory should have remained one bounded share-truth object rather than two stories linked only by an attacker-timed Curve state.