💰 $197M Lost⟠ EthereumMarch 2023

Euler Finance: The $197M Flash Loan Exploit

How a vulnerable donation function combined with flash loans led to one of the largest DeFi hacks of 2023.

By Hexific Research Team15 min readAdvanced
$197M
Total Lost
6
Transactions
$89M
Recovered
45%
Recovery Rate

Background on Euler Finance

Euler Finance was a non-custodial lending protocol on Ethereum that allowed users to lend and borrow almost any ERC-20 token. What made Euler unique was its permissionless listing and reactive interest rate model.

Before the hack, Euler had accumulated over $200M in Total Value Locked (TVL) and was considered one of the leading DeFi lending protocols.

The Vulnerability

The exploit combined two issues in Euler's codebase:

🔓 Issue #1: Donation Function

The donateToReserves() function allowed users to donate eTokens to the protocol reserves without properly checking if the donor had sufficient collateral.

🔓 Issue #2: Soft Liquidation

When a user's health factor dropped below 1, anyone could liquidate them. However, the liquidation process didn't account for the artificially deflated eToken balance.

Root Cause

The core issue was that donateToReserves() reduced the user's eToken balance without a corresponding health check, allowing users to become artificially underwater.

Attack Flow

1

Flash Loan DAI

Attacker borrowed 30M DAI from Aave V2 via flash loan

2

Deposit & Mint eTokens

Deposited 20M DAI into Euler, receiving eDAI tokens

3

Borrow with Leverage

Used mint() to create leveraged position (10x)

4

Donate to Reserves 💥

Called donateToReserves() with most of their eDAI, making themselves liquidatable

5

Self-Liquidate

Created a second account to liquidate the first, receiving the collateral at a discount

6

Withdraw & Repay

Withdrew DAI from Euler and repaid the flash loan with profit

Code Analysis

Let's examine the vulnerable donateToReserves() function:

EulerVulnerable.sol
1// Vulnerable donateToReserves function
2function donateToReserves(uint subAccountId, uint amount) external nonReentrant {
3    (address underlying, AssetStorage storage assetStorage, 
4     address proxyAddr, address msgSender) = CALLER();
5
6    // Get user's sub-account
7    address account = getSubAccount(msgSender, subAccountId);
8
9    // Update user's eToken balance - NO HEALTH CHECK! ❌
10    assetStorage.users[account].balance = 
11        encodeAmount(
12            decodeAmount(assetStorage.users[account].balance) - amount
13        );
14
15    // Add to reserves
16    assetStorage.reserveBalance = 
17        assetStorage.reserveBalance + amount;
18
19    // Emit event
20    emit Transfer(account, address(0), amount);
21    
22    // Missing: checkLiquidity(account) ❌
23}

What's Missing

The function should have included a checkLiquidity(account) call after reducing the user's balance to ensure they remain solvent.

Here's what the fixed version looks like:

EulerFixed.sol
1// Fixed donateToReserves function
2function donateToReserves(uint subAccountId, uint amount) external nonReentrant {
3    (address underlying, AssetStorage storage assetStorage, 
4     address proxyAddr, address msgSender) = CALLER();
5
6    address account = getSubAccount(msgSender, subAccountId);
7
8    // Update balance
9    assetStorage.users[account].balance = 
10        encodeAmount(
11            decodeAmount(assetStorage.users[account].balance) - amount
12        );
13
14    assetStorage.reserveBalance = 
15        assetStorage.reserveBalance + amount;
16
17    emit Transfer(account, address(0), amount);
18    
19    // ✅ Critical: Check liquidity after balance change
20    checkLiquidity(account);
21}

Aftermath & Recovery

In a surprising turn of events, the attacker eventually returned the stolen funds:

March 13, 2023

Attack executed, $197M drained from protocol

March 14, 2023

Euler team offers $1M bounty for information leading to arrest

March 18, 2023

Attacker sends on-chain message apologizing

March 25 - April 4, 2023

Attacker returns ~$89M in multiple transactions

Lessons Learned

✅ Always Check Invariants

After any state change that affects user balances, verify that system invariants (like health factors) still hold.

✅ Audit Donation Functions

Donation and reserve functions can be attack vectors if they don't properly validate the donor's resulting position.

✅ Flash Loan Testing

Always test protocols with flash loans in mind. Any function that can be called atomically should be tested for manipulation.

✅ Multiple Audits

Euler had been audited multiple times, but this bug was missed. Consider ongoing security reviews and bug bounties.

Flash LoanDonation AttackLendingEthereumLiquidation

🛡️ Protect Your Protocol

Don't wait for an exploit. Get your smart contracts audited by Hexific's security experts.

Get a Free Audit