Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: m02 and liquidity #930

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions test/ModifyLiquidity.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
53 changes: 53 additions & 0 deletions test/PoolManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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(<tick>)
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,
Expand Down
6 changes: 4 additions & 2 deletions test/utils/Deployers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
);
}
Expand Down Expand Up @@ -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
);
}
Expand Down
Loading