BEVM Janus June 15, 2026 · 10 min read

Re-auditing the Euler attack: what BEVM + Janus surface from bytecode alone

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's detector suite and attack-graph builder 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

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 is 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.

← All posts