OAK — OnChain Attack Knowledge

Worked example · 2023-07

Curve Finance Vyper compiler-level reentrancy — Ethereum — 2023-07-30

Loss
~$73M aggregate across multiple Curve Finance stable-asset pools at time of exploit. Per-pool figures from public post-mortems: pETH/ETH (JPEG'd) ~$11.5M, alETH/ETH (Alchemix) ~$20.5M (in some accounts ~$13.6M depending on snapshot), msETH/ETH (Metronome) ~$1.6M, CRV/ETH (Curve native) ~$24.2M. Aggregate post-recovery loss settled at ~$20M.
Recovery
~70% of the stolen value was returned. Three of the five exploit transactions were front-run by MEV searchers (notably c0ffeebabe.eth, who safeguarded ~2,800 ETH and returned it to the affected protocols). Subsequent attacker-initiated returns: ~5,494 wETH (~$10M) returned to JPEG'd on 2023-08-04; ~$22M returned to Alchemix on 2023-08-05.
OAK Techniques observed
OAK-T9.005 (Reentrancy) — but with the structurally distinct twist that the contract source code correctly invoked the reentrancy guard; the failure was in how the Vyper compiler emitted the guard's storage layout. See "Discussion" — this case sits at the edge of T9.005's current scope and is the v0.1 reference incident for compiler-level vulnerabilities as a structural class.
Attribution
pseudonymous — pseudonymous; multiple independent attackers across the five exploit transactions (each pool was drained in a separate tx, in some cases by different addresses). No publicly named-individual attribution. White-hat MEV operators (notably c0ffeebabe.eth) were the principal recovery channel for three of the five drains.
Key teaching point
Curve-Vyper is OAK's reference case for a category of incident where the standard defender narrative ("review the source, audit, deploy, monitor") is structurally insufficient because the failure occurs in a layer beneath the one the defender was inspecting. The application-layer code was correct. The audit was correct given its scope. The reentrancy guard was present and used as documented. The vulnerability was nonetheless real and was triggered exactly the way the textbook says to trigger reentrancy. The teaching point is not that Curve or its auditors made a mistake; it is that the threat model implicit in 2021–2023 DeFi audit practice — "the bytecode that runs is faithfully derived from the source we reviewed" — was an unstated assumption, and that unstated assumptions are exactly the soft surfaces an opportunistic attacker monetises.

Summary

On 2023-07-30, multiple Curve Finance liquidity pools were drained via a reentrancy exploit against pools that had been deployed using Vyper compiler versions 0.2.15, 0.2.16, and 0.3.0. The pools' Vyper source code was, by inspection, correct: the relevant add_liquidity / remove_liquidity functions carried the @nonreentrant('lock') decorator that should have prevented re-entry. The vulnerability was in the compiler. In Vyper 0.2.15–0.3.0, the bytecode emitted for @nonreentrant decorators allocated a separate storage slot per decorated function instead of a shared slot per key — i.e. two functions both decorated @nonreentrant('lock') did not actually share a lock. An attacker re-entering pool A's remove_liquidity from inside pool A's ETH-receive callback hit a different storage slot than the one set on entry, so the guard read "unlocked" and the second-entry call proceeded. The malformed-state-during-callback condition let the attacker compute LP-token-to-asset ratios against pre-burn pool balances, mint inflated LP receipts, and extract assets at a fraudulent ratio.

For OAK's purposes this is the canonical compiler-level vulnerability case: every standard application-layer audit pattern (manual review of the Vyper source, slither/equivalent linting, reentrancy-guard-presence checks) would pass. The bug was below the layer those tools inspect. The Vyper team's own post-mortem identifies the bug as introduced by Vyper PR #2391 (released in 0.2.15, 2021-07-21), partially fixed in PR #2439, fully fixed by the v0.3.1 release (2021-11-30) — meaning vulnerable bytecode could be produced for a 4-month window during which a number of widely-used pools were deployed and remained in production for ~2 years before the incident.

Timeline (UTC)

When Event OAK ref
2021-07-21 Vyper 0.2.15 released; PR #2391 introduces the malformed @nonreentrant storage-slot allocation (latent precondition)
2021-07–2021-11 Curve pools (pETH/ETH, msETH/ETH, alETH/ETH, CRV/ETH and others) deployed using vulnerable compiler versions (latent precondition)
2021-11-30 Vyper 0.3.1 ships full fix; previously-deployed bytecode is unaffected by the fix (no recompile path for live contracts) (defensive baseline gap)
2023-07-30 (multiple txs across same day) Five exploit transactions drain the affected pools in sequence T9.005 execution (compiler-level)
2023-07-30 c0ffeebabe.eth MEV bot front-runs three of the five exploits, capturing ~2,800 ETH and returning it to the protocols (white-hat recovery)
2023-07-30 Curve disclosure; coordinated communication with affected integrators (Alchemix, JPEG'd, Metronome) (defender response)
2023-08-03 Curve announces $1.85M public bounty on attacker (defender response)
2023-08-04 JPEG'd attacker returns 5,494 wETH (~$10M) (recovery)
2023-08-05 Alchemix attacker returns ~$22M (recovery)
Subsequent Curve governance proposes LP recompensation for residual losses; aggregate net loss ~$20M (defender response)

What defenders observed and learned

  • Application-layer audit was clean. Curve's pool source code correctly used @nonreentrant('lock') on the affected functions. Manual review at the Vyper-source level would not (and did not) flag the issue. Slither, MythX, and equivalent application-layer static analysers operating on Vyper source — or on the AST — likewise would not see the bug, because the bug is in the compiler's bytecode emitter, not in any source artefact those tools read. This is the teaching point: "this code was audited and uses a reentrancy guard" was true and yet not protective.
  • The vulnerable compiler-version window was knowable in retrospect. Vyper 0.2.15, 0.2.16, and 0.3.0 are a discrete, enumerable set of releases. Any defender capable of associating a deployed contract's bytecode with the compiler version that produced it could, post-disclosure, query for affected deployments. The structural gap is that most production pipelines in mid-2023 did not record or verify the compiler version used at deployment in a way that supported a "list every contract compiled by Vyper N.M.K" query at incident time. Reproducible builds and contract-to-toolchain provenance metadata (analogous to SLSA in the conventional software supply chain) would have shortened the response window from hours-to-days to minutes.
  • Front-running by MEV searchers was the dominant recovery channel for half the loss. c0ffeebabe.eth and other MEV operators observed the exploit transaction in the public mempool, replicated the call, and front-ran with a faster tx — capturing the proceeds and (voluntarily) returning them. This is a real but non-replicable defence: it depends on a public mempool, on the attacker not using a private orderflow channel (Flashbots etc.), and on the searcher's voluntary good-faith return. As private orderflow grew through 2024–2025, the structural reliability of MEV-front-run-as-recovery has eroded; OAK contributors should not project the Curve recovery rate onto future incidents.
  • Cross-protocol blast radius was significant. Alchemix (alETH peg), JPEG'd (pETH peg), and Metronome (msETH peg) all relied on Curve pool composability for their stable-asset pegs. The drain forced peg dislocations and downstream margin calls at integrating protocols that had no direct interaction with the vulnerable code path. Composability risk multiplies the impact of a compiler-level vulnerability across every pool — and every protocol depending on those pools — that shares the toolchain.
  • "Audited code is safe" no longer holds without toolchain attestation. The most generalisable lesson: an audit attests to the source artefact reviewed; it does not by itself attest to the bytecode actually deployed unless the audit explicitly covers the source-to-bytecode pipeline. Post-Curve, top-tier audit reports increasingly enumerate the exact compiler version assumed and recommend deployment-time verification that production bytecode was produced from the audited source by that exact compiler.

What this example tells contributors writing future Technique pages

  • T9.005 (Reentrancy) needs explicit sub-classification for compiler-level failures. Canonical T9.005 (the DAO 2016, Cream 2021) is "the source code did not include a reentrancy guard or used the guard incorrectly". The Curve case is "the source code used the guard correctly, and the compiler emitted incorrect bytecode for that guard". Mechanistically these are the same end-state (re-entry succeeds when it should not); structurally the defensive baselines are different (source review vs. toolchain attestation). T9.005 contributors should distinguish source-level reentrancy from compiler-level reentrancy explicitly, and link this worked example as the v0.1 reference for the latter sub-class.
  • Compiler-level vulnerabilities are a structural class not currently captured in OAK v0.1. The Curve case is the most prominent example, but the class is broader: ABI-encoder bugs (abi.decode underflows in early Solidity), optimiser-introduced miscompilations (Solidity 0.8.13–0.8.15 yul-optimiser bug affecting inline assembly), and storage-layout aliasing bugs all fall in this class. v0.x consideration: add a distinct Technique (proposed working name "Toolchain-Level Vulnerability") under the T9 family or as a new T-tier, to capture vulnerabilities introduced by the source-to-bytecode pipeline rather than by the source itself. The defensive baselines for such a Technique are reproducible builds, per-contract compiler-version attestation in deployment metadata, monitoring of compiler-vendor security advisories with automated cross-reference against deployed bytecode, and explicit audit-scope statements covering the toolchain version. Until that Technique exists, contributors documenting compiler-level cases should classify under the closest application-layer Technique and flag the compiler-level dimension explicitly in the per-incident discussion.
  • Recovery-rate distributions in T9.005 cases are bimodal, not normal. Curve recovered ~70%; Cream 2021 recovered ~0%; the DAO 2016 was reversed via hard fork (a non-replicable governance action). Contributors writing future T9.005 examples should report recovery as a per-incident outcome and avoid implying a "typical" T9.005 recovery rate exists.
  • Incident-response telemetry must include toolchain provenance. A defender who at 2023-07-30 09:00 UTC could answer "which of our deployed contracts were compiled by Vyper 0.2.15, 0.2.16, or 0.3.0?" was hours ahead of one who could not. Future T9 Technique pages on supply-chain-adjacent risks should treat toolchain-provenance recordkeeping as a v0.x defensive baseline alongside conventional monitoring and incident-response runbooks.

Public references

Discussion

Curve-Vyper is OAK's reference case for a category of incident where the standard defender narrative ("review the source, audit, deploy, monitor") is structurally insufficient because the failure occurs in a layer beneath the one the defender was inspecting. The application-layer code was correct. The audit was correct given its scope. The reentrancy guard was present and used as documented. The vulnerability was nonetheless real and was triggered exactly the way the textbook says to trigger reentrancy. The teaching point is not that Curve or its auditors made a mistake; it is that the threat model implicit in 2021–2023 DeFi audit practice — "the bytecode that runs is faithfully derived from the source we reviewed" — was an unstated assumption, and that unstated assumptions are exactly the soft surfaces an opportunistic attacker monetises.

Three open questions remain for OAK v0.x. First, whether a dedicated Toolchain-Level Vulnerability Technique should sit under T9 (alongside T9.005, treating it as a structural sibling of source-level access-control and reentrancy issues) or under a new tier (treating it as a fundamentally different layer of the stack — closer in flavour to T8 supply-chain Techniques than to T9 application-layer Techniques). The Curve case alone does not settle this; OAK contributors writing analogous compiler-level cases (the Solidity yul-optimiser cases, ABI-encoder cases) should report into the same v0.x decision. Second, whether reproducible-builds-style toolchain attestation should be a defensive baseline at the Tactic level (i.e., applicable across every T9 Technique that touches deployed code) or a per-Technique baseline. The author's lean is the former — provenance is foundational and orthogonal to which application-layer flaw is being defended against — but this is a v0.x discussion. Third, whether the white-hat MEV recovery channel should be documented as a recurring v0.1 mitigation pattern or as an incident-specific footnote. The author's lean is the latter, on the grounds that private orderflow has materially eroded its replicability since mid-2023; future Curve-class incidents should not assume a c0ffeebabe.eth is watching.

Techniques demonstrated (1)