Platform: Code4rena
Start Date: 28/11/2022
Pot Size: $192,500 USDC
Total HM: 33
Participants: 106
Period: 11 days
Judge: LSDan
Total Solo HM: 15
Id: 186
League: ETH
Rank: 15/106
Findings: 5
Award: $1,711.52
π Selected for report: 0
π Solo Findings: 0
π Selected for report: Franfran
Also found by: __141345__, poirots
1219.6604 USDC - $1,219.66
The oracleData
calculation takes into account the difference between token0 and token1 decimals. But the current implementation is inconsistent. If the lp pair has 2 different decimal tokens, the returned price could be way off.
In the case token1Decimal > token0Decimal
, the denominator is 1E9
, but if token1Decimal < token0Decimal
, the denominator becomes 10 ** (9 + oracleData.token0Decimal - oracleData.token1Decimal)
. The first treatment seems to be right, since the decimal difference is included in the nominator.
File: paraspace-core/contracts/misc/UniswapV3OracleWrapper.sol 221: function _getOracleData(UinswapV3PositionData memory positionData) 222: internal 223: view 224: returns (PairOracleData memory) 225: { 249: } else if (oracleData.token1Decimal > oracleData.token0Decimal) { 250: // multiple by 10^(decimalB - decimalA) to preserve price in wei 251: oracleData.sqrtPriceX96 = uint160( 252: (SqrtLib.sqrt( 253: (oracleData.token0Price * 254: (10 ** 255: (18 + 256: oracleData.token1Decimal - 257: oracleData.token0Decimal))) / 258: (oracleData.token1Price) 259: ) * 2**96) / 1E9 260: ); 261: } else { 262: // multiple by 10^(decimalA - decimalB) to preserve price in wei then divide by the same number 263: oracleData.sqrtPriceX96 = uint160( 264: (SqrtLib.sqrt( 265: (oracleData.token0Price * 266: (10 ** 267: (18 + 268: oracleData.token0Decimal - 269: oracleData.token1Decimal))) / 270: (oracleData.token1Price) 271: ) * 2**96) / 272: 10 ** 273: (9 + 274: oracleData.token0Decimal - 275: oracleData.token1Decimal) 276: ); 277: }
Manual analysis.
Change the last else block to:
} else { // multiple by 10^(decimalA - decimalB) to preserve price in wei then divide by the same number oracleData.sqrtPriceX96 = uint160( (SqrtLib.sqrt( (oracleData.token0Price * (10 ** (18 + oracleData.token0Decimal - oracleData.token1Decimal))) / (oracleData.token1Price) ) * 2**96) / 1E9 ); }
#0 - c4-judge
2022-12-20T16:38:36Z
dmvt marked the issue as duplicate of #237
#1 - c4-judge
2022-12-20T16:39:44Z
dmvt marked the issue as selected for report
#2 - c4-judge
2023-01-23T13:20:47Z
dmvt marked the issue as not selected for report
#3 - c4-judge
2023-01-23T13:22:05Z
dmvt marked the issue as partial-50
#4 - WalidOfNow
2023-01-25T18:33:45Z
the current decimal calculation correct. and the recommendation here is incorrect when i tried it.
π Selected for report: ladboy233
Also found by: Kong, Lambda, R2, __141345__, mahdikarimi
266.7397 USDC - $266.74
The total value locked sum of the pair is used to price the LP. But the reserves of the underlying can be easily influenced by flashloan, then the TVL can vary dramatically.Just like what happened before here Warp.
The price of the LP pair is determined by the TVL of the pool, given by:
amt0 * price0 + amt1 * price1
.
File: paraspace-core/contracts/misc/UniswapV3OracleWrapper.sol 156: function getTokenPrice(uint256 tokenId) public view returns (uint256) { 157: UinswapV3PositionData memory positionData = getOnchainPositionData( 158: tokenId 159: ); 160: 161: PairOracleData memory oracleData = _getOracleData(positionData); 162: 163: (uint256 liquidityAmount0, uint256 liquidityAmount1) = LiquidityAmounts 164: .getAmountsForLiquidity( 165: oracleData.sqrtPriceX96, 166: TickMath.getSqrtRatioAtTick(positionData.tickLower), 167: TickMath.getSqrtRatioAtTick(positionData.tickUpper), 168: positionData.liquidity 169: ); 170: 171: ( 172: uint256 feeAmount0, 173: uint256 feeAmount1 174: ) = getLpFeeAmountFromPositionData(positionData); 175: 176: return 177: (((liquidityAmount0 + feeAmount0) * oracleData.token0Price) / 178: 10**oracleData.token0Decimal) + 179: (((liquidityAmount1 + feeAmount1) * oracleData.token1Price) / 180: 10**oracleData.token1Decimal); 181: }
However, when a malicious user dumps large amount of any token into the pool, the whole TVL will be significantly increased, which leads to improper calculation of the price.
Manual analysis.
Update the TWAP LP price according to the "fair LP pricing" formula mentioned below:
#0 - c4-judge
2022-12-20T17:52:42Z
dmvt marked the issue as duplicate of #50
#1 - c4-judge
2023-01-09T16:43:39Z
dmvt changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-01-23T16:14:36Z
dmvt marked the issue as satisfactory
π Selected for report: carlitox477
Also found by: 0xDave, Jeiwan, Rolezn, __141345__, imare, nicobevi
102.8853 USDC - $102.89
https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/misc/NFTFloorOracle.sol#L362-L367 https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/misc/NFTFloorOracle.sol#L404-L407
For a newly added asset, the first price is always accepted without the requirement for MIN_ORACLES_NUM
or price deviation check. Hence if some feeder intended to abuse the system, or in case of some feeder get compromised, the NFT floor oracle could be utilized to return distort price. And the protocol could loss fund due to the wrong price.
Newly added asset has no prior price _priorTwap
or _updatedAt
. In setPrice()
, the 2 checks are in _checkValidity()
and _combine()
.
File: paraspace-core/contracts/misc/NFTFloorOracle.sol 351: function _checkValidity(address _asset, uint256 _twap) { 361: //first price is always valid 362: if (_priorTwap == 0 || _updatedAt == 0) { 363: return true; 364: } 397: function _combine(address _asset, uint256 _twap) { 404: //first time just use the feeding value 405: if (assetPriceMap[_asset].twap == 0) { 406: return (true, _twap); 407: }
A malicious or compromised feeder can monitor the new event of added asset, and immediately set the price abnormally high. Then by using the underlying NFT, the feeder can take loan way above the market price. The pool has to take the fund loss.
But in fact, it can be mitigated by just require the same number of feeds for new assets.
Manual analysis.
Enforce the MIN_ORACLES_NUM
requirement even if no prior price is set.
#0 - c4-judge
2022-12-20T16:46:23Z
dmvt marked the issue as duplicate of #28
#1 - c4-judge
2023-01-09T13:52:10Z
dmvt changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-01-23T20:36:54Z
dmvt marked the issue as partial-50
π Selected for report: IllIllI
Also found by: 0x52, 0xNazgul, Franfran, IllIllI, Jeiwan, Lambda, RaymondFam, Rolezn, Trust, __141345__, codecustard, erictee, gzeon, hansfriese, imare, rbserver, rvierdiiev, seyni, skinz, ujamal_
18.3064 USDC - $18.31
https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/protocol/libraries/logic/GenericLogic.sol#L152-L155 https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/misc/ParaSpaceOracle.sol#L127-L129
In calculateUserAccountData()
, each reserve needs to call external oracle for price feed. However, if any single one of the external oracle fails, the whole function will revert, causing DoS. The impacts could be the following:
In calculateUserAccountData()
, every reserve will call external oracle for price feed.
File: paraspace-core/contracts/protocol/libraries/logic/GenericLogic.sol 74: function calculateUserAccountData() { 152: vars.assetPrice = _getAssetPrice( 153: params.oracle, 154: vars.currentReserveAddress 155: );
But latestAnswer()
in Chainlink is depreciated, in the future it could stop the service anytime.
File: paraspace-core/contracts/misc/ParaSpaceOracle.sol 115: function getAssetPrice(address asset) 116: public 117: view 118: override 119: returns (uint256) 120: { 121: if (asset == BASE_CURRENCY) { 122: return BASE_CURRENCY_UNIT; 123: } 124: 125: uint256 price = 0; 126: IEACAggregatorProxy source = IEACAggregatorProxy(assetsSources[asset]); 127: if (address(source) != address(0)) { 128: price = uint256(source.latestAnswer()); 129: } 130: if (price == 0 && address(_fallbackOracle) != address(0)) { 131: price = _fallbackOracle.getAssetPrice(asset); 132: }
Manual analysis.
try/catch
for potential external call failure#0 - c4-judge
2022-12-20T17:44:57Z
dmvt marked the issue as duplicate of #5
#1 - c4-judge
2023-01-23T15:56:47Z
dmvt marked the issue as satisfactory
π Selected for report: IllIllI
Also found by: 0x52, 0xNazgul, Franfran, IllIllI, Jeiwan, Lambda, RaymondFam, Rolezn, Trust, __141345__, codecustard, erictee, gzeon, hansfriese, imare, rbserver, rvierdiiev, seyni, skinz, ujamal_
18.3064 USDC - $18.31
For ERC20, according to Chainlink's documentation, the latestAnswer()
function is deprecated.
The following could happen:
_fallbackOracle
is not set._fallbackOracle
uses uniswap v2 getAmountOut()
, it could be manipulated. Malicious user could monitor the Chainlink API, and start the price manipulation right after to steal fund from Paraspace.File: paraspace-core/contracts/misc/ParaSpaceOracle.sol 127: if (address(source) != address(0)) { 128: price = uint256(source.latestAnswer()); 129: } 130: if (price == 0 && address(_fallbackOracle) != address(0)) { 131: price = _fallbackOracle.getAssetPrice(asset); 132: }
But the _fallbackOracle
uses uniswap v2 function getAmountOut()
to return the price, which is vulnerable to manipulation. A malicious user could monitor the Chainlink support of the API, as soon as the service is not available, the uniswap v2 lp manipulation could follow immediately.
getAmountOut()
latestRoundData()
function to get the price instead. Add checks on the return data with proper revert messages if the price is stale or the round is incomplete, for example:(uint80 roundID, int256 price, , uint256 timeStamp, uint80 answeredInRound) = source.latestRoundData(); require(answeredInRound >= roundID, "..."); require(timeStamp != 0, "..."); require(price != 0, "...");
#0 - c4-judge
2022-12-20T17:45:02Z
dmvt marked the issue as duplicate of #5
#1 - c4-judge
2023-01-23T15:56:41Z
dmvt marked the issue as satisfactory
π Selected for report: IllIllI
Also found by: 0x4non, 0x52, 0xAgro, 0xNazgul, 0xSmartContract, 0xackermann, 9svR6w, Awesome, Aymen0909, B2, BRONZEDISC, Bnke0x0, Deekshith99, Deivitto, Diana, Dravee, HE1M, Jeiwan, Kaiziron, KingNFT, Lambda, Mukund, PaludoX0, RaymondFam, Rolezn, Sathish9098, Secureverse, SmartSek, __141345__, ahmedov, ayeslick, brgltd, cccz, ch0bu, chrisdior4, cryptonue, cryptostellar5, csanuragjain, datapunk, delfin454000, erictee, gz627, gzeon, helios, i_got_hacked, ignacio, imare, jadezti, jayphbee, joestakey, kankodu, ksk2345, ladboy233, martin, nadin, nicobevi, oyc_109, pashov, pavankv, pedr02b2, pzeus, rbserver, ronnyx2017, rvierdiiev, shark, unforgiven, xiaoming90, yjrwkk
103.9175 USDC - $103.92
ERC20 lib in ERC721 contract.
File: paraspace-core/contracts/protocol/tokenization/NToken.sol 20: import {SafeERC20} from "../../dependencies/openzeppelin/contracts/SafeERC20.sol";
The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting. If the block is an empty if-statement block to avoid doing subsequent checks in the else-if/else conditions, the else-if/else conditions should be nested under the negation of the if-statement, because they involve different classes of checks, which may lead to the introduction of errors when the code is later modified.
Instances number of this issue:
File: paraspace-core/contracts/protocol/tokenization/NTokenUniswapV3.sol 149: receive() external payable {} 150: }
When changing critical state variables events are not emitted. Emitting events allows monitoring activities with off-chain monitoring tools.
Instances number of this issue: 1
File: protocol/configuration/PoolAddressesProvider.sol 56: function setMarketId(string memory newMarketId) 57: external 58: override 59: onlyOwner 60: { 61: _setMarketId(newMarketId); 62: }
Suggestion: Consider emitting events after sensitive changes take place, to facilitate tracking and notify off-chain clients following the contractβs activity.
Currently it does not allow repay in full on behalf of other users.
File: paraspace-core/contracts/protocol/libraries/logic/ValidationLogic.sol 378: require( 379: amountSent != type(uint256).max || msg.sender == onBehalfOf, 380: Errors.NO_EXPLICIT_AMOUNT_TO_REPAY_ON_BEHALF 381: );
However, from business logic it does not seem to have any bad effects to allow repaying in full for others.
Suggestion: Allow repay for others in full.
#0 - c4-judge
2023-01-25T16:10:44Z
dmvt marked the issue as grade-b