All incidents

Euler DAI Reserve Donation

Share
Mar 13, 2023 08:50 UTCAttackLoss: 8,877,507.35 DAIPending manual check1 exploit txWindow: Atomic
Estimated Impact
8,877,507.35 DAI
Label
Attack
Exploit Tx
1
Addresses
3
Attack Window
Atomic
Mar 13, 2023 08:50 UTC → Mar 13, 2023 08:50 UTC

Exploit Transactions

TX 1Ethereum
0xc310a0affe2169d1f6feec1c63dbc7f7c62a887fa48795d327d4d2da2d6b111d
Mar 13, 2023 08:50 UTCExplorer

Victim Addresses

0x27182842e098f60e3d576794a5bffb0777e025d3Ethereum
0xe025e3ca2be02316033184551d4d3aa22024d9dcEthereum
0x6085bc95f506c326dcbcd7a6dd6c79fbc18d4686Ethereum

Loss Breakdown

8,877,507.35DAI

Similar Incidents

Root Cause Analysis

Euler DAI Reserve Donation

1. Incident Overview TL;DR

On Ethereum mainnet block 16817996, the attacker EOA 0x5f259d0b76665c337c6104145894f4d1d2758b8c called attacker contract 0xebc29199c817dc47ba12e3f86102564d640cbf99, borrowed 30,000,000 DAI from Aave v2, built a leveraged Euler DAI position through helper contracts, donated 100,000,000 internal eDAI units to Euler reserves, liquidated the now-underwater position, repaid Aave with its deterministic 27,000 DAI premium, and kept 8,877,507.348306697267428294 DAI.

The root cause is a missing post-state solvency check in Euler’s reserve-donation path. EToken.donateToReserves reduces a debtor’s eToken collateral balance and credits reserves, but unlike Euler’s other collateral-changing paths it does not call checkLiquidity(account). That lets an unprivileged actor self-create insolvency and then use Euler’s normal liquidation path to seize discounted collateral.

2. Key Background

Euler’s DAI market splits state across:

  • Euler core 0x27182842e098f60e3d576794a5bffb0777e025d3
  • eDAI proxy 0xe025e3ca2be02316033184551d4d3aa22024d9dc
  • dDAI proxy 0x6085bc95f506c326dcbcd7a6dd6c79fbc18d4686
  • liquidation module 0xf43ce1d09050bafd6980dd43cde2ab9f18c85b34

Users deposit DAI to receive eDAI exposure and can borrow DAI debt through the dToken path. Euler normally protects these positions by re-checking account liquidity after state transitions that change collateral or debt.

Aave v2 supplied the initial capital through public flash loans. Its DAI pool 0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9 lent 30,000,000 DAI to the attacker transaction, and the trace shows a matching 27,000 DAI premium before repayment. No privileged funding or private key compromise was required.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK case caused by an accounting and solvency-check omission in Euler’s eToken reserve-donation flow. The critical invariant is straightforward: any operation that reduces collateral for an account that still owes debt must ensure the account remains solvent, or the operation must revert. Euler enforces that invariant in its ordinary collateral-changing flows, but donateToReserves is the exception. The function burns internal eToken balance from the caller’s account and credits the same amount to the reserve bucket without performing a post-update liquidity check. Once the attacker uses that function on a debt-bearing account, Euler’s liquidation module recomputes health from the new reduced collateral state and correctly observes healthScore < 1e18. At that point the attacker no longer needs a second bug: the standard liquidation path transfers discounted collateral to the liquidator, which converts the attacker-created insolvency into immediately withdrawable DAI.

The verified EToken module shows the missing guard directly:

function donateToReserves(uint subAccountId, uint amount) external nonReentrant {
    (address underlying, AssetStorage storage assetStorage, address proxyAddr, address msgSender) = CALLER();
    address account = getSubAccount(msgSender, subAccountId);

    updateAverageLiquidity(account);
    emit RequestDonate(account, amount);

    AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);
    uint origBalance = assetStorage.users[account].balance;
    uint newBalance;

    if (amount == type(uint).max) {
        amount = origBalance;
        newBalance = 0;
    } else {
        require(origBalance >= amount, "e/insufficient-balance");
        unchecked { newBalance = origBalance - amount; }
    }

    assetStorage.users[account].balance = encodeAmount(newBalance);
    assetStorage.reserveBalance = assetCache.reserveBalance = encodeSmallAmount(assetCache.reserveBalance + amount);
    emit Withdraw(assetCache.underlying, account, amount);
    emitViaProxy_Transfer(proxyAddr, account, address(0), amount);
    logAssetStatus(assetCache);
}

4. Detailed Root Cause Analysis

The attacker first used a flash loan to obtain enough DAI to create a large self-borrow position in Euler. The seed trace shows the violator helper 0x583c21631c48d442b5c0e605d624f54a0b366c72 executing deposit, mint, repay, mint, and finally donateToReserves against the DAI market in one transaction.

The critical on-chain breakpoint is visible in the trace immediately before liquidation:

0xe025E3ca2bE02316033184551D4d3Aa22024D9DC::donateToReserves(0, 100000000000000000000000000)
emit RequestDonate(account: 0x583c21631c48D442B5C0E605d624f54A0B366c72, amount: 100000000000000000000000000)
emit Withdraw(underlying: Dai, account: 0x583c21631c48D442B5C0E605d624f54A0B366c72, amount: 100000000000000000000000000)
emit AssetStatus(... reserveBalance: 100192726706131943046318424 ...)
0xf43ce1d09050BAfd6980dD43Cde2aB9F18C85b34::checkLiquidation(
  0xA0b3ee897f233F385E5D61086c32685257d4f12b,
  0x583c21631c48D442B5C0E605d624f54A0B366c72,
  Dai,
  Dai
)

