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 cvcrvUSD ERC-4626 collateral path inside ResupplyPair 0x6e90c85a495d54c6d7e1f3400fef1f6e59f86bd6. They flash-borrowed 4,000 USDC, swapped into crvUSD, donated 2,000 crvUSD into the near-empty collateral controller so the vault share price exploded, then minted just 1 wei of cvcrvUSD / sCrvUSD shares with another 2 crvUSD. Resupply’s _updateExchangeRate() logic inverted the oracle price as 1e36 / price; once the donated share price exceeded 1e36, Solidity floor division rounded the exchange rate down to 0. The decisive implementation detail is that addCollateralVault() merely booked the 1 wei share, while borrow() later called _updateExchangeRate() and stored that reciprocal-rounded 0 into exchangeRateInfo before the post-body isSolvent modifier rechecked the position. _isSolvent() therefore computed loan-to-value with _exchangeRate = 0, so the attacker could post 1 wei of collateral, borrow 10,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 0x6e90c85a495d54c6d7e1f3400fef1f6e59f86bd6 accepted 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 returned 0.
    • Once _exchangeRate hit 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 0 case.
    • 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:
    1. The attacker funded address 0x6D9f6E900ac2CE6770Fd9f04f98B7B0fc355E2EA from Tornado Cash in transaction 0x1962eb353a37ca816a6d967279dfdb005a640fe3b22ccb9e00939fe5810d8fb5 and prepared helper contracts 0x151aA63dbb7C605E7b0a173Ab7375e1450E79238 and 0xf90dA523A7C19A0A3d8d4606242c46f1eE459dc7.
    2. In attack transaction 0xffbbd492e0605a8bb6d490c3cd879e87ff60862b0684160d08fd5711e7a872d3, they flash-loaned 4,000 USDC from Morpho and swapped it for crvUSD.
    3. They transferred 2,000 crvUSD directly to the crvUSD controller / vault-side address 0x89707721927d7aaeeee513797A8d6cBbD0e08f41, then deposited 2 crvUSD into the share contract 0x01144442fba7aDccB5C9DC9cF33dd009D50A9e1D to mint just 1 wei of collateral shares.
    4. 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() computed 1e36 / price ≈ 0.499... and stored 0 after floor division.
    5. The attacker posted the 1 wei share as collateral through addCollateralVault(), called borrow(), and received the full 10,000,000 reUSD borrow limit because _isSolvent() now evaluated LTV as 0.
    6. They swapped the stolen reUSD through Curve into scrvUSD, redeemed into crvUSD, converted onward into harder assets, repaid the flash loan, and split proceeds across follow-on addresses including 0x31129a5c13306A48E827e851D44E19Ca07d4928A and 0x886f786618623ffFB2be59830A47661Ae6492E16.
  • 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 crvUSD transfers 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 = 0 as valid instead of reverting.
  • Technical anatomy worth remembering:
    • This was not a Hundred-style under-burn replay. 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 0 exchange rate and therefore 0 LTV.
    • 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 reused exchangeRateInfo.exchangeRate from storage.
    • In code terms, the final check was ((_borrowerAmount * _exchangeRate * LTV_PRECISION) / EXCHANGE_PRECISION) / _collateralAmount <= maxLTV. Once _exchangeRate had been stored as 0, 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.
  • 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 / sCrvUSD in public writeups): 0x01144442fba7aDccB5C9DC9cF33dd009D50A9e1D
    • Primary attacker EOA: 0x6D9f6E900ac2CE6770Fd9f04f98B7B0fc355E2EA
    • Main exploit helper: 0x151aA63dbb7C605E7b0a173Ab7375e1450E79238
    • Secondary helper: 0xf90dA523A7C19A0A3d8d4606242c46f1eE459dc7
  • 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 d84b3bf added an abnormal-price guard that rejected oracle prices above 1e22 before reciprocal conversion inside ResupplyPairCore, and commit c6a80f0 kept the if (_exchangeRate == 0) revert InvalidExchangeRate() invariant explicit while moving the upper-bound check into BasicVaultOracle.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 collateral into zero exchange rate and 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 missing version.
  • 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, from fresh wrapper accounting moved, to that move became canonical reciprocal lending truth, to that accepted truth still inheriting same-window eight-figure borrow authority.

Three wrapper-and-reciprocal promotions this incident collapsed

  1. A fresh wrapper-local share mark moved

    • The cvcrvUSD / sCrvUSD collateral path was still effectively at launch-state economics, so a 2,000 crvUSD direct donation plus a 2 crvUSD mint could dominate the first meaningful assets / shares answer.
    • 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.
  2. Resupply promoted that attacker-shaped wrapper mark into canonical reciprocal borrow truth

    • borrow() called _updateExchangeRate(), which inverted the oracle-reported wrapper price as 1e36 / price and stored the result into exchangeRateInfo before 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 collateral and instead published exchangeRate = 0 as if it were valid canonical market state.
    • The decisive failure was therefore not only the vault price jumped, but Resupply's own publication path still promoted that jump into borrow-authoritative reciprocal truth.
  3. That published reciprocal truth still inherited immediate reserve-spending authority

    • _isSolvent() reused the stored exchangeRateInfo.exchangeRate, so the impossible reciprocal state 0 algebraically collapsed LTV to safety and unlocked the market’s full 10,000,000 reUSD borrow limit against 1 wei of 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, and that accepted reciprocal may already spend the reserve are 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 crvUSD transfer 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 / price floor-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 turned 1e36 / price into 0 instead of a small positive number.
  • The protocol then treated that zero as a valid exchangeRate and 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 != 0 and 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 0 until 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 supported price onchain and revert before integer division can round the reciprocal to zero.
  • Enforce exchangeRate > 0 as 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 deposited from collateral 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 -> borrow round 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 / price remains 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 between price published and borrow 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 designed can still mean protocol published attacker-authored solvency truth.
  • Reciprocal pricing needs its own safety envelope. Designers often think only about price too low; Resupply shows that price too high can be just as lethal when the protocol inverts it.
  • exchangeRate == 0 is 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 market into first 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 collateral into exchangeRate = 0 and 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 exchangeRate state.
  • Positive-design publication companion: sync-harvest-and-cache-write-authority-discipline — the sharper follow-up is not just why was the vault price manipulable?, but why 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 / price to 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 != 0 invariant 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