POST-MORTEM ANALYSIS

EULER FINANCE
$197M EXPLOIT

A single missing function call. Eight months in production. The ghost in the machine.

LOSS
$197,000,000
STATUS
VERIFIED
VECTOR
LOGIC ERROR
DATE
MAR 13, 2023

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.

// The Vulnerability

Code Anatomy

Every other function in EToken.sol that reduces a user's collateral balance calls checkLiquidity() afterward. Except this one.

EToken.sol:359-386 — VULNERABLE
359function donateToReserves(uint subAccountId, uint amount)
360 external nonReentrant
361{
362 (address underlying, AssetStorage storage assetStorage,
363 address proxyAddr, address msgSender) = CALLER();
364
365 address account = getSubAccount(msgSender, subAccountId);
366
367 AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);
368
369 uint currentBalance = cycleBalances(assetCache, account);
370 uint newBalance = currentBalance - amount; // Reduces collateral
371
372 // Update storage
373 assetStorage.users[account].balance = encodeAmount(newBalance);
374 assetStorage.reserveBalance = assetCache.reserveBalance
375 + decodeExternalAmount(assetCache, amount);
376
377 logAssetStatus(assetCache);
378
379 // 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}
THE MISSING GUARDIAN

Fig. 2: The vulnerable donateToReserves() function — note the missing checkLiquidity() call

Function Comparison

FunctionLinecheckLiquidity()
withdraw()197 YES
transfer()345 YES
burn()262 YES
mint()227 YES
donateToReserves()386 MISSING
// Proof of Concept

Live Exploit Reproduction

Watch the vulnerability come to life. This PoC runs against a mainnet fork at the exact block before the exploit (16817995).

euler_exploit_poc.sh — Block 16817995 (Mainnet Fork)

Fig. 1: Live reproduction of the exploit on Ethereum Mainnet Fork (Block 16817995)

// Attack Vector

The Four-Step Heist

STEP 1

Flash Loan

Borrow 30M DAI from Aave V2

STEP 2

Leverage Up

Deposit + mint() to create leveraged position

STEP 3

THE EXPLOIT

Call donateToReserves() — position becomes insolvent without revert

STEP 4

Self-Liquidate

Liquidate own position at 20% discount, repay flash loan, keep profit

Verified on Real Data

Source Code
euler-xyz/euler-contracts
Git Intel
232 commits analyzed
PoC Execution
PASSED

Analysis by 0xWalterWhiteHat

Request an Audit