Root Cause
ResupplyFi's ResupplyPair lending market for wstUSR deployed without a virtual offset protecting against the ERC-4626 first-deposit donation attack. When total shares = 1 wei, the convertToAssets function calculates each share's value by dividing total deposited assets by total shares. Donating a large amount of assets before minting shares inflates the exchange rate astronomically, causing the protocol to treat 1 wei of collateral as worth tens of millions of dollars.
// Simplified ResupplyPair exchange rate (vulnerable pattern)
function convertToAssets(uint256 shares) public view returns (uint256) {
uint256 _totalShares = totalShares; // = 1 after attacker seeds
uint256 _totalAssets = vault.totalAssets(); // approx 2000 crvUSD after donation
// With shares=1e18, totalAssets approx 2000e18, totalShares=1 returns approx 2e39
return shares.mulDiv(_totalAssets + 1, _totalShares + 1);
}
// LTV check passes because inflated price makes debt appear negligible:
// ltv = debt / (collateral * inflatedExchangeRate) approx 0 < MAX_LTV
The solvency check evaluates ltv = debt / collateralValue. With an inflated exchange rate, even 1 wei of collateral appears to cover $10M+ in debt, so ltv approx 0, below the maximum LTV threshold. The market had been live for only ~1.5 hours when the attack executed.
Attack Steps
| Step | Action | Detail |
|---|---|---|
| 1 | Flash loan | Borrowed ~4,000 USDC from Morpho; swapped to crvUSD |
| 2 | Donate | Transferred ~2,000 crvUSD directly into the empty wstUSR vault (bypassing deposit()) |
| 3 | Seed mint | Called deposit(2 crvUSD) to mint 1 wei of shares |
| 4 | Rate inflation | convertToAssets(1e18) approx 2x10^36; each share now treated as worth billions |
| 5 | Borrow | Used 1 wei of share collateral to pass the solvency check; borrowed 10M reUSD |
| 6 | Repay flash loan | Returned 4,000 USDC to Morpho |
| 7 | Extract | Swapped stolen reUSD to crvUSD and exited |
Impact
Approximately $9.56M-$9.8M in reUSD stablecoin drained from the wstUSR market. Only this market was affected; other ResupplyFi pairs remained solvent. Protocol paused operations immediately following detection.
Lessons for Auditors
- Virtual share offset: Always add a virtual offset (e.g., OpenZeppelin's 1e3 internal shares minted to
address(0)at deployment) to new ERC-4626 vaults before any external deposit is possible. This prevents the donated-assets-to-1-wei-share ratio from reaching a manipulable magnitude. - Minimum initialization deposit: Require a non-trivial initialization deposit from the protocol itself during market deployment so the vault never reaches the unguarded zero-shares state.
- Time-delay between deployment and funding: Enforce a cooldown window (e.g., 24h) between market creation and liquidity acceptance to allow monitoring systems to catch suspicious early-deposit patterns.
- LTV zero-guard: Revert on
collateralValue == 0rather than silently approving loans with effectively zero collateral. - ERC-4626 donation attack PoC: Every new lending market wrapping an ERC-4626 vault should run the standard first-deposit PoC before deployment to mainnet with real user funds.