claim function lacks slippage controls for amount0 and amount1 returned by pool.burn function call
Lines of code
https://github.com/code-423n4/2024-06-vultisig/blob/58ebda57ccf6a74bdef2b88eb18a62ec4ad46112/src/ILOPool.sol#L184-L261 https://github.com/Uniswap/v3-periphery/blob/main/contracts/NonfungiblePositionManager.sol#L257-L306
Vulnerability details
Impact
Because the claim function does not have slippage controls for amount0 and amount1 returned by the pool.burn function call, the claim function call can suffer from price manipulation on the associated Uniswap v3 pool. If a price manipulation frontruns the claim transaction, the claimed token amounts can be much less than what they should be.
Proof of Concept
When calling the following claim function, there are no slippage controls for amount0 and amount1 returned by the pool.burn function call. This is unlike Uniswap's decreaseLiquidity function below that does execute require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check'), where amount0 and amount1 are also returned by the pool.burn function call. Thus, if a price manipulation on the associated Uniswap v3 pool frontruns the claim transaction, amount0 and amount1 can be much less than what they should be when the claim transaction is executed, which would cause the investor to claim token amounts that are much less than what they should be.
solidityfunction claim(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) returns (uint256 amount0, uint256 amount1) { // only can claim if the launch is successfully require(_launchSucceeded, "PNL"); // calculate amount of unlocked liquidity for the position uint128 liquidity2Claim = _claimableLiquidity(tokenId); IUniswapV3Pool pool = IUniswapV3Pool(_cachedUniV3PoolAddress); Position storage position = _positions[tokenId]; { IILOManager.Project memory _project = IILOManager(MANAGER).project(address(pool)); uint128 positionLiquidity = position.liquidity; require(positionLiquidity >= liquidity2Claim); // get amount of token0 and token1 that pool will return for us (amount0, amount1) = pool.burn(TICK_LOWER, TICK_UPPER, liquidity2Claim); // get amount of token0 and token1 after deduct platform fee (amount0, amount1) = _deductFees(amount0, amount1, _project.platformFee); bytes32 positionKey = PositionKey.compute(address(this), TICK_LOWER, TICK_UPPER); // calculate amount of fees that position generated (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); uint256 fees0 = FullMath.mulDiv( feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, positionLiquidity, FixedPoint128.Q128 ); uint256 fees1 = FullMath.mulDiv( feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, positionLiquidity, FixedPoint128.Q128 ); // amount of fees after deduct performance fee (fees0, fees1) = _deductFees(fees0, fees1, _project.performanceFee); // fees is combined with liquidity token amount to return to the user amount0 += fees0; amount1 += fees1; position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; // subtraction is safe because we checked positionLiquidity is gte liquidity2Claim position.liquidity = positionLiquidity - liquidity2Claim; ... } // real amount collected from uintswap pool (uint128 amountCollected0, uint128 amountCollected1) = pool.collect( address(this), TICK_LOWER, TICK_UPPER, type(uint128).max, type(uint128).max ); ... // transfer token for user TransferHelper.safeTransfer(_cachedPoolKey.token0, ownerOf(tokenId), amount0); TransferHelper.safeTransfer(_cachedPoolKey.token1, ownerOf(tokenId), amount1); ... address feeTaker = IILOManager(MANAGER).FEE_TAKER(); // transfer fee to fee taker TransferHelper.safeTransfer(_cachedPoolKey.token0, feeTaker, amountCollected0-amount0); TransferHelper.safeTransfer(_cachedPoolKey.token1, feeTaker, amountCollected1-amount1); }
https://github.com/Uniswap/v3-periphery/blob/main/contracts/NonfungiblePositionManager.sol#L257-L306
solidityfunction decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable override isAuthorizedForToken(params.tokenId) checkDeadline(params.deadline) returns (uint256 amount0, uint256 amount1) { require(params.liquidity > 0); Position storage position = _positions[params.tokenId]; uint128 positionLiquidity = position.liquidity; require(positionLiquidity >= params.liquidity); PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); (amount0, amount1) = pool.burn(position.tickLower, position.tickUpper, params.liquidity); require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check'); ... }
Tools Used
Manual Review
Recommended Mitigation Steps
The claim function can be updated to include slippage controls for amount0 and amount1 returned by the pool.burn function call like what Uniswap's decreaseLiquidity function does.
Assessed type
Invalid Validation
