code-423n4 / 2024-06-panoptic-findings

1 stars 0 forks source link

Pool deployment can be DoS'd through price manipulation #37

Closed howlbot-integration[bot] closed 5 months ago

howlbot-integration[bot] commented 5 months ago

Lines of code

https://github.com/code-423n4/2024-06-panoptic/blob/153f0d82440b7e63075d55b0659706531431145f/contracts/PanopticFactory.sol#L230 https://github.com/code-423n4/2024-06-panoptic/blob/153f0d82440b7e63075d55b0659706531431145f/contracts/PanopticFactory.sol#L321

Vulnerability details

During Pool deployment, Uniswap's slot0.sqrtPriceX96 is being used to calculate required liquidity. This value represents the most recent price and can be easily manipulated.

function _mintFullRange(
        IUniswapV3Pool v3Pool,
        address token0,
        address token1,
        uint24 fee
    ) internal returns (uint256, uint256) {
@>      (uint160 currentSqrtPriceX96, , , , , , ) = v3Pool.slot0();

        // For full range: L = Δx * sqrt(P) = Δy / sqrt(P)
        // We start with fixed token amounts and apply this equation to calculate the liquidity
        // Note that for pools with a tickSpacing that is not a power of 2 or greater than 8 (887272 % ts != 0),
        // a position at the maximum and minimum allowable ticks will be wide, but not necessarily full-range.
        // In this case, the `fullRangeLiquidity` will always be an underestimate in respect to the token amounts required to mint.
        uint128 fullRangeLiquidity;
        unchecked {
            // Since we know one of the tokens is WETH, we simply add 0.1 ETH + worth in tokens
            if (token0 == WETH) {
                fullRangeLiquidity = uint128(
                    Math.mulDiv96RoundingUp(FULL_RANGE_LIQUIDITY_AMOUNT_WETH, currentSqrtPriceX96)
                );
            } else if (token1 == WETH) {
                fullRangeLiquidity = uint128(
                    Math.mulDivRoundingUp(
                        FULL_RANGE_LIQUIDITY_AMOUNT_WETH,
                        Constants.FP96,
                        currentSqrtPriceX96
                    )
                );
            } else {
                // Find the resulting liquidity for providing 1e6 of both tokens
                uint128 liquidity0 = uint128(
                    Math.mulDiv96RoundingUp(FULL_RANGE_LIQUIDITY_AMOUNT_TOKEN, currentSqrtPriceX96)
                );
                uint128 liquidity1 = uint128(
                    Math.mulDivRoundingUp(
                        FULL_RANGE_LIQUIDITY_AMOUNT_TOKEN,
                        Constants.FP96,
                        currentSqrtPriceX96
                    )
                );

                // Pick the greater of the liquidities - i.e the more "expensive" option
                // This ensures that the liquidity added is sufficiently large
                fullRangeLiquidity = liquidity0 > liquidity1 ? liquidity0 : liquidity1;
            }
        }

        // The maximum range we can mint is determined by the tickSpacing of the pool
        // The upper and lower ticks must be divisible by `tickSpacing`, so
        // tickSpacing = 1: tU/L = +/-887272
        // tickSpacing = 10: tU/L = +/-887270
        // tickSpacing = 60: tU/L = +/-887220
        // tickSpacing = 200: tU/L = +/-887200
        int24 tickLower;
        int24 tickUpper;
        unchecked {
            int24 tickSpacing = v3Pool.tickSpacing();
            tickLower = (Constants.MIN_V3POOL_TICK / tickSpacing) * tickSpacing;
            tickUpper = -tickLower;
        }

        bytes memory mintCallback = abi.encode(
            CallbackLib.CallbackData({
                poolFeatures: CallbackLib.PoolFeatures({token0: token0, token1: token1, fee: fee}),
                payer: msg.sender
            })
        );

        return
            IUniswapV3Pool(v3Pool).mint(
                address(this),
                tickLower,
                tickUpper,
                fullRangeLiquidity,
                mintCallback
            );
    }

There are slippage checks in PanopticFactory.sol to ensure that the user does not spend more token0 and token1 than intended :

        if (amount0 > amount0Max || amount1 > amount1Max) revert Errors.PriceBoundFail();

However, a malicious user can still DoS pool deployment by taking advantage of these strict slippage checks.

To make pool deployment revert, an attacker needs to manipulate the price to the extent that either amount0 > amount0Max or amount1 > amount1Max.

Attack path :

  1. Attacker observes transaction in mempool, and notes amount0Max and amount1Max values.
  2. The attacker front-runs the transaction and alters the Uniswap pool reserves to manipulate the price so that either amount0 or amount1 go past their slippage limits.
  3. Pool deployment will revert

One can argue that the amount0Max and amount1Max values can be set to a really large value to mitigate this. However, this is highly impractical as a higher token spend is beneficial to the attacker and causes a loss for the user.

Impact

DoS / Loss of funds

Proof of Concept

Tools Used

Manual Review

Recommended Mitigation Steps

Use TWAP price

Assessed type

DoS

Picodes commented 5 months ago

The cost of manipulating is way higher than the cost of just submitting the transaction, and it can just be redeployed so there is no upside in doing this

c4-judge commented 5 months ago

Picodes marked the issue as unsatisfactory: Insufficient proof