Light ModeLight
Light ModeDark

One Bug Per Day

One H/M every day from top Wardens

Checkmark

Join over 1130 wardens!

Checkmark

Receive the email at any hour!

Ad

claim function lacks slippage controls for amount0 and amount1 returned by pool.burn function call

mediumCode4rena

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.

https://github.com/code-423n4/2024-06-vultisig/blob/58ebda57ccf6a74bdef2b88eb18a62ec4ad46112/src/ILOPool.sol#L184-L261

solidity
function 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

solidity
function 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