Platform: Code4rena
Start Date: 05/07/2023
Pot Size: $390,000 USDC
Total HM: 136
Participants: 132
Period: about 1 month
Judge: LSDan
Total Solo HM: 56
Id: 261
League: ETH
Rank: 94/132
Findings: 1
Award: $76.55
🌟 Selected for report: 1
🚀 Solo Findings: 0
76.5537 USDC - $76.55
get_virtual_price()
was originally considered to be a manipulation-resistant price - suitable as a price oracle, but it was later found to be vulnerable to a read-only reentrancy attack, where the Curve contract could be put into a partially-modified state, and an attacker could gain control via the raw external call remove_liquidity()
makes. The attacker could use this to artificially inflate the price of the LP token/its balance, and use the inflated balance to take out loans which become undercollateralized at the end of the transaction, or to buy assets at exchange rates not actually available on the open market.
ARBTriCryptoOracle
calls get_virtual_price()
without calling any nonreentrant functions:
File: contracts/oracle/implementations/ARBTriCryptoOracle.sol 117 function _get() internal view returns (uint256 _maxPrice) { 118 @> uint256 _vp = TRI_CRYPTO.get_virtual_price(); 119 120 // Get the prices from chainlink and add 10 decimals 121 uint256 _btcPrice = uint256(BTC_FEED.latestAnswer()) * 1e10; 122 uint256 _wbtcPrice = uint256(WBTC_FEED.latestAnswer()) * 1e10; 123 uint256 _ethPrice = uint256(ETH_FEED.latestAnswer()) * 1e10; 124 uint256 _usdtPrice = uint256(USDT_FEED.latestAnswer()) * 1e10; 125 126 uint256 _minWbtcPrice = (_wbtcPrice < 1e18) 127 ? (_wbtcPrice * _btcPrice) / 1e18 128 : _btcPrice; 129 130 uint256 _basePrices = (_minWbtcPrice * _ethPrice * _usdtPrice); 131 132 _maxPrice = (3 * _vp * FixedPointMathLib.cbrt(_basePrices)) / 1 ether; 133 134 // ((A/A0) * (gamma/gamma0)**2) ** (1/3) 135 uint256 _g = (TRI_CRYPTO.gamma() * 1 ether) / GAMMA0; 136 uint256 _a = (TRI_CRYPTO.A() * 1 ether) / A0; 137 uint256 _discount = Math.max((_g ** 2 / 1 ether) * _a, 1e34); // handle qbrt nonconvergence 138 // if discount is small, we take an upper bound 139 _discount = (FixedPointMathLib.sqrt(_discount) * DISCOUNT0) / 1 ether; 140 141 _maxPrice -= (_maxPrice * _discount) / 1 ether; 142: }
_get()
is used by get()
, peek()
, and peekSpot()
, which are used by the Magnetar to price tokens.
IllIllI-bot
In order to protect against the attack, many protocols call uint256[2] calldata amts; ICurvePool(token).remove_liquidity(0, amts);
prior to calling get_virtual_price()
since calling remove_liquidity()
will ensure, via a reentrancy guard, that the user isn't currently manipulating the value, and since amounts are zero, it has no other effect. Another alternative is to call claim_admin_fees()
.
Reentrancy
#0 - c4-pre-sort
2023-08-05T06:47:10Z
minhquanym marked the issue as duplicate of #704
#1 - c4-judge
2023-09-13T08:57:56Z
dmvt marked the issue as satisfactory
#2 - c4-judge
2023-09-20T20:12:27Z
dmvt changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-10-08T11:45:59Z
dmvt marked the issue as selected for report