Curve Finance
Summary
ExpandThe codebase implements Curve's CryptoSwap invariant for two-coin volatile asset pools, paired with zap contracts for composing with stable base pools. The core AMM employs sophisticated mathematical routines—Newton's method for computing the invariant D and solving for balance y—alongside an internal exponential moving average (EMA) price oracle, dynamic fee calculation, and automatic price scale adjustment. The architecture cleanly separates the ERC20 LP token contract from the pool logic, while zap contracts serve as stateless intermediaries handling safe approvals and token routing. Security patterns throughout the codebase are mature: `@nonreentrant('lock')` guards protect all state-modifying external functions, admin operations enforce a 3-day timelock (`ADMIN_ACTIONS_DELAY`), A/gamma parameter ramping is bounded by `MAX_A_CHANGE = 10`, a time-limited kill switch provides emergency shutdown capability, and the Newton iteration math enforces strict convergence and safety bounds.
The audit identified 5 findings across the codebase: 1 high-severity, 1 medium-severity, and 3 low-severity issues. The most critical risk is a read-only reentrancy vector in the ETH-native pool variant (`CurveCryptoSwap2ETH.vy`), where `get_virtual_price()` lacks reentrancy protection and can be called mid-execution during ETH transfers in `remove_liquidity`, returning an inflated value exploitable by external protocols relying on it as a price oracle (H-1, L-2). This is a well-documented vulnerability class in Curve pools that has led to real-world exploits in composing protocols. The medium-severity finding (M-1) identifies a related oracle integrity concern: `virtual_price` manipulation through `balanceOf` gulp in `_claim_admin_fees`, which can inflate the `lp_price` oracle. The remaining low-severity findings address fee-on-transfer token incompatibility with internal balance accounting (L-1) and the absence of deadline parameters on swap and liquidity functions, which exposes users to transaction-holding attacks in mempool congestion scenarios (L-3).
The codebase exhibits strong defensive programming throughout, particularly in the mathematical library and core AMM logic. The separation of concerns between pool contracts, LP token contracts, and zap layers is well-executed, and the consistent application of reentrancy guards to state-modifying functions reflects a security-conscious development methodology.
Findings
2 issues identified
Read-only reentrancy via get_virtual_price() in ETH pool during liquidity removal
contracts/two/CurveCryptoSwap2ETH.vy:370
View Details
Description
The get_virtual_price() and lp_price() view functions lack @nonreentrant(‘lock’) protection. During remove_liquidity with use_eth=True, ETH is sent via raw_call before self.D is updated. The receiver gains execution control and can call get_virtual_price(), which computes using the old (larger) D divided by the already-reduced totalSupply, returning an inflated value. External protocols using this as a price oracle can be exploited.
Impact
External protocols using get_virtual_price() as an LP token price oracle read an inflated value during the reentrancy window. Attackers can borrow against LP collateral at inflated valuation or manipulate LP-token-denominated positions in composing protocols. This vulnerability class has been exploited in production.
Recommendation
Add @nonreentrant(‘lock’) decorator to both get_virtual_price() and lp_price() view functions so they revert when called during any locked state-modifying operation.
Fix
Adding @nonreentrant('lock') to the view functions causes them to check the reentrancy lock state. When called during any function that holds the 'lock' (exchange, add_liquidity, remove_liquidity, etc.), these view functions will revert, preventing external protocols from reading stale pricing data mid-state-update.
contracts/two/CurveCryptoSwap2ETH.vy
virtual_price manipulation via balanceOf gulp in _claim_admin_fees inflates lp_price oracle
contracts/two/CurveCryptoSwap2.vy:_claim_admin_fees (lines 349-380) and lp_price (lines 736-741)
View Details
Description
The _claim_admin_fees function updates internal balances directly from ERC20.balanceOf(self) (line 358), then recalculates self.D and self.virtual_price based on those balances (lines 375-377). The publicly callable claim_admin_fees() (line 620) triggers this code path. An attacker can donate tokens directly to the pool contract, then call claim_admin_fees() to force a gulp that inflates self.balances, self.D, and self.virtual_price. The lp_price() view function (line 736) and get_virtual_price() (line 345) both rely on self.virtual_price and return spot-manipulable values. Unlike the internal EMA price oracle which has time-weighted smoothing, virtual_price has no smoothing or deviation check and updates immediately. Any external protocol using lp_price() or get_virtual_price() as a collateral oracle is vulnerable to flash-loan-funded donation attacks.
Impact
An attacker can flash-loan tokens, donate them to the pool, call claim_admin_fees(), and inflate lp_price() / get_virtual_price() in a single transaction. External lending protocols relying on these functions for LP token valuation could be exploited to borrow against artificially inflated collateral.
Recommendation
Do not use lp_price() or get_virtual_price() as a spot oracle for collateral valuation. The internal EMA oracle (price_oracle()) has smoothing, but virtual_price does not. Consider adding a TWAP or EMA mechanism to virtual_price, or add a reentrancy-guarded check that virtual_price has not changed more than a threshold within the current block. Alternatively, track balances internally rather than gulping from balanceOf in _claim_admin_fees.
Conclusion
ExpandThe Curve Finance CryptoSwap two-coin pool codebase demonstrates high security maturity, evidenced by comprehensive reentrancy protection on state-modifying functions, timelocked administrative operations, bounded parameter ramping, and robust Newton iteration math with convergence assertions. Code quality is consistently high, with clear architectural boundaries and defensive patterns applied systematically rather than ad hoc. The zap contracts correctly delegate all security guarantees to the underlying pool contracts and maintain no persistent state of their own.
The most pressing concern is the read-only reentrancy surface on get_virtual_price() in the ETH-native pool variant (H-1), which endangers external protocols composing with the pool’s pricing functions rather than direct pool users. The _claim_admin_fees gulp vector (M-1) presents a subtler oracle manipulation path that warrants attention from any protocol consuming the lp_price oracle. Both issues are addressable without architectural changes—applying @nonreentrant to the view function and tightening the admin fee claim flow respectively. The fee-on-transfer incompatibility (L-1) and missing deadline parameters (L-3) are lower-risk but should be documented as known limitations or addressed in wrapper contracts.
With the high-severity reentrancy fix applied and the medium-severity oracle concern mitigated, the codebase is well-positioned for production deployment. The overall security posture reflects a battle-tested protocol infrastructure, and the identified issues are concentrated in composability edge cases rather than core invariant or fund-safety logic. We recommend applying the reentrancy fix to get_virtual_price() prior to deployment, evaluating the lp_price oracle path for manipulation resistance, and clearly documenting the fee-on-transfer and deadline limitations for integrating protocols.
Legal Disclaimer: This report covers the code submitted for analysis. It does not account for infrastructure, deployment configuration, third-party dependencies, or changes made after the audit date. Automated analysis may produce false positives or miss context-dependent vulnerabilities. audited.xyz provides this report “as is” without warranty of any kind.