Files

559 lines
19 KiB
Solidity

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {stdError} from "forge-std/StdError.sol";
import {Constants} from "./utils/Constants.sol";
import {Pool} from "../src/libraries/Pool.sol";
import {TickMath} from "../src/libraries/TickMath.sol";
contract LiquidityMathRef {
function addDelta(uint128 x, int128 y) external pure returns (uint128) {
return y < 0 ? x - uint128(-y) : x + uint128(y);
}
function addDelta(bool upper, int128 liquidityNetBefore, int128 liquidityDelta)
external
pure
returns (int128 liquidityNet)
{
liquidityNet = upper ? liquidityNetBefore - liquidityDelta : liquidityNetBefore + liquidityDelta;
}
}
contract TickTest is Test {
using Pool for Pool.State;
int24 constant LOW_TICK_SPACING = 10;
int24 constant MEDIUM_TICK_SPACING = 60;
int24 constant HIGH_TICK_SPACING = 200;
Pool.State public pool;
LiquidityMathRef internal liquidityMath;
function setUp() public {
liquidityMath = new LiquidityMathRef();
}
function ticks(int24 tick) internal view returns (Pool.TickInfo memory) {
return pool.ticks[tick];
}
function tickBitmap(int16 word) internal view returns (uint256) {
return pool.tickBitmap[word];
}
function tickSpacingToMaxLiquidityPerTick(int24 tickSpacing) internal pure returns (uint128) {
return Pool.tickSpacingToMaxLiquidityPerTick(tickSpacing);
}
function setTick(int24 tick, Pool.TickInfo memory info) internal {
pool.ticks[tick] = info;
}
function setTickBitmap(int16 word, uint256 bitmap) internal {
pool.tickBitmap[word] = bitmap;
}
function getFeeGrowthInside(
int24 tickLower,
int24 tickUpper,
int24 tickCurrent,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128
) internal returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) {
pool.slot0 = pool.slot0.setTick(tickCurrent);
pool.feeGrowthGlobal0X128 = feeGrowthGlobal0X128;
pool.feeGrowthGlobal1X128 = feeGrowthGlobal1X128;
return pool.getFeeGrowthInside(tickLower, tickUpper);
}
function update(
int24 tick,
int24 tickCurrent,
int128 liquidityDelta,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128,
bool upper
) internal returns (bool flipped, uint128 liquidityGrossAfter) {
pool.slot0 = pool.slot0.setTick(tickCurrent);
pool.feeGrowthGlobal0X128 = feeGrowthGlobal0X128;
pool.feeGrowthGlobal1X128 = feeGrowthGlobal1X128;
return pool.updateTick(tick, liquidityDelta, upper);
}
function clear(int24 tick) internal {
pool.clearTick(tick);
}
function cross(int24 tick, uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128)
internal
returns (int128 liquidityNet)
{
return pool.crossTick(tick, feeGrowthGlobal0X128, feeGrowthGlobal1X128);
}
function getMinTick(int24 tickSpacing) internal pure returns (int256) {
return (TickMath.MIN_TICK / tickSpacing) * tickSpacing;
}
function getMaxTick(int24 tickSpacing) internal pure returns (int256) {
return (TickMath.MAX_TICK / tickSpacing) * tickSpacing;
}
function checkCantOverflow(int24 tickSpacing, uint128 maxLiquidityPerTick) internal pure {
assertLe(
uint256(
uint256(maxLiquidityPerTick)
* uint256((getMaxTick(tickSpacing) - getMinTick(tickSpacing)) / tickSpacing + 1)
),
uint256(Constants.MAX_UINT128)
);
}
function testTick_tickSpacingToMaxLiquidityPerTick_returnsTheCorrectValueForLowFee() public pure {
uint128 maxLiquidityPerTick = tickSpacingToMaxLiquidityPerTick(LOW_TICK_SPACING);
assertEq(maxLiquidityPerTick, 1917559095893846719543856547154045);
checkCantOverflow(LOW_TICK_SPACING, maxLiquidityPerTick);
}
function testTick_tickSpacingToMaxLiquidityPerTick_returnsTheCorrectValueForMediumFee() public pure {
uint128 maxLiquidityPerTick = tickSpacingToMaxLiquidityPerTick(MEDIUM_TICK_SPACING);
assertEq(maxLiquidityPerTick, 11505354575363080317263139282924270);
checkCantOverflow(MEDIUM_TICK_SPACING, maxLiquidityPerTick);
}
function testTick_tickSpacingToMaxLiquidityPerTick_returnsTheCorrectValueForHighFee() public pure {
uint128 maxLiquidityPerTick = tickSpacingToMaxLiquidityPerTick(HIGH_TICK_SPACING);
assertEq(maxLiquidityPerTick, 38345995821606768476828330790147420);
checkCantOverflow(HIGH_TICK_SPACING, maxLiquidityPerTick);
}
function testTick_tickSpacingToMaxLiquidityPerTick_returnsTheCorrectValueForMinTickSpacing() public pure {
uint128 maxLiquidityPerTick = tickSpacingToMaxLiquidityPerTick(TickMath.MIN_TICK_SPACING);
assertEq(maxLiquidityPerTick, 191757530477355301479181766273477);
checkCantOverflow(TickMath.MIN_TICK_SPACING, maxLiquidityPerTick);
}
function testTick_tickSpacingToMaxLiquidityPerTick_returnsTheCorrectValueForMaxTickSpacing() public pure {
uint128 maxLiquidityPerTick = tickSpacingToMaxLiquidityPerTick(TickMath.MAX_TICK_SPACING);
assertEq(maxLiquidityPerTick, 6076470837873901133274546561281575204);
checkCantOverflow(TickMath.MAX_TICK_SPACING, maxLiquidityPerTick);
}
function testTick_tickSpacingToMaxLiquidityPerTick_returnsTheCorrectValueForEntireRange() public pure {
uint128 maxLiquidityPerTick = tickSpacingToMaxLiquidityPerTick(TickMath.MAX_TICK);
assertEq(maxLiquidityPerTick, type(uint128).max / 3);
checkCantOverflow(TickMath.MAX_TICK, maxLiquidityPerTick);
}
function testTick_tickSpacingToMaxLiquidityPerTick_returnsTheCorrectValueFor2302() public pure {
uint128 maxLiquidityPerTick = tickSpacingToMaxLiquidityPerTick(2302);
assertEq(maxLiquidityPerTick, 440780268032303709149448973357212709);
checkCantOverflow(2302, maxLiquidityPerTick);
}
function testTick_tickSpacingToMaxLiquidityPerTick_gasCostMinTickSpacing() public {
vm.startSnapshotGas("tickSpacingToMaxLiquidityPerTick_gasCostMinTickSpacing");
tickSpacingToMaxLiquidityPerTick(TickMath.MIN_TICK_SPACING);
vm.stopSnapshotGas();
}
function testTick_tickSpacingToMaxLiquidityPerTick_gasCost60TickSpacing() public {
vm.startSnapshotGas("tickSpacingToMaxLiquidityPerTick_gasCost60TickSpacing");
tickSpacingToMaxLiquidityPerTick(60);
vm.stopSnapshotGas();
}
function testTick_tickSpacingToMaxLiquidityPerTick_gasCostMaxTickSpacing() public {
vm.startSnapshotGas("tickSpacingToMaxLiquidityPerTick_gasCostMaxTickSpacing");
tickSpacingToMaxLiquidityPerTick(TickMath.MAX_TICK_SPACING);
vm.stopSnapshotGas();
}
function testTick_getFeeGrowthInside_returnsAllForTwoUninitializedTicksIfTickIsInside() public {
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = getFeeGrowthInside(-2, 2, 0, 15, 15);
assertEq(feeGrowthInside0X128, 15);
assertEq(feeGrowthInside1X128, 15);
}
function testTick_getFeeGrowthInside_returns0ForTwoUninitializedTicksIfTickIsAbove() public {
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = getFeeGrowthInside(-2, 2, 4, 15, 15);
assertEq(feeGrowthInside0X128, 0);
assertEq(feeGrowthInside1X128, 0);
}
function testTick_getFeeGrowthInside_returns0ForTwoUninitializedTicksIfTickIsBelow() public {
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = getFeeGrowthInside(-2, 2, -4, 15, 15);
assertEq(feeGrowthInside0X128, 0);
assertEq(feeGrowthInside1X128, 0);
}
function testTick_getFeeGrowthInside_subtractsUpperTickIfBelow() public {
Pool.TickInfo memory info;
info.feeGrowthOutside0X128 = 2;
info.feeGrowthOutside1X128 = 3;
info.liquidityGross = 0;
info.liquidityNet = 0;
setTick(2, info);
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = getFeeGrowthInside(-2, 2, 0, 15, 15);
assertEq(feeGrowthInside0X128, 13);
assertEq(feeGrowthInside1X128, 12);
}
function testTick_getFeeGrowthInside_subtractsLowerTickIfAbove() public {
Pool.TickInfo memory info;
info.feeGrowthOutside0X128 = 2;
info.feeGrowthOutside1X128 = 3;
info.liquidityGross = 0;
info.liquidityNet = 0;
setTick(-2, info);
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = getFeeGrowthInside(-2, 2, 0, 15, 15);
assertEq(feeGrowthInside0X128, 13);
assertEq(feeGrowthInside1X128, 12);
}
function testTick_getFeeGrowthInside_subtractsUpperAndLowerTickIfInside() public {
Pool.TickInfo memory info;
info.feeGrowthOutside0X128 = 2;
info.feeGrowthOutside1X128 = 3;
info.liquidityGross = 0;
info.liquidityNet = 0;
setTick(-2, info);
info.feeGrowthOutside0X128 = 4;
info.feeGrowthOutside1X128 = 1;
info.liquidityGross = 0;
info.liquidityNet = 0;
setTick(2, info);
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = getFeeGrowthInside(-2, 2, 0, 15, 15);
assertEq(feeGrowthInside0X128, 9);
assertEq(feeGrowthInside1X128, 11);
}
function testTick_getFeeGrowthInside_worksCorrectlyWithOverflowOnInsideTick() public {
Pool.TickInfo memory info;
info.feeGrowthOutside0X128 = Constants.MAX_UINT256 - 3;
info.feeGrowthOutside1X128 = Constants.MAX_UINT256 - 2;
info.liquidityGross = 0;
info.liquidityNet = 0;
setTick(-2, info);
info.feeGrowthOutside0X128 = 3;
info.feeGrowthOutside1X128 = 5;
info.liquidityGross = 0;
info.liquidityNet = 0;
setTick(2, info);
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = getFeeGrowthInside(-2, 2, 0, 15, 15);
assertEq(feeGrowthInside0X128, 16);
assertEq(feeGrowthInside1X128, 13);
}
function testTick_update_flipsFromZeroToNonzero() public {
(bool flipped, uint128 liquidityGrossAfter) = update(0, 0, 1, 0, 0, false);
assertEq(flipped, true);
assertEq(liquidityGrossAfter, 1);
}
function testTick_update_doesNotFlipFromNonzeroToGreaterNonzero() public {
update(0, 0, 1, 0, 0, false);
(bool flipped, uint128 liquidityGrossAfter) = update(0, 0, 1, 0, 0, false);
assertEq(flipped, false);
assertEq(liquidityGrossAfter, 2);
}
function testTick_update_flipsFromNonzeroToZero() public {
update(0, 0, 1, 0, 0, false);
(bool flipped, uint128 liquidityGrossAfter) = update(0, 0, -1, 0, 0, false);
assertEq(flipped, true);
assertEq(liquidityGrossAfter, 0);
}
function testTick_update_doesNotFlipFromNonzeroToLesserZero() public {
update(0, 0, 2, 0, 0, false);
(bool flipped, uint128 liquidityGrossAfter) = update(0, 0, -1, 0, 0, false);
assertEq(flipped, false);
assertEq(liquidityGrossAfter, 1);
}
function testTick_update_netsTheLiquidityBasedOnUpperFlag() public {
Pool.TickInfo memory tickInfo;
update(0, 0, 2, 0, 0, false);
update(0, 0, 1, 0, 0, true);
update(0, 0, 3, 0, 0, true);
update(0, 0, 1, 0, 0, false);
tickInfo = ticks(0);
assertEq(tickInfo.liquidityGross, 2 + 1 + 3 + 1);
assertEq(tickInfo.liquidityNet, 2 - 1 - 3 + 1);
}
function testTick_update_revertsOnOverflowLiquidityGross() public {
update(0, 0, int128(Constants.MAX_UINT128 / 2 - 1), 0, 0, false);
vm.expectRevert();
update(0, 0, int128(Constants.MAX_UINT128 / 2 - 1), 0, 0, false);
}
function testTick_update_assumesAllGrowthHappensBelowTicksLteCurrentTick() public {
Pool.TickInfo memory tickInfo;
update(1, 1, 1, 1, 2, false);
tickInfo = ticks(1);
assertEq(tickInfo.feeGrowthOutside0X128, 1);
assertEq(tickInfo.feeGrowthOutside1X128, 2);
}
function testTick_update_doesNotSetAnyGrowthFieldsIfTickIsAlreadyInitialized() public {
Pool.TickInfo memory tickInfo;
update(1, 1, 1, 1, 2, false);
update(1, 1, 1, 6, 7, false);
tickInfo = ticks(1);
assertEq(tickInfo.feeGrowthOutside0X128, 1);
assertEq(tickInfo.feeGrowthOutside1X128, 2);
}
function testTick_update_doesNotSetAnyGrowthFieldsForTicksGtCurrentTick() public {
Pool.TickInfo memory tickInfo;
update(2, 1, 1, 1, 2, false);
tickInfo = ticks(2);
assertEq(tickInfo.feeGrowthOutside0X128, 0);
assertEq(tickInfo.feeGrowthOutside1X128, 0);
}
function testTick_update_liquidityParsing_parsesMaxUint128StoredLiquidityGrossBeforeUpdate() public {
Pool.TickInfo memory info;
info.feeGrowthOutside0X128 = 0;
info.feeGrowthOutside1X128 = 0;
info.liquidityGross = Constants.MAX_UINT128;
info.liquidityNet = 0;
setTick(2, info);
update(2, 1, -1, 1, 2, false);
info = ticks(2);
assertEq(info.liquidityGross, Constants.MAX_UINT128 - 1);
assertEq(info.liquidityNet, -1);
}
function testTick_update_liquidityParsing_parsesMaxUint128StoredLiquidityGrossAfterUpdate() public {
Pool.TickInfo memory info;
info.feeGrowthOutside0X128 = 0;
info.feeGrowthOutside1X128 = 0;
info.liquidityGross = (Constants.MAX_UINT128 / 2) + 1;
info.liquidityNet = 0;
setTick(2, info);
update(2, 1, int128(Constants.MAX_UINT128 / 2), 1, 2, false);
info = ticks(2);
assertEq(info.liquidityGross, Constants.MAX_UINT128);
assertEq(info.liquidityNet, int128(Constants.MAX_UINT128 / 2));
}
function testTick_update_liquidityParsing_parsesMaxInt128StoredLiquidityGrossBeforeUpdate() public {
Pool.TickInfo memory info;
info.feeGrowthOutside0X128 = 0;
info.feeGrowthOutside1X128 = 0;
info.liquidityGross = 1;
info.liquidityNet = int128(Constants.MAX_UINT128 / 2);
setTick(2, info);
update(2, 1, -1, 1, 2, false);
info = ticks(2);
assertEq(info.liquidityGross, 0);
assertEq(info.liquidityNet, int128(Constants.MAX_UINT128 / 2 - 1));
}
function testTick_update_liquidityParsing_parsesMaxInt128StoredLiquidityGrossAfterUpdate() public {
Pool.TickInfo memory info;
info.feeGrowthOutside0X128 = 0;
info.feeGrowthOutside1X128 = 0;
info.liquidityGross = 0;
info.liquidityNet = int128(Constants.MAX_UINT128 / 2 - 1);
setTick(2, info);
update(2, 1, 1, 1, 2, false);
info = ticks(2);
assertEq(info.liquidityGross, 1);
assertEq(info.liquidityNet, int128(Constants.MAX_UINT128 / 2));
}
function testTick_update_fuzz(uint128 liquidityGross, int128 liquidityNet, int128 liquidityDelta, bool upper)
public
{
try liquidityMath.addDelta(liquidityGross, liquidityDelta) returns (uint128 liquidityGrossAfter) {
try liquidityMath.addDelta(upper, liquidityNet, liquidityDelta) returns (int128 liquidityNetAfter) {
Pool.TickInfo memory info = Pool.TickInfo({
liquidityGross: liquidityGross,
liquidityNet: liquidityNet,
feeGrowthOutside0X128: 0,
feeGrowthOutside1X128: 0
});
setTick(2, info);
update({
tick: 2,
tickCurrent: 1,
liquidityDelta: liquidityDelta,
feeGrowthGlobal0X128: 0,
feeGrowthGlobal1X128: 0,
upper: upper
});
info = ticks(2);
assertEq(info.liquidityGross, liquidityGrossAfter);
assertEq(info.liquidityNet, liquidityNetAfter);
} catch (bytes memory reason) {
assertEq(reason, stdError.arithmeticError);
}
} catch (bytes memory reason) {
assertEq(reason, stdError.arithmeticError);
}
}
function testTick_clear_deletesAllTheDataInTheTick() public {
Pool.TickInfo memory info;
info.feeGrowthOutside0X128 = 1;
info.feeGrowthOutside1X128 = 2;
info.liquidityGross = 3;
info.liquidityNet = 4;
setTick(2, info);
clear(2);
info = ticks(2);
assertEq(info.feeGrowthOutside0X128, 0);
assertEq(info.feeGrowthOutside1X128, 0);
assertEq(info.liquidityGross, 0);
assertEq(info.liquidityNet, 0);
}
function testTick_cross_flipsTheGrowthVariables() public {
Pool.TickInfo memory info;
info.feeGrowthOutside0X128 = 1;
info.feeGrowthOutside1X128 = 2;
info.liquidityGross = 3;
info.liquidityNet = 4;
setTick(2, info);
cross(2, 7, 9);
info = ticks(2);
assertEq(info.feeGrowthOutside0X128, 6);
assertEq(info.feeGrowthOutside1X128, 7);
}
function testTick_cross_twoFlipsAreNoOp() public {
Pool.TickInfo memory info;
info.feeGrowthOutside0X128 = 1;
info.feeGrowthOutside1X128 = 2;
info.liquidityGross = 3;
info.liquidityNet = 4;
setTick(2, info);
cross(2, 7, 9);
cross(2, 7, 9);
info = ticks(2);
assertEq(info.feeGrowthOutside0X128, 1);
assertEq(info.feeGrowthOutside1X128, 2);
}
function test_getPoolTickInfo(int24 tick, Pool.TickInfo memory info) public {
setTick(tick, info);
Pool.TickInfo memory actualInfo = ticks(tick);
assertEq(actualInfo.liquidityGross, info.liquidityGross);
assertEq(actualInfo.liquidityNet, info.liquidityNet);
assertEq(actualInfo.feeGrowthOutside0X128, info.feeGrowthOutside0X128);
assertEq(actualInfo.feeGrowthOutside1X128, info.feeGrowthOutside1X128);
}
function test_getPoolBitmapInfo(int16 word, uint256 bitmap) public {
setTickBitmap(word, bitmap);
assertEq(tickBitmap(word), bitmap);
}
function testTick_tickSpacingToParametersInvariants_fuzz(int24 tickSpacing) public pure {
tickSpacing = int24(bound(tickSpacing, TickMath.MIN_TICK_SPACING, TickMath.MAX_TICK_SPACING));
int24 minTick = (TickMath.MIN_TICK / tickSpacing) * tickSpacing;
int24 maxTick = (TickMath.MAX_TICK / tickSpacing) * tickSpacing;
uint128 maxLiquidityPerTick = Pool.tickSpacingToMaxLiquidityPerTick(tickSpacing);
// symmetry around 0 tick
assertEq(maxTick, -minTick);
// positive max tick
assertGt(maxTick, 0);
// divisibility
assertEq((maxTick - minTick) % tickSpacing, 0);
uint256 numTicks = uint256(int256((maxTick - minTick) / tickSpacing)) + 1;
// sum of max liquidity on each tick is at most the cap
assertGe(type(uint128).max, uint256(maxLiquidityPerTick) * numTicks);
}
}