💰 $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