{
 "schema": 1,
 "slug": "2023-03-Euler",
 "url": "https://crypto.training/hacks/2023-03-euler/",
 "title": "Euler Finance $197M Exploit — donateToReserves Enables Self-Liquidation That Drains the Protocol's Reserves",
 "kind": "incident",
 "source": "DeFiHackLabs",
 "date": "2023-03",
 "chain": "Ethereum",
 "protocol": null,
 "severity": null,
 "auditPlatform": null,
 "vulnTags": [
  "vuln/logic/liquidation-logic",
  "vuln/logic/incorrect-state-transition"
 ],
 "categories": [
  "logic"
 ],
 "bugClass": "Accounting/logic flaw — `donateToReserves` lets a user mint eToken-credit without depositing underlying, combined with a self-liquidation path that uses inflated collateral to absorb the violator's debt and seize reserves.",
 "vulnerableLocation": "Euler protocol — `0x27182842E098f60e3D576794A5bFFb0777E025d3` (Exec/Liquidation module via custom module dispatch).",
 "loss": "~$197M (this PoC extracts 8,877,507.35 DAI net after repaying a 30M DAI flash loan; the protocol held 38,904,507 DAI). The live attack drained ~$197M across DAI/USDC/WBTC/stETH.",
 "tldr": "Euler's EToken accounting keeps a per-asset `totalBalances` (sum of eToken balances) and `internalBalance` per user. Two functions matter:\n\n- `mint()` (EToken `mint(subAccountId, amount)`): mints `amount` of eToken to the user **and** records `amount` of debt (dToken) — a *borrow*. It raises both the user's eToken and dToken proportionally, so net the user is not made \"richer.\"\n- `donateToReserves(subAccountId, amount)`: **burns `amount` of the caller's own eToken** and credits it to the protocol reserve pool (a *donation* of collateral to all depositors).\n\nThe bug is in `liquidate`'s solvency math. Euler computes an account's liability with `accountLiquidity`/`getAccountLiquidity`, which compares eToken value (collateral) against dToken value (debt). Crucially, **`liquidate` allows the `underlying` (debt asset) and `collateral` to be the same token** (self-liquidation), and its health check used a flawed `eTokens < dTokens` comparison that could be driven to \"insolvent but profitable-to-liquidate\" by the donation trick:\n\nThe attack (from the PoC header + trace):\n\n1. **Flash-loan 30M DAI** from Aave V2.\n2. **Violator account**: `deposit(20M DAI)` → 19.56M eDAI; `mint(200M)` → +195.68M eDAI & +200M dDAI (borrow 200M DAI of debt, no real DAI leaves); `repay(10M)` → burns 10M dDAI; `mint(200M)` again → +195.68M eDAI & +200M dDAI; then **`donateToReserves(100M)`** — burns 100M of the violator's own eDAI.\n3. The donation collapses the violator's collateral (eDAI) below its debt (dDAI) → the violator is now **underwater / liquidatable**, *while the protocol's reserve bookkeeping was corrupted*.\n4. **Liquidator account**: calls `liquidate(violator, DAI, DAI, …)` — repays the violator's dDAI and receives eDAI collateral + a discount `yield`. Because of the accounting corruption, the liquidation yields **317,792,963 eDAI** — far more than real.\n5. **Withdraw** the liquidator's eDAI for real DAI: `withdraw(0, DAI.balanceOf(Euler_Protocol))` pulls **38.9M DAI** out of the protocol.\n6. Repay the 30M DAI flash loan → net **8,877,507 DAI** profit.\n\nThe single enabling flaw: `donateToReserves` could be used to *destroy collateral that had already been used to back debt*, and the liquidation logic trusted the resulting corrupted balances to settle debt against phantom reserves, letting the attacker extract real deposited funds.",
 "rootCause": "1. **Collateral and debt can diverge via donation.** `donateToReserves` is an asymmetric operation: it removes collateral (eToken) but leaves the paired debt (dToken) intact. This breaks the invariant that an account's collateral always covers (at the configured factor) its debt.\n2. **Self-liquidation is permitted.** `liquidate` did not forbid `underlying == collateral`. Combined with (1), an attacker controls *both* sides of the liquidation: it makes its own violator account under-collateralized (via donation) and then liquidates it with a second account it also controls.\n3. **Liquidation yield is taken from real reserves.** The `yield` (discount collateral) minted to the liquidator is ultimately redeemable (`withdraw`) for real underlying that other users deposited. The corrupted accounting let `withdraw` believe the protocol held enough to honor it — it did hold it (38.9M DAI), but that DAI belonged to honest depositors, not the attacker.\n4. **`mint`/`borrow` had no effective cap against the donate trick.** The violator borrows 400M DAI of *notional* debt against 20M real DAI + inflated eToken; normally `requireLiquidity` would stop this, but the sequence (deposit → mint → repay → mint → donate) is arranged so each step passes the post-condition health check, and only after the donation is the account insolvent — which is the *intended* trigger for liquidation, not a guard.",
 "vulnerableCode": [
  {
   "language": "solidity",
   "code": "// EToken.donateToReserves — burns caller's eToken to the reserve pool\nfunction donateToReserves(uint256 subAccountId, uint256 amount) external nonReentrant {\n    (, AssetStorage storage assetStorage, address proxyAddr) = CALLER();\n    address underlying = assetStorage.underlying;\n    // ⚠️ burns the caller's OWN eToken without touching their dToken debt\n    updateBalance(underlying, proxyAddr, msg.sender, -(int256(amount)));\n    assetStorage.reserveBalance += amount;            // credited to protocol reserves\n    emit RequestDonate(...); emit Withdraw(...);\n}",
   "caption": "The reconstructed vulnerable logic (selectors observed in the trace; the canonical Euler-v1 `Exec.sol`/`Liquidation.sol`/`EToken.sol` source these were compiled from)",
   "markedVulnerable": true,
   "section": "vulnerableCode"
  },
  {
   "language": "solidity",
   "code": "// Liquidation._doLiquidate — allows underlying == collateral (self-liquidation)\nfunction _doLiquidate(address liquidator, address violator, address underlying,\n                      address collateral, uint256 repay, uint256 yield) internal {\n    ...\n    // repay violator's dToken debt, then SEIZE collateral eToken + discount yield\n    // the health/bounds checks trusted accountLiquidity computed from corrupted balances\n    dTokenBurn(underlying, violator, repay);\n    ...\n    eTokenMint(collateral, liquidator, yield);        // ⚠️ yields eToken backed by phantom reserves\n}",
   "markedVulnerable": true,
   "section": "vulnerableCode"
  }
 ],
 "preconditions": "- A funded Euler market with depositors (here DAI: 38.9M of honest DAI).\n- A flash-loan source for working capital (Aave V2, 30M DAI).\n- The ability to deploy two fresh accounts (violator + liquidator) — trivial.\n- The buggy `donateToReserves` + same-asset `liquidate` combination (the live Euler v1 deployment).",
 "attack": "All figures from. Two attacker-controlled accounts: `Iviolator` (`0x5615…b72f`) and `Iliquidator` (`0x2e23…470b`).\n\n| # | Step | Violator eDAI | Violator dDAI | Euler DAI reserve | Effect |\n|---|------|--------------:|--------------:|------------------:|--------|\n| 0 | Flash-loan 30M DAI (Aave V2); fund violator | — | — | 38,904,507 | Working capital in hand. |\n| 1 | `deposit(20M)` | +19,568,124 | 0 | 38,904,507 (+20M in, then reused) | Violator collateral = 19.56M eDAI. |\n| 2 | `mint(200M)` | +195,681,244 → **215.25M** | +200,000,000 | unchanged (debt notional) | Borrow 200M DAI of debt; `requireLiquidity` passes vs 215M eDAI. |\n| 3 | `repay(10M)` | 215.25M | **190,000,000** | unchanged | Reduce debt to widen headroom. |\n| 4 | `mint(200M)` again | +195,681,244 → **410.93M** | +200,000,000 → **390,000,000** | unchanged | Now eDAI 410.9M vs dDAI 390M — barely solvent. |\n| 5 | **`donateToReserves(100M)`** | **−100,000,000 → 310.93M** | 390,000,000 (unchanged) | unchanged | ⚠️ **eDAI (310.9M) < dDAI (390M)** → violator is insolvent/liquidatable. |\n| 6 | `checkLiquidation` → `liquidate(violator, DAI, DAI, repay=259.3M, yield=317.79M)` | dDAI −259.3M | **130.7M** | unchanged | Liquidator repays 259.3M dDAI, **receives 317,792,963 eDAI** (255). |\n| 7 | `withdraw(0, DAI.balanceOf(Euler_Protocol) = 38.9M)` | — | — | 38.9M → ~0 | Liquidator pulls all real DAI. |\n| 8 | Repay 30M DAI flash loan (+premium) | — | — | — | Net **8,877,507.35 DAI** profit. |\n\n### Profit/loss accounting (DAI)\n\n| Direction | Amount |\n|---|---:|\n| Flash-loaned (Aave V2) | 30,000,000 (borrowed, repaid) |\n| Real DAI withdrawn from Euler | +38,904,507 |\n| Flash-loan + premium repaid | −30,027,000 (≈) |\n| **Net profit (logged)** | **+8,877,507.35 DAI** |\n\nThe trace confirms: `Attacker DAI balance after exploit: 8877507.348306697267428294`. In the live attack the attacker repeated this across DAI/USDC/WBTC/stETH for ~$197M total.",
 "impact": null,
 "remediation": "1. **Forbid self-liquidation.** `liquidate` must reject `underlying == collateral` (or any case where violator and liquidator are controlled such that the attacker profits from both sides). This alone blocks the exploit.\n2. **Make `donateToReserves` symmetric or guarded.** Donations should not be able to put an account into a state that is profitable to liquidate against phantom reserves — e.g., disallow donating collateral below the account's required maintenance margin, or burn proportional debt too.\n3. **Strong invariant checks on liquidation yield.** The `yield` minted must never exceed what real reserves can back; bound it against the asset's `totalBalances` and real token holdings.\n4. **Conservative `accountLiquidity`.** Treat donated-away collateral as still encumbered by the debt it was backing, so a donation cannot manufacture insolvency.\n5. **Circuit breakers / pause on large withdrawals.** A withdrawal of the entire protocol balance by a single account in one tx is an obvious red flag; a cap or timelocked pause would have limited the $197M loss.\n\n*(Euler patched this in v1 by disabling the vulnerable `donateToReserves`/self-liquidation path and later launched Euler v2 with reworked accounting.)*",
 "hasReplay": true,
 "references": {
  "page": "https://crypto.training/hacks/2023-03-euler/",
  "poc": "https://github.com/sanbir/evm-hack-registry/tree/main/2023-03-Euler_exp",
  "attackTx": "https://etherscan.io/tx/0xc310a0affe2169d1f6feec1c63dbc7f7c62a887fa48795d327d4d2da2d6b111d",
  "upstream": "https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2023-03/Euler_exp.sol",
  "report": null,
  "alert": "https://twitter.com/FrankResearcher/status/1635241475989721089"
 }
}
