In June 2025, ResupplyFi’s wstUSR lending market lost $9.6 million in a single transaction. One division operation rounded down to zero at a boundary the solvency check did not anticipate.
The check read the zero as “this borrower is well within their loan-to-value ceiling,” and the attacker borrowed the full pool against 1 wei of collateral. The bug class is an ERC-4626 inflation attack, well-documented and discussed in EIP-4626 itself. What makes the case worth walking through is how easy the flow was to miss in manual review.
TL;DR
- The ResupplyFi hack resulted in ~$9.6M drained after an attacker manipulated an effectively empty ERC-4626 vault, causing a solvency-critical exchange-rate calculation to round down to zero.
- The zero rate did not halt borrowing. It caused the downstream loan-to-value check to evaluate the attacker as safely collateralized.
- Boundary-condition reasoning asks what protocol arithmetic produces when adversarial inputs push calculations to their extremes.
- Exchange-rate arithmetic and solvency invariants should be treated as critical attack surfaces requiring explicit boundary analysis.
Why empty vaults are dangerous
ResupplyFi prices borrower collateral by querying an ERC-4626 vault’s convertToAssets() function. When that vault is freshly deployed and holds little to no assets, an attacker can donate underlying assets directly to it without minting shares, which inflates the share price the vault reports.
The exploit on the wstUSR market followed that path. The cvcrvUSD vault used as the oracle’s input held essentially no shares. The attacker donated about 2,000 crvUSD straight into it, then deposited 2 crvUSD to mint 1 wei of shares. After those two steps, convertToAssets(1e18) returned roughly 2e36 instead of a sensible number. The protocol’s exchange rate is computed as 1e36 / convertToAssets(1e18), and under Solidity integer arithmetic that rounded down to zero.
The dangerous boundary: from zero to borrow
The solvency check uses the exchange rate as a multiplier when computing the borrower’s LTV. With a zero rate, the LTV evaluates to zero, and zero is less than any positive max LTV. The check passes.
|
1 2 3 4 5 6 7 8 |
function _isSolvent(address _borrower, uint256 _exchangeRate) internal view returns (bool) { uint256 _maxLTV = maxLTV; if (_maxLTV == 0) return true; // user borrow shares should be synced before _isSolvent is called // ... elided for brevity uint256 _ltv = ((_borrowerAmount * _exchangeRate * LTV_PRECISION) / EXCHANGE_PRECISION) / _collateralAmount; return _ltv <= _maxLTV; } |
That is how the attacker borrowed the full ceiling (~$10M) of the wstUSR pool against 1 wei of collateral. The solvency check exists to prevent borrows above the LTV ceiling. On this code path it did the opposite.
What kind of review catches this
When I review an oracle exchange rate calculation in a lending protocol, the question I work through first is what the result looks like at the input boundaries. Not under normal market conditions. At the limits of what an attacker can push the input to.
For the ResupplyFi calculation, that question has two halves. What does 1e36 / oracle.getPrices(collateral) look like when the oracle returns a number close to zero? What does it look like when the oracle returns a very large number? Treat any arithmetic operation that feeds a solvency check as a boundary problem. The code is reasonable for the inputs the developer anticipated. The auditor’s job is the inputs the developer did not.
How a modern auditor automates this
When we audit a rate-and-solvency path at Veridise, the review doesn’t stop at the manual read. We encode what the reviewer noticed as a check that survives the engagement.
For ResupplyFi, that means two artifacts. A custom static detector for the “division before multiplication through storage” shape, so any future change to the rate path is flagged at commit time. And a formal expression of the loan-to-value invariant we can hand to a fuzzer to exercise against the deployed market before user funds arrive. Next week’s Pre-Mortem walks through exactly that workflow, end to end.
The tooling doesn’t replace the reviewer. It catches the arithmetic edge cases human review is most likely to miss, which frees our review time for the protocol-specific logic the tooling cannot reason about.
What separates a modern auditor
Every new lending market a protocol ships is a new attack surface, even when the pair contract has been audited multiple times in prior incarnations. ResupplyFi’s pair logic has a long audit history. That history did not protect a freshly opened market with a fresh, manipulable oracle input.
When we audit a system like this, we do two things a manual-only review does not. We treat the exchange rate calculation as a single point of failure and encode that thinking as a re-runnable check. We express the LTV invariant formally, separate from the implementation, so it can be exercised against the deployed market at any future commit.
The difference is durability. A traditional audit catches what one reviewer noticed on one read. A modern audit leaves a check behind.
If your protocol is shipping new lending markets and you want this kind of audit before launch, request an audit or view our past DeFi audits to see how we approach oracle and collateral ratio verification.