diff --git a/test/ModifyLiquidity.t.sol b/test/ModifyLiquidity.t.sol index 007e41658..44e86bff4 100644 --- a/test/ModifyLiquidity.t.sol +++ b/test/ModifyLiquidity.t.sol @@ -312,4 +312,171 @@ contract ModifyLiquidityTest is Test, Logger, Deployers, JavascriptFfi, Fuzzers modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES); vm.snapshotGasLastCall("add liquidity to already existing position with salt"); } + + /// @dev verify that liquidity positions are not impacted by m02, at the upper tick + /// i.e. slot0.tick = -61 and slot0.sqrtPriceX96 = sqrtPriceAtTick(-60) + /// a user cannot adjust slot0.sqrtPriceX96 to withdraw 2 tokens on the range [-120, -60] + /// because a small trade will realign the tick to -60 + function test_modifyLiquidity_m02_tickUpper() public { + // fee-less pool to ensure liquidity withdrawals are not impacted by fees + (simpleKey, simplePoolId) = initPool(currency0, currency1, IHooks(address(0)), 0, 60, SQRT_PRICE_1_1); + + // Add to range [-120, 120] + modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES); + + // Add to range [-120, -60] + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + LIQ_PARAM_SALT.tickLower = -120; + LIQ_PARAM_SALT.tickUpper = -60; + LIQ_PARAM_SALT.liquidityDelta = 1e18; + modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES); + + // paid token1 + assertGt(balance1Before, currency1.balanceOfSelf()); + // did not pay token0 + assertEq(balance0Before, currency0.balanceOfSelf()); + + // push the price to tick = -60 + IPoolManager.SwapParams memory swapParams = IPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: -10_000e18, + sqrtPriceLimitX96: TickMath.getSqrtPriceAtTick(-60) + }); + swapRouter.swap(simpleKey, swapParams, SWAP_SETTINGS, ZERO_BYTES); + + // validate m02: tick and sqrtPriceX96 disagree + (uint160 sqrtPriceX96, int24 tick,,) = manager.getSlot0(simplePoolId); + assertEq(tick, -61); + assertEq(sqrtPriceX96, TickMath.getSqrtPriceAtTick(-60)); + assertEq(TickMath.getTickAtSqrtPrice(sqrtPriceX96), -60); + + // push the price just slightly + swapParams.zeroForOne = false; + swapParams.amountSpecified = -1 wei; + swapParams.sqrtPriceLimitX96 = MAX_PRICE_LIMIT; + swapRouter.swap(simpleKey, swapParams, SWAP_SETTINGS, ZERO_BYTES); + + // even with a 1 wei trade, the tick realigns + (sqrtPriceX96, tick,,) = manager.getSlot0(simplePoolId); + assertEq(tick, -60); + assertEq(TickMath.getTickAtSqrtPrice(sqrtPriceX96), -60); + + // withdraw liquidity and receive token1 only + balance0Before = currency0.balanceOfSelf(); + balance1Before = currency1.balanceOfSelf(); + LIQ_PARAM_SALT.liquidityDelta = -1e18; + modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES); + + // receive token1 + assertGt(currency1.balanceOfSelf(), balance1Before); + // did not receive token0 + assertEq(balance0Before, currency0.balanceOfSelf()); + } + + /// @dev verify that liquidity positions are not impacted by m02, at the upper tick + /// i.e. slot0.tick = -61 and slot0.sqrtPriceX96 = sqrtPriceAtTick(-60) + /// when withdrawing liquidity on the range [-120, -60], it is not possible to withdraw 2 tokens + function test_modifyLiquidity_m02_tickUpper_withoutSwap() public { + // fee-less pool to ensure liquidity withdrawals are not impacted by fees + (simpleKey, simplePoolId) = initPool(currency0, currency1, IHooks(address(0)), 0, 60, SQRT_PRICE_1_1); + + // Add to range [-120, 120] + modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES); + + // Add to range [-120, -60] + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + LIQ_PARAM_SALT.tickLower = -120; + LIQ_PARAM_SALT.tickUpper = -60; + LIQ_PARAM_SALT.liquidityDelta = 1e18; + modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES); + + // paid token1 + assertGt(balance1Before, currency1.balanceOfSelf()); + // did not pay token0 + assertEq(balance0Before, currency0.balanceOfSelf()); + + // push the price to tick = -60 + IPoolManager.SwapParams memory swapParams = IPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: -200e18, + sqrtPriceLimitX96: TickMath.getSqrtPriceAtTick(-60) + }); + swapRouter.swap(simpleKey, swapParams, SWAP_SETTINGS, ZERO_BYTES); + + // validate m02: tick and sqrtPriceX96 disagree + (uint160 sqrtPriceX96, int24 tick,,) = manager.getSlot0(simplePoolId); + assertEq(tick, -61); + assertEq(sqrtPriceX96, TickMath.getSqrtPriceAtTick(-60)); + assertEq(TickMath.getTickAtSqrtPrice(sqrtPriceX96), -60); + + // withdraw liquidity and receive token1 only + balance0Before = currency0.balanceOfSelf(); + balance1Before = currency1.balanceOfSelf(); + LIQ_PARAM_SALT.liquidityDelta = -1e18; + modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES); + + // receive token1 + assertGt(currency1.balanceOfSelf(), balance1Before); + // did not receive token0 + assertEq(balance0Before, currency0.balanceOfSelf()); + } + + /// @dev verify that liquidity positions are not impacted by m02 on tickLower + /// LP range [60, 120], tick = 59, sqrtPriceX96 = sqrtPriceAtTick(60) + function test_modifyLiquidity_m02_tickLower() public { + // fee-less pool to ensure liquidity withdrawals are not impacted by fees + (simpleKey, simplePoolId) = initPool(currency0, currency1, IHooks(address(0)), 0, 60, SQRT_PRICE_1_1); + + // Add to range [-120, 120] + modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES); + + // Add to range [60, 120] + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + LIQ_PARAM_SALT.tickLower = 60; + LIQ_PARAM_SALT.tickUpper = 120; + LIQ_PARAM_SALT.liquidityDelta = 1e18; + modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES); + + // paid token0 + assertGt(balance0Before, currency0.balanceOfSelf()); + // did not pay token1 + assertEq(balance1Before, currency1.balanceOfSelf()); + + // push the price to tick = 61 + IPoolManager.SwapParams memory swapParams = IPoolManager.SwapParams({ + zeroForOne: false, + amountSpecified: -20000e18, + sqrtPriceLimitX96: TickMath.getSqrtPriceAtTick(61) + }); + swapRouter.swap(simpleKey, swapParams, SWAP_SETTINGS, ZERO_BYTES); + (uint160 sqrtPriceX96, int24 tick,,) = manager.getSlot0(simplePoolId); + assertEq(tick, 61); + assertEq(sqrtPriceX96, TickMath.getSqrtPriceAtTick(61)); + assertEq(TickMath.getTickAtSqrtPrice(sqrtPriceX96), 61); + + // push the price to tick = 60, but tick is set to 59 because of m02 behavior + swapParams.zeroForOne = true; + swapParams.sqrtPriceLimitX96 = TickMath.getSqrtPriceAtTick(60); + swapRouter.swap(simpleKey, swapParams, SWAP_SETTINGS, ZERO_BYTES); + + // validate m02: tick and sqrtPriceX96 disagree + (sqrtPriceX96, tick,,) = manager.getSlot0(simplePoolId); + assertEq(tick, 59); + assertEq(sqrtPriceX96, TickMath.getSqrtPriceAtTick(60)); + assertEq(TickMath.getTickAtSqrtPrice(sqrtPriceX96), 60); + + // withdraw liquidity and receive token0 only + balance0Before = currency0.balanceOfSelf(); + balance1Before = currency1.balanceOfSelf(); + LIQ_PARAM_SALT.liquidityDelta = -1e18; + modifyLiquidityRouter.modifyLiquidity(simpleKey, LIQ_PARAM_SALT, ZERO_BYTES); + + // receive token0 + assertGt(currency0.balanceOfSelf(), balance0Before); + // did not receive token1 + assertEq(balance1Before, currency1.balanceOfSelf()); + } } diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index de050bf60..aa44585e0 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -1198,6 +1198,59 @@ contract PoolManagerTest is Test, Deployers { nestedActionRouter.unlock(abi.encode(_actions)); } + /// @dev verify that m02 behavior only occurs for zeroForOne trades (moving tick towards negative infinity) + /// and only occurs when sqrtPriceLimit is equal to getSqrtPriceAtTick() + function test_slot0_tick_sqrtPrice_m02(bool zeroForOne, int8 tickOffset) public { + PoolId poolId = key.toId(); + (, int24 currentTick,,) = manager.getSlot0(poolId); + assertEq(key.tickSpacing, 60); + assertEq(currentTick, 0); + + // Add full range liquidity + LIQUIDITY_PARAMS.tickLower = TickMath.minUsableTick(key.tickSpacing); + LIQUIDITY_PARAMS.tickUpper = TickMath.maxUsableTick(key.tickSpacing); + modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES); + + // Create positions such that [-240, -120, -60, 120, 180, 300] are initialized + LIQUIDITY_PARAMS.tickLower = -240; + LIQUIDITY_PARAMS.tickUpper = -120; + modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES); + + LIQUIDITY_PARAMS.tickLower = -60; + LIQUIDITY_PARAMS.tickUpper = 120; + modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES); + + LIQUIDITY_PARAMS.tickLower = 180; + LIQUIDITY_PARAMS.tickUpper = 300; + modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES); + + // fuzz target tick 180 +/- 128 + int24 targetTick = zeroForOne ? -int24(180) : int24(180); + targetTick += int24(tickOffset); + + uint160 targetSqrtPrice = TickMath.getSqrtPriceAtTick(targetTick); + IPoolManager.SwapParams memory swapParams = IPoolManager.SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: -100_000_000e18, + sqrtPriceLimitX96: targetSqrtPrice + }); + swapRouter.swap(key, swapParams, SWAP_SETTINGS, ZERO_BYTES); + + (uint160 sqrtPriceX96, int24 tick,,) = manager.getSlot0(poolId); + if (zeroForOne && (targetTick == -240 || targetTick == -120 || targetTick == -60)) { + // for zeroForOne trades (moving tick towards negative infinity), if the slot0.sqrtPrice lands exactly + // on a tick, the slot0.tick should be decremented by one + assertEq(tick, targetTick - 1, "M02 behavior"); + } else { + // non-M02 behavior where slot0.tick is pushed to the target tick + assertEq(tick, targetTick); + } + + // price (slot0.sqrtPriceX96) was pushed to the desired price + assertEq(sqrtPriceX96, targetSqrtPrice); + assertEq(targetTick, TickMath.getTickAtSqrtPrice(sqrtPriceX96)); + } + // function testExtsloadForPoolPrice() public { // IPoolManager.key = IPoolManager.PoolKey({ // currency0: currency0, diff --git a/test/utils/Deployers.sol b/test/utils/Deployers.sol index 88559148b..6a4fefcb7 100644 --- a/test/utils/Deployers.sol +++ b/test/utils/Deployers.sol @@ -48,6 +48,8 @@ contract Deployers is Test { IPoolManager.ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: -1e18, salt: 0}); IPoolManager.SwapParams public SWAP_PARAMS = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_PRICE_1_2}); + PoolSwapTest.TestSettings public SWAP_SETTINGS = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); // Global variables Currency internal currency0; @@ -227,7 +229,7 @@ contract Deployers is Test { amountSpecified: amountSpecified, sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT }), - PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), + SWAP_SETTINGS, hookData ); } @@ -272,7 +274,7 @@ contract Deployers is Test { amountSpecified: amountSpecified, sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT }), - PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), + SWAP_SETTINGS, hookData ); }