MEDIUM severity: Missing slippage protection when removing liquidity. **Description:** When a user removes liquidity, they can expect a specific amount of tokens in exchange; however, depending on the relative position of the price to the range from which the concentrated liquidity is being removed, this can be computed as single-sided liquidity in either token or a.....

Vulnerable code:
function _decrease(
        uint256 tokenId,
        uint256 liquidity,
@>      uint128 amount0Min,
@>      uint128 amount1Min,
        bytes calldata hookData
    ) internal onlyIfApproved(msgSender(), tokenId) {
        (PoolKey memory poolKey, PositionInfo info) = getPoolAndPositionInfo(tokenId);

        // Note: the tokenId is used as the salt.
        (BalanceDelta liquidityDelta, BalanceDelta feesAccrued) =
            _modifyLiquidity(info, poolKey, -(liquidity.toInt256()), bytes32(tokenId), hookData);
        // Slippage checks should be done on the principal liquidityDelta which is the liquidityDelta - feesAccrued
@>      (liquidityDelta - feesAccrued).validateMinOut(amount0Min, amount1Min);
    }

function test_MissingSlippageProtection() public {
    int24 lowerTick = -1020;
    int24 upperTick = 1020;
    int24 currentTick = 0;

    uint160 currentSqrtPrice = TickMath.getSqrtPriceAtTick(currentTick);

    initPool(key2.currency0, key2.currency1, IHooks(address(multiRange)), 1000, currentSqrtPrice);

    rangeKey = RangeKey(id2, lowerTick, upperTick);
    rangeId = rangeKey.toId();

    multiRange.createRange(key2, rangeKey);

    uint256 token0Amount = 100 ether;
    uint256 token1Amount = 100 ether;

    // User1 mints 100 tokens of each worth of liquidity when the price is between the range
    uint128 mintedLiquidity = multiRange.addLiquidity(
        MultiRangeHook.AddLiquidityParams(
            key2, rangeKey, token0Amount, token1Amount, 99 ether, 99 ether, address(this), MAX_DEADLINE
        )
    );

    IPoolManager.SwapParams memory params =
        IPoolManager.SwapParams({ zeroForOne: true, amountSpecified: -1000 ether, sqrtPriceLimitX96: TickMath.getSqrtPriceAtTick(lowerTick - 60) });
    HookEnabledSwapRouter.TestSettings memory settings =
        HookEnabledSwapRouter.TestSettings({ takeClaims: false, settleUsingBurn: false });

    // User1 is about to remove all the liquidity but gets frontrun by someone that moves the price out of the range
    router.swap(key2, params, settings, ZERO_BYTES);

    address liquidityToken = multiRange.rangeLpToken(rangeId);
    IncentivizedERC20(liquidityToken).approve(address(multiRange), type(uint256).max);

    MultiRangeHook.RemoveLiquidityParams memory removeLiquidityParams =
        MultiRangeHook.RemoveLiquidityParams(key2, rangeKey, mintedLiquidity - 1000, MAX_DEADLINE);

    // User1 expected similar amount of each tokens from what he deposited but instead they only receives an amount of token 0
    BalanceDelta delta = multiRange.removeLiquidity(removeLiquidityParams);

    console.log("Token 0 received from liquidity removal:", delta.amount0());
    console.log("Token 1 received from liquidity removal:", delta.amount1());
}

Ran 1 test for test/MultiRangeHook.t.sol:TestMultiRangeHook
[PASS] test_MissingSlippageProtection() (gas: 1988383)
Logs:
  Token 0 received from liquidity removal: 205337358362575417542
  Token 1 received from liquidity removal: 0

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.33ms (1.32ms CPU time)