That sequence matters because the donation reduces the violator’s collateral before any solvency gate runs. The liquidation module then evaluates the now-degraded account exactly as designed. Its verified code computes health from current collateral and liability, and returns a liquidation opportunity whenever collateral falls below liability:

(uint collateralValue, uint liabilityValue) = getAccountLiquidity(liqLocs.violator);

if (liabilityValue == 0) {
    liqOpp.healthScore = type(uint).max;
    return;
}

liqOpp.healthScore = collateralValue * 1e18 / liabilityValue;

if (collateralValue >= liabilityValue) {
    return;
}

// At this point healthScore must be < 1 since collateral < liability

The same module then transfers the violator’s debt to the liquidator and transfers discounted collateral to the liquidator:

transferBorrow(
    underlyingAssetStorage,
    underlyingAssetCache,
    underlyingAssetStorage.dTokenAddress,
    liqLocs.violator,
    liqLocs.liquidator,
    repay
);

transferBalance(
    collateralAssetStorage,
    collateralAssetCache,
    eTokenAddress,
    liqLocs.violator,
    liqLocs.liquidator,
    underlyingAmountToBalance(collateralAssetCache, yield)
);

The incident trace confirms that this exact path fired. After checkLiquidation, Euler executed liquidate with DAI as both liability and collateral asset. The emitted liquidation event records a sub-1e18 health score and discounted collateral transfer:

emit Liquidation(
  liquidator: 0xA0b3ee897f233F385E5D61086c32685257d4f12b,
  violator: 0x583c21631c48D442B5C0E605d624f54A0B366c72,
  underlying: Dai,
  collateral: Dai,
  repay: 254234370990601202559999999,
  yield: 317792963738251503199999998,
  healthScore: 750978642951301046,
  baseDiscount: 269021357048698954,
  discount: 200000000000000000
)

Because the attacker chose DAI as both collateral and debt asset, the seized collateral could be withdrawn immediately from Euler’s pool and recycled into flash-loan repayment plus profit. No oracle manipulation or secondary accounting flaw was needed after the missing solvency check.

5. Adversary Flow Analysis

The adversary flow was a single adversary-crafted transaction, 0xc310a0affe2169d1f6feec1c63dbc7f7c62a887fa48795d327d4d2da2d6b111d, mined in block 16817996.

  1. The attack contract 0xebc29199c817dc47ba12e3f86102564d640cbf99 received an Aave flash loan of 30,000,000 DAI.
  2. During the callback, it deployed two fresh helper contracts: a violator helper to create the Euler position and a liquidator helper to consume the liquidation opportunity.
  3. The violator helper deposited 20,000,000 DAI, minted 200,000,000 DAI-equivalent debt, repaid 10,000,000 DAI, minted again, and then donated 100,000,000 internal eDAI units to reserves.
  4. The liquidator helper queried checkLiquidation, liquidated the violator once the donation pushed health below one, withdrew DAI from Euler, and transferred proceeds back to the attack contract.
  5. The attack contract approved Aave for 30,027,000 DAI, repaid the flash loan plus premium, and retained the remaining DAI as profit in attacker-controlled execution flow.

The collected trace also shows the public flash-loan event near the end of execution:

emit FlashLoan(
  target: 0xeBC29199C817Dc47BA12E3F86102564D640CBf99,
  initiator: 0xeBC29199C817Dc47BA12E3F86102564D640CBf99,
  asset: Dai,
  amount: 30000000000000000000000000,
  premium: 27000000000000000000000,
  referralCode: 0
)

This confirms the exploit remained permissionless from start to finish: public funding, public Euler entrypoints, public liquidation path, and public withdrawal of seized collateral.

6. Impact & Losses

Euler’s DAI market lost 8,877,507.348306697267428294 DAI to the attacker in the incident transaction. In raw on-chain units, that is:

{
  "token_symbol": "DAI",
  "amount": "8877507348306697267428294",
  "decimal": 18
}

Aave was not the victim. It received full repayment of principal plus its deterministic 27,000 DAI premium. The transaction sender separately paid 0.110796608064610836 ETH in gas, but that did not change the DAI-denominated exploit outcome.

Independent balance checks against mainnet state confirm the attacker contract held 0 DAI at block 16817995 and 8877507348306697267428294 wei of DAI at block 16817996, matching the reported profit figure.

7. References

  1. Exploit transaction 0xc310a0affe2169d1f6feec1c63dbc7f7c62a887fa48795d327d4d2da2d6b111d
  2. Related deployment transaction 0x725db76cf4bf3bdc72be4f755ed72df4adbc8e4e803c044617cc11e34907ccdc
  3. Euler core 0x27182842e098f60e3d576794a5bffb0777e025d3
  4. Euler EToken module source 0xbb0D4bb654a21054aF95456a3B29c63e8D1F4c0a
  5. Euler liquidation module source 0xd737eE2bB39F49C62a436002A77f2710cc45eD98
  6. eDAI proxy 0xe025e3ca2be02316033184551d4d3aa22024d9dc
  7. dDAI proxy 0x6085bc95f506c326dcbcd7a6dd6c79fbc18d4686
  8. Aave v2 lending pool 0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9
  9. Collected seed artifacts for tx metadata, trace, and balance deltas under the session collector outputs