ResupplyFi cvcrvUSD Donation / Zero-Exchange-Rate Exploit (2025-06-26)
- Name: ResupplyFi cvcrvUSD donation / zero-exchange-rate exploit
- Date: 2025-06-26
- Protocol: ResupplyFi
- Chain: Ethereum
- Category: donation / share-price manipulation / zero-exchange-rate solvency bypass / cross-market lending drain
- Loss: about 9.8M, commonly reported as roughly $9.56M
- Summary: Roughly 95 minutes after Resupply deployed its new wstUSR market, the attacker targeted the paired
cvcrvUSDERC-4626 collateral path insideResupplyPair0x6e90c85a495d54c6d7e1f3400fef1f6e59f86bd6. They flash-borrowed4,000 USDC, swapped intocrvUSD, donated2,000 crvUSDinto the near-empty collateral controller so the vault share price exploded, then minted just1 weiofcvcrvUSD/sCrvUSDshares with another2 crvUSD. Resupply’s_updateExchangeRate()logic inverted the oracle price as1e36 / price; once the donated share price exceeded1e36, Solidity floor division rounded the exchange rate down to0. The decisive implementation detail is thataddCollateralVault()merely booked the1 weishare, whileborrow()later called_updateExchangeRate()and stored that reciprocal-rounded0intoexchangeRateInfobefore the post-bodyisSolventmodifier rechecked the position._isSolvent()therefore computed loan-to-value with_exchangeRate = 0, so the attacker could post1 weiof collateral, borrow10,000,000 reUSD, dump it through Curve, and leave the protocol with bad debt. - Trigger / entry point:
- Resupply had just launched the relevant market hours earlier, leaving the ERC-4626 collateral path effectively empty and donation-sensitive.
- The pair contract at
0x6e90c85a495d54c6d7e1f3400fef1f6e59f86bd6accepted the vault share token as collateral and trusted the oracle-reported share price immediately. - The collateral-side pricing path inverted price with
_exchangeRate = 1e36 / oracle.getPrices(address(collateral))and did not reject the case where integer division returned0. - Once
_exchangeRatehit zero, Resupply’s solvency check treated the position as perfectly safe because LTV algebraically collapsed to zero.
- Root cause:
- Resupply accepted a newly deployed, near-empty ERC-4626 vault share as money-good collateral without dead shares, virtual offsets, or a collateral quarantine.
- A direct donation into the vault controller increased assets per share without minting proportional new shares, letting the attacker manufacture an absurd oracle price.
- The lending layer then inverted that huge price with integer division and failed to defend the
0case. - In practice, the protocol combined two brittle assumptions: that vault share price could not be cheaply printed, and that an inverted exchange-rate calculation could never round down to zero.
- Exploit path:
- The attacker funded address
0x6D9f6E900ac2CE6770Fd9f04f98B7B0fc355E2EAfrom Tornado Cash in transaction0x1962eb353a37ca816a6d967279dfdb005a640fe3b22ccb9e00939fe5810d8fb5and prepared helper contracts0x151aA63dbb7C605E7b0a173Ab7375e1450E79238and0xf90dA523A7C19A0A3d8d4606242c46f1eE459dc7. - In attack transaction
0xffbbd492e0605a8bb6d490c3cd879e87ff60862b0684160d08fd5711e7a872d3, they flash-loaned4,000 USDCfrom Morpho and swapped it forcrvUSD. - They transferred
2,000 crvUSDdirectly to thecrvUSDcontroller / vault-side address0x89707721927d7aaeeee513797A8d6cBbD0e08f41, then deposited2 crvUSDinto the share contract0x01144442fba7aDccB5C9DC9cF33dd009D50A9e1Dto mint just1 weiof collateral shares. - That donation-plus-1-wei mint caused the oracle price for the collateral share to jump to roughly
2,000,000,000,000,000,001,998,001,998,001,998, so_updateExchangeRate()computed1e36 / price ≈ 0.499...and stored0after floor division. - The attacker posted the
1 weishare as collateral throughaddCollateralVault(), calledborrow(), and received the full10,000,000 reUSDborrow limit because_isSolvent()now evaluated LTV as0. - They swapped the stolen
reUSDthrough Curve intoscrvUSD, redeemed intocrvUSD, converted onward into harder assets, repaid the flash loan, and split proceeds across follow-on addresses including0x31129a5c13306A48E827e851D44E19Ca07d4928Aand0x886f786618623ffFB2be59830A47661Ae6492E16.
- The attacker funded address
- What the attacker needed to be true:
- The target ERC-4626 collateral vault had to still be empty or near-empty so a tiny share position could dominate ownership.
- Direct
crvUSDtransfers into the controller had to increase assets per share immediately. - The oracle had to report the inflated vault price faithfully, because the exploit depended on truthful reporting of manipulated state rather than on a stale or compromised feed.
- Resupply had to accept the manipulated share token as collateral before any seeding, smoothing, or sanity check blocked the move.
- The borrow path had to treat
_exchangeRate = 0as valid instead of reverting.
- Technical anatomy worth remembering:
- This was not a Hundred-style
under-burnreplay. The money leg came earlier: the solvency check itself collapsed once an inverse price formula rounded down to zero. - The vault side and the lending side were one attack surface. The donation printed a fake share price, but the real theft required Resupply’s lending math to turn that fake price into
0exchange rate and therefore0LTV. - The borrow path, not the collateral-add path, published the fatal zero.
addCollateralVault()only incremented_userCollateralBalance;borrow()then executed_updateExchangeRate()before_borrow(), and the modifier’s final_isSolvent()check reusedexchangeRateInfo.exchangeRatefrom storage. - In code terms, the final check was
((_borrowerAmount * _exchangeRate * LTV_PRECISION) / EXCHANGE_PRECISION) / _collateralAmount <= maxLTV. Once_exchangeRatehad been stored as0, the post-borrow solvency test itself algebraically collapsed to safety. - The oracle was not wrong; it honestly reflected a manipulable empty-vault state. That makes this a clean reminder that ERC-4626 collateral integrations are oracle integrations in disguise.
- Launch state mattered. The attack window existed because the market was live before meaningful collateral depth or protective initialization made donation inflation expensive.
- This was not a Hundred-style
- Representative transactions and addresses:
- Main exploit tx:
0xffbbd492e0605a8bb6d490c3cd879e87ff60862b0684160d08fd5711e7a872d3 - Market deployment / governance execution tx cited in public writeups:
0x852eca15a9fd352817346915f7bc8817d46de349bd7a8fc6ee73c7b66ec9ab41 - Tornado funding tx:
0x1962eb353a37ca816a6d967279dfdb005a640fe3b22ccb9e00939fe5810d8fb5 - Vulnerable pair contract:
0x6e90c85a495d54c6d7e1f3400fef1f6e59f86bd6 - Vault controller / donation target:
0x89707721927d7aaeeee513797A8d6cBbD0e08f41 - Collateral share token (
cvcrvUSD/sCrvUSDin public writeups):0x01144442fba7aDccB5C9DC9cF33dd009D50A9e1D - Primary attacker EOA:
0x6D9f6E900ac2CE6770Fd9f04f98B7B0fc355E2EA - Main exploit helper:
0x151aA63dbb7C605E7b0a173Ab7375e1450E79238 - Secondary helper:
0xf90dA523A7C19A0A3d8d4606242c46f1eE459dc7
- Main exploit tx:
- Response / mitigation:
- Resupply acknowledged the exploit publicly, said the affected contract had been identified and paused, and later set the borrow limit to zero for the affected market.
- Community and third-party postmortems converged on the same immediate fixes: reject zero exchange rates, seed or offset empty ERC-4626 vaults, and quarantine freshly launched collateral until donation-driven share-price distortion is no longer cheap.
- The public Resupply repository later reflected the fix path in code, not just in commentary: commit
d84b3bfadded an abnormal-price guard that rejected oracle prices above1e22before reciprocal conversion insideResupplyPairCore, and commitc6a80f0kept theif (_exchangeRate == 0) revert InvalidExchangeRate()invariant explicit while moving the upper-bound check intoBasicVaultOracle.getPrices(). - The incident also exposed an uncomfortable audit boundary issue: public discussion after the hack suggested the core rounding-to-zero condition was in-scope code, but safe deployment and initialization assumptions around fresh vaults were not validated tightly enough.
Why this incident matters
- Resupply is a sharp modern exemplar of direct donation / share-price manipulation where the lending layer’s own inverse-price math turns
very expensive collateralintozero exchange rateand therefore unlimited borrow power. - It usefully complements Wise Lending. Wise shows iterative stealth-donation compounding; Resupply shows the brutally simple
empty vault + direct donation + zero-check missingversion. - It also broadens the exploit corpus beyond under-burn and empty-market redemption cases by preserving a different failure mode: borrow-side solvency collapse without needing redemption rounding to cash out.
- The durable design lesson is sharper than
ERC-4626 donation attack: Resupply crossed three different promotions too quickly, fromfresh wrapper accounting moved, tothat move became canonical reciprocal lending truth, tothat accepted truth still inheriting same-window eight-figure borrow authority.
Three wrapper-and-reciprocal promotions this incident collapsed
-
A fresh wrapper-local share mark moved
- The
cvcrvUSD/sCrvUSDcollateral path was still effectively at launch-state economics, so a2,000 crvUSDdirect donation plus a2 crvUSDmint could dominate the first meaningfulassets / sharesanswer. - That only proved the wrapper’s local accounting had changed. It did not yet mean the new share price deserved to become lending-solvency truth for Resupply.
- The
-
Resupply promoted that attacker-shaped wrapper mark into canonical reciprocal borrow truth
borrow()called_updateExchangeRate(), which inverted the oracle-reported wrapper price as1e36 / priceand stored the result intoexchangeRateInfobefore the final solvency modifier re-ran.- Once the truthful-but-attacker-shaped wrapper mark exceeded the reciprocal path’s supported range, the protocol stopped publishing
very expensive collateraland instead publishedexchangeRate = 0as if it were valid canonical market state. - The decisive failure was therefore not only
the vault price jumped, butResupply's own publication path still promoted that jump into borrow-authoritative reciprocal truth.
-
That published reciprocal truth still inherited immediate reserve-spending authority
_isSolvent()reused the storedexchangeRateInfo.exchangeRate, so the impossible reciprocal state0algebraically collapsed LTV to safety and unlocked the market’s full10,000,000 reUSDborrow limit against1 weiof collateral.- This is where the printed wrapper mark became real protocol loss. The manipulated state did not merely distort paper accounting; it retained same-window authority to mint debt, drain Curve-routable value, and leave bad debt behind.
- The preserved lesson is that
wrapper-local price exists,that price still maps to nonzero reciprocal truth, andthat accepted reciprocal may already spend the reserveare three different promotions. Resupply failed all three.
Pattern tags:
- donation-share-price-manipulation
- inverse-price-floor-to-zero
- Near-empty wrapper collateral admission: Resupply treated a freshly launched, effectively empty ERC-4626 collateral path as if its first meaningful share price already deserved full borrow-side authority.
- Direct-donation share-price printing: a raw
crvUSDtransfer into the controller raised assets per share without minting proportional new shares. - Reciprocal-price domain collapse: the decisive cash-out rail was not only inflated collateral, but
1e36 / pricefloor-rounding to literal zero. - Truthful oracle, unsafe downstream trust: the oracle did not lie; the protocol let a truthful read of attacker-shaped wrapper state become immediate solvency truth.
- Design companions: reciprocal-price-normalization-and-nonzero-invariant-discipline.
Why the exploit worked
- The attacker could cheaply own the meaningful share supply because the collateral wrapper was still at launch-state economics.
- A direct donation changed the wrapper’s assets-per-share ratio without forcing proportional share issuance, so the printed price was economically attacker-authored even though the oracle reported it faithfully.
- Resupply’s borrow-side conversion assumed the reciprocal price stayed inside a safe domain. Once the wrapper price exceeded
1e36, integer division turned1e36 / priceinto0instead of a small positive number. - The protocol then treated that zero as a valid
exchangeRateand reused it inside the final solvency check, so the very guard meant to stop over-borrowing algebraically collapsed to safety. - Put differently: a fresh wrapper-local mark became canonical reciprocal market truth, and that reciprocal truth still kept full same-window borrow authority. Vault accounting, oracle publication, reciprocal normalization, and borrow authorization were one joined security object with no fail-closed boundary between them.
Detection and audit pivots
- Fuzz wrapper-collateral integrations at launch-state and near-empty-supply conditions, not only at normal TVL. Ask how cheaply one actor can own the rate-setting share supply.
- Stress reciprocal price paths with attacker-inflated wrapper marks. If
1 / price,Q / price, or similar inverse math can round to zero, the supported domain is underspecified. - Require explicit
exchangeRate != 0and upper-bound / abnormal-price guards before any collateral mark reaches solvency or borrow-limit code. - Trace exactly which function publishes wrapper value into borrow truth. Here the fatal publication step was
borrow()calling_updateExchangeRate()before the final solvency modifier re-check. - Treat fresh wrapper admission as a launch-control problem, not only an oracle problem: dead shares, virtual offsets, delayed collateral enablement, and borrow-side quarantine all belong in the review.
Failure-prevention checklist
- Do not admit a fresh ERC-4626 wrapper as fully money-good collateral until its opening share price is no longer attacker-definable. Seed dead shares or virtual offsets first, then keep borrow power at
0until the wrapper has meaningful depth. - Treat direct donations as hostile by default. If
convertToAssets()or another share-price primitive can jump because raw underlying hit the vault, either quarantine that state, smooth it, or require an explicit recognition step before lending solvency reads it. - Define the reciprocal-price domain explicitly. If borrow-side math uses
Q / price, prove the maximum supportedpriceonchain and revert before integer division can round the reciprocal to zero. - Enforce
exchangeRate > 0as a hard invariant at every publication boundary, not only in downstream consumers. The write path that stores borrow-truth state should fail closed before modifiers or later functions reuse it. - Separate
collateral can be depositedfromcollateral can publish borrow power. Fresh wrappers often need a warm-up phase where deposits are allowed but LTV, borrow caps, or market-wide authority remain disabled. - Put blast-radius controls under fresh collateral anyway: tiny borrow caps, isolated-market mode, and fast pause hooks should keep one wrapper-price failure from immediately turning into eight-figure bad debt.
- Test
donate -> mint dust shares -> post collateral -> borrowround trips directly, including extreme share-price cases where the oracle is perfectly honest but the wrapper state is economically fake. - Review oracle adapters and wrapper oracles as solvency code, not passive helpers. A one-line
convertToAssets()oracle can still be the only thing standing between a manipulable wrapper and protocol-wide borrow authority.
Audit prompts
- What exact onchain condition proves this wrapper is past its dangerous first-depositor / first-donor state, and why can an attacker not cheaply recreate that state later?
- Can raw token transfers, rebases, fee donations, or strategy-side accounting moves change the wrapper price without minting proportional new shares?
- What is the maximum price the reciprocal conversion path supports, and where does the code prove that
Q / priceremains strictly positive for every reachable oracle value? - Which function first turns wrapper price into canonical borrow truth, and does that function itself reject abnormal or zero-domain outputs before any solvency check reuses them?
- If the oracle is completely honest but the wrapper state is attacker-authored, what prevents that truthful read from becoming immediate collateral authority?
- Are
addCollateral,borrow, and post-body solvency modifiers effectively one joined authority object, and if so where is the fail-closed boundary betweenprice publishedandborrow allowed? - Could a fresh collateral listing have been safe if deposits were allowed but borrowing, recursive self-collateralization, or cross-market exposure stayed quarantined for longer?
- Which alerts would fire if a wrapper with almost no supply suddenly printed an extreme
convertToAssets(1e18)value or if reciprocal exchange-rate math approached zero?
Lessons that should already be standard
- ERC-4626 collateral admission is market launch logic. A fresh wrapper whose first meaningful share price is still attacker-definable is not initialized just because the pair contract is deployed.
- A truthful oracle is not a defense when the underlying state is cheap to forge.
Oracle worked as designedcan still meanprotocol published attacker-authored solvency truth. - Reciprocal pricing needs its own safety envelope. Designers often think only about
price too low; Resupply shows thatprice too highcan be just as lethal when the protocol inverts it. exchangeRate == 0is not a weird edge case; it is a catastrophic statement about the supported math domain. If zero can enter borrow authorization, the solvency check may stop meaning anything at all.- Deposit-side and borrow-side wrapper logic are the same security object. A donation-sensitive share price is already a lending bug once the protocol lets that share price set collateral authority.
- Fresh-market quarantine buys time for reality to form. Dead shares, delayed collateral enablement, isolated caps, and abnormal-price guards are not belt-and-suspenders extras; they are what turns
first user sets the marketintofirst user joins the market.
Internal linkages
- Branch-dominant pattern: donation-share-price-manipulation — the first money-good lie was attacker-authored share-price truth becoming canonical collateral authority on a fresh ERC-4626 path.
- Sharper reciprocal-math lens: inverse-price-floor-to-zero — the cash-out rail only fully opened because Resupply’s borrow-side inverse-price math turned
very expensive collateralintoexchangeRate = 0and therefore false solvency. - Fresh-state companion: fresh-market-initialization-failure — the exploit window existed because the paired market and ERC-4626 collateral path were still effectively at launch-state economics, so a tiny opening position could define the first meaningful wrapper price.
- Publication-authority companion: state-publication-authority-failure — the donation only became borrowable truth once Resupply’s own oracle / reciprocal-conversion path crystallized the attacker-shaped fresh wrapper mark into canonical
exchangeRatestate. - Positive-design publication companion: sync-harvest-and-cache-write-authority-discipline — the sharper follow-up is not just
why was the vault price manipulable?, butwhy did one freshly published wrapper mark get immediate lending authority with no abnormal-price or nonzero guardrail. - Sharper reciprocal-math lens: inverse-price-floor-to-zero — the decisive bug was not just printed share price, but reciprocal borrow-side math that floor-rounded
1e36 / priceto literal zero and let solvency checks pass. - Positive-design reciprocal companion: reciprocal-price-normalization-and-nonzero-invariant-discipline — the clean design lesson is that reciprocal pricing itself needed an explicit supported domain, upstream abnormal-price rejection, and a hard
exchangeRate != 0invariant before any borrow-side solvency math ran. - Adjacent pattern: first-deposit-inflation — the exploit began from the same empty-vault bootstrap sensitivity, even though the payoff came through a lending-layer zero-exchange-rate failure rather than direct theft from the next depositor.
- Adjacent pattern: solvency-check-bypass — the share-price manipulation mattered because it forced lending math into a false-solvency state.
Control surface
-
The decisive leverage sat in market-launch hygiene, ERC-4626 collateral admission, the oracle-to-exchange-rate conversion formula, and the missing invariant that
exchangeRatemust remain strictly positive before any solvency check can pass. -
Resupply did not just trust a manipulable vault price. It transformed that manipulable price into a literal zero and then interpreted zero as safety.
-
The publication step mattered as much as the donation:
oracle.getPrices()plus_updateExchangeRate()were the protocol-authored write path that turned a fresh, attacker-defined wrapper mark into immediate solvency truth. -
addCollateralVault(),_updateExchangeRate(), and modifier-scopedisSolventeffectively formed one joined authority object: one call booked attacker-shaped shares, a later call published a reciprocal-rounded zero, and the final solvency gate silently assumed that published value could never exit the valid domain. -
The subtle launch lesson is that a
fresh collateral wrapperis part of market initialization even when the lending pair itself is already deployed: if the first economically meaningful vault state is still attacker-definable, the market is not actually initialized. -
Sources:
- https://rekt.news/resupplyfi-rekt
- https://x.com/ResupplyFi/status/1938092252431036491
- https://ackee.xyz/blog/resupply-hack-analysis/
- https://www.quillaudits.com/blog/hack-analysis/resupply-hack-analysis
- https://olympixai.medium.com/resupply-exploit-post-mortem-how-9-56m-was-stolen-in-90-minutes-75eced577077
- https://olympix.security/biweekly-exploits/cork-resupplyfi-meta-pool-hookless-priceless-and-permissionless-losses
- https://raw.githubusercontent.com/SunWeb3Sec/DeFiHackLabs/main/src/test/2025-06/ResupplyFi_exp.sol
- https://github.com/resupplyfi/resupply/blob/main/src/protocol/pair/ResupplyPairCore.sol
- https://github.com/resupplyfi/resupply/blob/main/src/protocol/BasicVaultOracle.sol
- https://github.com/resupplyfi/resupply/commit/d84b3bf6319e974980b45b8b126ac93614836500
- https://github.com/resupplyfi/resupply/commit/c6a80f0720a6f0c9f3aea75ab8f983f62619a13c
-
Last reviewed: 2026-08-02 UTC