Résumé
On 30 July 2023, several Curve Finance pools were drained of about $70 million, and the bug was not in Curve's code at all. It was in the compiler. Specific versions of Vyper, the programming language many Ethereum contracts are written in, generated a broken reentrancy guard, so the protection developers thought they had was silently not working. It is the rare and unsettling case of a reentrancy attack reaching contracts whose authors had correctly added the guard, because the tool that built them betrayed them. It put the spotlight on the compiler and the build pipeline as part of a smart contract's trust boundary.
How it happened
Curve's contracts used Vyper's @nonreentrant(key) decorator to prevent reentrancy, the exact bug class that destroyed The DAO in 2016. They did this correctly. The failure was underneath them, in the Vyper compiler itself (versions 0.2.15, 0.2.16, and 0.3.0; the flaw was fixed in 0.3.1, but vulnerable contracts stayed deployed for nearly two years).
A reentrancy lock works by setting a flag in one shared storage slot, so that any guarded function sees the flag and refuses to run while another is mid-execution. The buggy compiler gave each guarded function its own separate storage slot instead of sharing one slot per key. So every function got its own independent lock. The guard still stopped an attacker from re-entering the same function, but it completely failed to stop them re-entering a different guarded function, which is exactly what an attacker did, calling back into a second guarded function through the native-ETH transfer callback while balances were still mid-update, and draining the pool. Pools paired with wrapped ETH were safe because they have no such callback; the native-ETH pools were hit, with the biggest losses in the alETH/ETH pool (around $20 million), the CRV/ETH pool (around $18.5 million, exploited twice), pETH/ETH (around $11.5 million), and msETH/ETH, dragging in projects like Alchemix, JPEG'd, and Metronome.
The aftermath
Gross losses were around $70 million (some tallies put the total near $73.5 million once every affected project is summed). A large share was returned: white-hat actors and front-running (MEV) bots recovered funds, most famously the operator c0ffeebabe.eth, who captured 2,879 ETH (about $5.4 million) from the CRV/ETH pool through a flash-loan arbitrage that beat the attacker to it and handed it back to Curve, while the main attacker voluntarily returned millions more to Alchemix and JPEG'd, cutting net losses to roughly $52 million. There was a scarier second-order risk: the price of Curve's CRV token wobbled, threatening the roughly $110 million in loans its founder Michael Egorov had taken out against CRV collateral. The sharpest danger was a Fraxlend loan whose interest rate doubled every twelve hours, which would have forced a fire-sale of CRV and a cascading liquidation across DeFi; Egorov defused it by selling large blocks of CRV over-the-counter to a dozen buyers, and the cascade was narrowly avoided. It was a supply-chain attack in the truest sense, the compromised artifact was the compiler.
Why Curve still matters
Curve is the lesson where supply-chain risk meets reentrancy. The developers did everything right at the source level and were still exploited, because the compiler mis-generated their defence. The takeaway is that the compiler and build toolchain are part of your trust boundary, just like any dependency. Pin exact, audited compiler versions and verify the deployed bytecode against the audited source; track compiler security advisories and re-audit when a toolchain bug drops; treat the build pipeline as trusted supply chain with reproducible, signed builds; add explicit cross-function and read-only reentrancy guards rather than relying solely on a compiler-generated lock; and test reentrancy protection at the bytecode level, including native-ETH callback paths.
Comment le corriger
- Identify and pause every contract built with the affected Vyper versions, and redeploy from a fixed compiler with verified bytecode.
- Add explicit cross-function and read-only reentrancy protection rather than relying on the compiler-generated lock that failed.
- Trace funds and coordinate with white-hat recoverers; a meaningful share of the Curve loss was returned.
Comment l’éviter
- Pin exact, audited compiler versions and rebuild/verify deployed bytecode against the audited source before release.
- Track compiler security advisories and re-audit or migrate contracts when a toolchain-level bug is disclosed.
- Treat the compiler and build pipeline as part of the trusted supply chain: reproducible builds and signed releases.
- Add explicit cross-function reentrancy guards and read-only reentrancy checks rather than relying solely on compiler-generated locks.
- Test reentrancy protection at the bytecode level, including native-ETH transfer callback paths.
Références
Vulnérabilités liées
Tout Web3 →- CRITICALWEB3-PENPIE-2024
On September 3, 2024, Penpie, a yield protocol built on Pendle, was drained of about $27.3 million (11,113.6 ETH in wstETH, sUSDe, egETH and rswETH) across Ethereum and Arbitrum. The root cause was a cross-function reentrancy enabled by permissionless market registration: registerPenpiePool trusted any market from Pendle's PendleMarketFactoryV3 without validating the Standardized Yield (SY) token, so the attacker registered a fake market whose SY was their own contract. PendleStakingBaseUpg.batchHarvestMarketRewards (and its internal _harvestBatchMarketRewards) snapshotted reward-token balances before and after calling the market's redeemRewards, but lacked a nonReentrant guard. The malicious SY's claimRewards callback re-entered PendleStakingBaseUpg.depositMarket with flash-loaned Pendle LP tokens mid-accounting, so the deposit was misattributed as harvested rewards, inflating the attacker's reward balance. Although depositMarket itself carried a nonReentrant modifier, the two functions did not share a lock, so the unguarded harvest path let the attacker re-enter the guarded deposit path and claim the inflated rewards via MasterPenpie.multiclaim.
- CRITICALWEB3-CONIC-2023
On 21 July 2023 Conic Finance's ETH Omnipool on Ethereum lost roughly 1,700 ETH, about $3.6 million, to a read-only reentrancy attack. The attacker flash-loaned around $134 million, deposited into the Curve rETH pool, then called Curve's remove_liquidity(), which sends ETH to the recipient before the pool's totalSupply and balances are finalized, triggering the attacker contract's fallback during an inconsistent intermediate state. Inside that callback the attacker re-entered ConicEthPool.withdraw(), causing Conic's Curve LP oracle to value the LP token from Curve's virtual price and totalSupply while the pool was mid-operation, returning an inflated price. Conic's reentrancy guard was bypassed because its _isETH check assumed Curve v2 ETH pools list the native ETH placeholder address (0xEeee...EEeE) as a coin, whereas they actually use the WETH address, so the guard never fired. The inflated valuation let the attacker mint excess cncETH and withdraw more than deposited.
- CRITICALWEB3-EULER-2023
On 13 March 2023 the Ethereum lending protocol Euler Finance was drained of about $197 million, the biggest DeFi hack of the year. The attacker did not steal a key or break any cryptography. They borrowed a fortune with a flash loan, used a single missing safety check in Euler's code to deliberately push their own position into bad debt, and then exploited Euler's own liquidation rules to be paid far more than they were owed. In a now-familiar twist, the attacker, identifying only as "Jacob," gave more than all of it back over the following weeks. It is a clean lesson in DeFi's defining risk: composable money where one unchecked code path can be turned into a money pump.
- CRITICALWEB3-RARI-FEI-2022
On 30 April 2022 the Rari Capital / Fei Protocol Fuse lending pools on Ethereum lost approximately $80 million (about $79.7 million across ETH, FEI, DAI, LUSD and USDC). Fuse pools were a fork of Compound's CToken, but the CEther contract sent ETH using low-level call.value() instead of Compound's gas-capped transfer(), forwarding all remaining gas to the recipient's fallback. The borrow() function called doTransferOut(), which performed that call.value() ETH transfer to the borrower before the borrow and collateral accounting was finalized, violating checks-effects-interactions. The attacker's fallback re-entered the Comptroller's exitMarket() while the deposited collateral was still counted as backing the loan, freeing the collateral while keeping the borrowed ETH; the Comptroller's reentrancy guard did not cover exitMarket on the affected pools. Funded by Balancer flash loans, this cross-contract reentrancy drained seven pools.
- CRITICALWEB3-BEANSTALK-2022
On April 17, 2022, the Beanstalk stablecoin protocol was drained of about $182 million in a governance attack amplified by a flash loan, netting the attacker roughly $80 million after repaying the loan. The attacker borrowed about $1 billion across Aave and other venues (350M DAI, 500M USDC, 150M USDT plus BEAN and LUSD), deposited it into Curve to mint roughly 795M BEAN3CRV-f and 59M BEANLUSD-f LP tokens, and supplied them to Beanstalk's Silo to instantly hold a supermajority (over 78%, above the two-thirds threshold) of STALK governance power. Beanstalk's emergencyCommit path let a proposal pass once 24 hours had elapsed and a two-thirds vote existed; the attacker had pre-submitted a malicious BIP (BIP-18) whose init contract transferred the protocol's funds, then executed emergencyCommit in a single transaction. The core flaw was that voting power could be acquired flash-loan-instantly with no time-lock against single-block voting. Funds were laundered through Tornado Cash and never recovered; the attacker remains anonymous.
- CRITICALWEB3-GRIM-2021
On 18 December 2021 Grim Finance, a yield-optimizer vault protocol on Fantom, lost approximately $30 million. The vulnerable depositFor() function in the GrimBoostVault contract let the caller pass an arbitrary token address, pulled it via safeTransferFrom(), and computed the deposited amount as the balanceOf difference before and after the transfer to mint vault shares. The function had no reentrancy guard and did not whitelist the token, so the attacker supplied a malicious contract whose safeTransferFrom handed control flow back mid-execution, functionally an ERC-777-style pre-transfer hook. The attacker re-entered depositFor() five times before any frame finalized its share accounting; because each nested frame observed overlapping cumulative balance snapshots, the outer call was credited roughly five times the collateral actually deposited, over-minting shares against a flash-loaned position that was then redeemed to drain the vaults. This was an arbitrary-token-callback cross-function reentrancy.