The Vyper Reentrancy Incident: When the Compiler Is the Attack Surface
Summary
Most exploit write-ups stop at the contract. This one cannot, because the
vulnerability was not in a contract at all — it was in the compiler that
produced them. On 30 July 2023, attackers drained on the order of $70 million
from several Curve Finance pools that used native ETH, exploiting a defect in the
Vyper language: its reentrancy guard was
mis-compiled in versions 0.2.15, 0.2.16 and 0.3.0, so the @nonreentrant
protection that every affected pool relied on silently did nothing.
The episode is the cleanest real-world demonstration of a principle that risk scoring has to take seriously: a dependency shared across many "safe" systems is a correlated failure, not an idiosyncratic one. Several separately audited pools failed in the same hour for the same reason. The loss was contained — about 73% of funds were ultimately recovered (CoinDesk) — but only after the incident exposed a second, larger risk that had nothing to do with the bug.
Timeline: 30 July 2023
The attacks arrived in quick succession against pools holding native ETH:
- JPEG'd pETH/ETH — ~$11–12M drained.
- Alchemix alETH/ETH — ~$20M (≈5,000 ETH plus ERC-20s).
- Metronome msETH/ETH — ~$1.6M.
- Curve CRV/ETH — ~$18.5M.
Headline figures vary by source ($61M–$73.5M) because recovery began almost
immediately (Chainalysis,
Halborn).
A white-hat MEV operator, c0ffeebabe.eth, front-ran the attacker on the
CRV/ETH pool and secured 2,800 ETH ($5.4M), which was returned to Curve
(TechCrunch).
Net realised loss settled near $52M before the longer recovery played out.
Root cause: a broken guard, one layer down
Curve's older pools that custody native ETH return it to users inside
remove_liquidity / remove_liquidity_one_coin using a low-level call. A
low-level ETH transfer hands execution to the recipient's fallback before the
pool has finished updating its own state — the textbook setup for a reentrancy
attack. The textbook defence is equally well known: a reentrancy guard, declared
in Vyper with the @nonreentrant(<key>) decorator, which is supposed to take a
lock in storage on entry and release it on exit so the function cannot be called
again mid-execution.
The defect was that, in the affected compiler versions, the guard did not hold the lock. The reentrancy-lock storage slot was mis-allocated by the compiler, so a function the developer had explicitly marked non-reentrant could be re-entered anyway. The contracts were correct. The audits were not wrong about the contracts. The generated bytecode simply did not enforce the protection the source code asked for (Hacken).
With the guard inert, the attack is mechanical: deposit, trigger the ETH return, re-enter the pool from the fallback while balances and the pool's virtual price are momentarily inconsistent, and withdraw against the stale accounting to extract more than was deposited.
Why "audited" did not mean "safe"
This is the part that matters for scoring. Each affected pool had been reviewed. Each was, by reputation, battle-tested. None of that helped, because the failure was shared infrastructure: a single compiler version sat underneath all of them. Auditing a contract verifies the contract; it does not verify the toolchain that compiled it, the language runtime, or the other forty contracts that share that toolchain. The correlated nature of the failure — many "independent" pools, one root cause, one hour — is precisely what an idiosyncratic, per-contract risk view fails to capture.
Attribution across the risk stack
Philidor scores every vault across three vectors. Forcing this incident through that decomposition is clarifying:
- Platform — this is where the loss originated, but not where most observers looked. The exploit lived in the language toolchain, a platform dependency beneath the contract. A platform vector that only counts "audited: yes/no" would have rated these pools well right up to the exploit. The lesson is that the platform surface includes the compiler, shared singleton contracts, and the version monoculture across a protocol's deployments.
- Asset — secondary. The pools held ETH and ETH-correlated assets (alETH, pETH, msETH); the assets did not fail, they were the medium through which the platform flaw was monetised. The native-ETH design that enabled the callback is better read as a platform property than an asset one.
- Control — not implicated in the exploit itself, but decisive in the aftermath, as the next section shows.
The headline attribution is therefore platform-dominant, supply-chain class — a category that per-contract audit scoring structurally under-weights.
The second-order risk: one borrower, ~47% of supply
The exploit was contained in dollar terms. The danger was what it threatened to set off. Curve's founder, Michael Egorov, had borrowed roughly $100M against about 427.5M CRV — close to 47% of circulating supply — spread across lending markets: ~$168M of CRV on Aave for ~$63M of USDT, ~$32M of CRV for ~$17M of FRAX on Fraxlend, ~$18M on Abracadabra, plus positions on Inverse and Silo (CoinDesk, DL News).
The exploit hit CRV's price. A large enough fall would liquidate those positions; the liquidations would sell CRV into thin liquidity, pushing the price down further and risking bad debt to Aave if collateral could not be unwound at size. A contract bug had created a path to a protocol-solvency event two hops away, through a concentration the bug never touched. This is the control/ governance vector expressing itself as systemic risk: a single entity's collateral footprint, opaque to anyone scoring the pools in isolation, was the real tail.
It was defused the slow way. Egorov sold CRV over-the-counter to a set of notable counterparties, paid down the loans, and ultimately settled the Aave position (The Block). The system got lucky with time and willing buyers; neither is a control.
What it changes for risk scoring
Three durable lessons survive this incident:
- Score the toolchain, not just the contract. Compiler version, language runtime, and shared singleton dependencies are part of the platform surface. A monoculture — every pool on one vulnerable compiler — is a correlated exposure, and correlated exposures belong in the platform vector, not in a footnote.
- "Audited" is a weak prior, not a verdict. Audits scope the contract. They do not certify the layer that compiled it. A scoring model that treats an audit as a large positive without asking what was in scope will misprice exactly the failures that hurt most.
- Off-pool concentration is on-pool risk. The pools were never insolvent, yet the most dangerous moment came from a borrower's collateral elsewhere. Control-vector signals — supply concentration, a protocol insider's leverage, single-entity dependencies — have to be scored even when they sit outside the contract being rated.
For comparison, here is how Philidor scores a representative live Curve-class vault today, with the platform and control vectors broken out — the two that this incident proved decisive:
Conclusion
The Vyper incident is remembered as a Curve hack. It is more useful as a statement about where risk actually lives: one layer below where audits look, and one hop beyond where pool-level analysis stops. The contracts did what they said. The compiler did not. And the worst-case scenario was never the $70M — it was the $100M of leverage that a falling CRV price could have detonated. A risk framework that scores only the contract in front of it would have missed both.
References
- Chainalysis — Curve Finance Pools Exploited Due to Code Vulnerabilities
- Halborn — Explained: The Vyper Bug Hack (July 2023)
- Hacken — Curve Finance Liquidity Pools Hack Explained
- TechCrunch — Curve Finance's $62M exploit exposes larger issues for DeFi
- CoinDesk — A Curve Founder's $168M Stash Is Under Stress
- CoinDesk — Curve Recoups 73% of Hacked Funds
- DL News — DeFi lenders hustle to limit exposure to Curve founder loans
- The Block — Curve founder Michael Egorov settles entire debt position on Aave