EULER FINANCE
$197M EXPLOIT
A single missing function call. Eight months in production. The ghost in the machine.
The Cruel Irony
The vulnerable function was added to FIX a different bug.
In July 2022, eight months before the exploit, Euler's team received a bug report through their Immunefi program. A whitehat had discovered a "first depositor" share inflation attack—a well-known vulnerability in vault-style contracts.
The fix was elegant: add a function called donateToReserves() that would allow anyone to seed the protocol's reserves, preventing the inflation attack.
But in adding it, the developers forgot something critical.
Code Anatomy
Every other function in EToken.sol that reduces a user's collateral balance calls checkLiquidity() afterward. Except this one.
359function donateToReserves(uint subAccountId, uint amount)360 external nonReentrant361{362 (address underlying, AssetStorage storage assetStorage,363 address proxyAddr, address msgSender) = CALLER();364365 address account = getSubAccount(msgSender, subAccountId);366367 AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);368369 uint currentBalance = cycleBalances(assetCache, account);370 uint newBalance = currentBalance - amount; // Reduces collateral371372 // Update storage373 assetStorage.users[account].balance = encodeAmount(newBalance);374 assetStorage.reserveBalance = assetCache.reserveBalance375 + decodeExternalAmount(assetCache, amount);376377 logAssetStatus(assetCache);378379 // BUG: Missing checkLiquidity(account) here!380 // All other functions that reduce balance call it:381 // - withdraw() calls checkLiquidity()382 // - transfer() calls checkLiquidity()383 // - burn() calls checkLiquidity()384}Fig. 2: The vulnerable donateToReserves() function — note the missing checkLiquidity() call
Function Comparison
| Function | Line | checkLiquidity() |
|---|---|---|
| withdraw() | 197 | YES |
| transfer() | 345 | YES |
| burn() | 262 | YES |
| mint() | 227 | YES |
| donateToReserves() | 386 | MISSING |
Live Exploit Reproduction
Watch the vulnerability come to life. This PoC runs against a mainnet fork at the exact block before the exploit (16817995).
Fig. 1: Live reproduction of the exploit on Ethereum Mainnet Fork (Block 16817995)
The Four-Step Heist
Flash Loan
Borrow 30M DAI from Aave V2
Leverage Up
Deposit + mint() to create leveraged position
THE EXPLOIT
Call donateToReserves() — position becomes insolvent without revert
Self-Liquidate
Liquidate own position at 20% discount, repay flash loan, keep profit
Verified on Real Data
Analysis by 0xWalterWhiteHat
Request an Audit