Incident analysis

Re-auditing the Euler attack: what the Auditing IDE surfaces from bytecode alone

June 15, 2026 · 4 min read

In March 2023, Euler Finance lost roughly $197M in one of the largest DeFi exploits to date. The root cause was a single missing check. It's a perfect case study for the kind of bug a bytecode-native, exploit-confirming pipeline is built to catch — so we walked the public, documented attack back through ours.

Retrospective. This analyzes a public, widely documented incident using information disclosed after the fact. It is an educational retrospective, not a claim of prior discovery. Figures follow public reporting.

The mechanism, in five moves

Euler is a lending protocol. Users deposit collateral, borrow against it, and the protocol enforces one core invariant: an account must stay healthy — its collateral must cover its debt. Liquidation exists to clean up accounts that fall below that line, and it pays the liquidator a discount as an incentive. The attack abused the interaction between a donation function and that liquidation discount:

  1. Flash loan. Borrow a large amount of DAI to fund the setup with no upfront capital.
  2. Leverage up. Deposit, then mint to create a heavily leveraged position — inflating both the collateral (eTokens) and the debt (dTokens).
  3. Donate into insolvency. Call donateToReserves to give away a large slice of the position's backing. This pushed the attacker's own account underwater — and crucially, the function did not re-check that the account stayed healthy afterward.
  4. Self-liquidate. From a second account, liquidate the now-insolvent first account. Euler's dynamic discount handed the liquidator collateral worth substantially more than the debt it absorbed.
  5. Walk away. Repay the flash loan; keep the difference. Net: ~$197M.
The whole exploit rests on one missing invariant: donateToReserves let an account make itself unhealthy without a post-condition check. Everything else is standard DeFi machinery used against itself.

What our pipeline flags — step by step

1 · A state-mutating function that touches solvency, ungated by a health check

BEVM — our bytecode-level EVM engine, the analysis core behind what the product calls the Auditing IDE — has a detector suite and attack-graph builder that look for exactly this shape: a function that reduces an account's backing (a write to balances/reserves) with no health-factor check on the path out. In the graph it shows up as a writer to the solvency-relevant slots that isn't dominated by the same require the borrow/withdraw paths are:

# BEVM · attack-graph (solvency surface)
withdraw()   → checkLiquidity()  guarded
borrow()     → checkLiquidity()  guarded
donateToReserves() → (no checkLiquidity)  ungated   ← anomaly
liquidate()  → dynamic discount, pays liquidator

One of these functions is not like the others. The asymmetry — three solvency- affecting entry points guarded, one not — is the tell.

2 · A composable chain across the flash-loan boundary

A single-transaction view never sees this. Our multi-contract modeling follows the flash-loan callback and composes the steps into one sequence, then asks the question that matters: does the discount on the self-liquidation exceed the value donated away?

# multi-contract chain (modeled)
flashLoan(DAI) ─► deposit ─► mint (leverage)
              ─► donateToReserves (self → insolvent)
              ─► liquidate (acct B liquidates acct A, discounted)
              ─► repay(DAI)
profit = collateral_seized − debt_absorbed − fees   > 0  →  exploit

3 · Confirmation on a fork, not a hunch

The reason this matters for a defender is the same reason it mattered for the attacker: it either nets a profit or it doesn't. Replaying the modeled chain on a mainnet fork turns "suspicious asymmetry" into a number — a positive, attacker-controlled delta — which is the only signal worth waking a team up for.

4 · Janus writes the verdict

Availability, September 2026. Janus AI is in development and not available — nothing in the product calls it today. The verdict below is from our development build, kept as written. The attack graph and the fork replay above are available in the workbench; engine-level fuzzing runs in the analysis engine, with no dedicated panel; this step is not. For current capability status, see the ecosystem support matrix.

CRITICAL — Self-liquidation via uncheck­ed donation. donateToReserves reduces account backing without enforcing the post-donation health check applied to withdraw/borrow. An attacker leverages up, donates into insolvency, and self-liquidates to harvest the liquidation discount on collateral worth more than the debt absorbed — funded risk-free by a flash loan. Fix: enforce the liquidity/health check on every path that can reduce an account's backing, donateToReserves included; treat "any state change that can affect solvency must re-assert the solvency invariant" as a hard rule.

The lesson generalizes

Euler was audited — multiple times. The bug survived because it wasn't a typo in one function; it was a missing invariant across functions: one path that could break solvency without re-checking it. That class — "a protected invariant with one unprotected door" — is hard for humans to hold in their head across a large codebase, and invisible to single-transaction tools. It is exactly what attack-graph reachability plus exploit-confirming fuzzing is designed to surface, and what Janus — still in development, not yet available — is being trained to reason about.

That's the bet behind Trilocore: make the analysis that would have caught this — bytecode-level, cross-function, replay-verified — cheap enough to run on every contract, before mainnet, not after the post-mortem.

Retrospective analysis of a publicly disclosed incident for educational purposes. The reconstruction follows public post-mortems and on-chain records; specifics are simplified for clarity. Trilocore was not involved in the incident or its response.

Scan a contract → Open the workbench
← All research