Skip to content

Commit

Permalink
use better bounds for liquidity (#688)
Browse files Browse the repository at this point in the history
* use better bounds for liquidity

* add comments

* update naming

* use min bound of 1

---------

Co-authored-by: Daniel Gretzke <[email protected]>
  • Loading branch information
snreynolds and gretzke authored May 23, 2024
1 parent 4ea21e0 commit e73be0a
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 33 deletions.
1 change: 0 additions & 1 deletion .forge-snapshots/removeLiquidity.snap

This file was deleted.

142 changes: 122 additions & 20 deletions src/test/Fuzzers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,97 @@ import {BalanceDelta} from "../types/BalanceDelta.sol";
import {TickMath} from "../libraries/TickMath.sol";
import {Pool} from "../libraries/Pool.sol";
import {PoolModifyLiquidityTest} from "./PoolModifyLiquidityTest.sol";
import {LiquidityAmounts} from "../../test/utils/LiquidityAmounts.sol";
import {SafeCast} from "../../src/libraries/SafeCast.sol";

contract Fuzzers is StdUtils {
using SafeCast for uint256;

Vm internal constant _vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));

function boundLiquidityDelta(PoolKey memory key, int256 liquidityDelta) internal pure returns (int256) {
return bound(
liquidityDelta, 0.0000001e18, int256(uint256(Pool.tickSpacingToMaxLiquidityPerTick(key.tickSpacing)) / 2)
function boundLiquidityDelta(PoolKey memory key, int256 liquidityDeltaUnbounded, int256 liquidityMaxByAmount)
internal
pure
returns (int256)
{
int256 liquidityMaxPerTick = int256(uint256(Pool.tickSpacingToMaxLiquidityPerTick(key.tickSpacing)));

// Finally bound the seeded liquidity by either the max per tick, or by the amount allowed in the position range.
int256 liquidityMax = liquidityMaxByAmount > liquidityMaxPerTick ? liquidityMaxPerTick : liquidityMaxByAmount;
return bound(liquidityDeltaUnbounded, 1, liquidityMax);
}

// Uses tickSpacingToMaxLiquidityPerTick/2 as one of the possible bounds.
// Potentially adjust this value to be more strict for positions that touch the same tick.
function boundLiquidityDeltaTightly(PoolKey memory key, int256 liquidityDeltaUnbounded, int256 liquidityMaxByAmount)
internal
pure
returns (int256)
{
// Divide by half to bound liquidity more. TODO: Probably a better way to do this.
int256 liquidityMaxTightBound = int256(uint256(Pool.tickSpacingToMaxLiquidityPerTick(key.tickSpacing)) / 2);

// Finally bound the seeded liquidity by either the max per tick, or by the amount allowed in the position range.
int256 liquidityMax =
liquidityMaxByAmount > liquidityMaxTightBound ? liquidityMaxTightBound : liquidityMaxByAmount;
return bound(liquidityDeltaUnbounded, 1, liquidityMax);
}

function getLiquidityDeltaFromAmounts(int24 tickLower, int24 tickUpper, uint160 sqrtPriceX96)
internal
pure
returns (int256)
{
// First get the maximum amount0 and maximum amount1 that can be deposited at this range.
(uint256 maxAmount0, uint256 maxAmount1) = LiquidityAmounts.getAmountsForLiquidity(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(tickLower),
TickMath.getSqrtPriceAtTick(tickUpper),
uint128(type(int128).max)
);

// Compare the max amounts (defined by the range of the position) to the max amount constrained by the type container.
// The true maximum should be the minimum of the two.
// (ie If the position range allows a deposit of more then int128.max in any token, then here we cap it at int128.max.)

uint256 amount0 = uint256(type(uint128).max / 2);
uint256 amount1 = uint256(type(uint128).max / 2);

maxAmount0 = maxAmount0 > amount0 ? amount0 : maxAmount0;
maxAmount1 = maxAmount1 > amount1 ? amount1 : maxAmount1;

int256 liquidityMaxByAmount = uint256(
LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(tickLower),
TickMath.getSqrtPriceAtTick(tickUpper),
maxAmount0,
maxAmount1
)
).toInt256();

return liquidityMaxByAmount;
}

function boundTicks(PoolKey memory key, int24 tickLower, int24 tickUpper) internal pure returns (int24, int24) {
function boundTicks(int24 tickLower, int24 tickUpper, int24 tickSpacing) internal pure returns (int24, int24) {
tickLower = int24(
bound(
int256(tickLower),
int256(TickMath.minUsableTick(key.tickSpacing)),
int256(TickMath.maxUsableTick(key.tickSpacing))
int256(TickMath.minUsableTick(tickSpacing)),
int256(TickMath.maxUsableTick(tickSpacing))
)
);
tickUpper = int24(
bound(
int256(tickUpper),
int256(TickMath.minUsableTick(key.tickSpacing)),
int256(TickMath.maxUsableTick(key.tickSpacing))
int256(TickMath.minUsableTick(tickSpacing)),
int256(TickMath.maxUsableTick(tickSpacing))
)
);

// round down ticks
tickLower = (tickLower / key.tickSpacing) * key.tickSpacing;
tickUpper = (tickUpper / key.tickSpacing) * key.tickSpacing;
tickLower = (tickLower / tickSpacing) * tickSpacing;
tickUpper = (tickUpper / tickSpacing) * tickSpacing;

(tickLower, tickUpper) = tickLower < tickUpper ? (tickLower, tickUpper) : (tickUpper, tickLower);

Expand All @@ -47,26 +109,66 @@ contract Fuzzers is StdUtils {
return (tickLower, tickUpper);
}

/// @dev Obtain fuzzed parameters for creating liquidity
function boundTicks(PoolKey memory key, int24 tickLower, int24 tickUpper) internal pure returns (int24, int24) {
return boundTicks(tickLower, tickUpper, key.tickSpacing);
}

function createRandomSqrtPriceX96(PoolKey memory key, int256 seed) internal pure returns (uint160) {
int24 tickSpacing = key.tickSpacing;
int256 min = int256(TickMath.minUsableTick(tickSpacing));
int256 max = int256(TickMath.maxUsableTick(tickSpacing));
int256 randomTick = bound(seed, min, max);
return TickMath.getSqrtPriceAtTick(int24(randomTick));
}

/// @dev Obtain fuzzed and bounded parameters for creating liquidity
/// @param key The pool key
/// @param params IPoolManager.ModifyLiquidityParams
function createFuzzyLiquidityParams(PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params)
internal
pure
returns (IPoolManager.ModifyLiquidityParams memory result)
{
/// @param params IPoolManager.ModifyLiquidityParams Note that these parameters are unbounded
/// @param sqrtPriceX96 The current sqrt price
function createFuzzyLiquidityParams(
PoolKey memory key,
IPoolManager.ModifyLiquidityParams memory params,
uint160 sqrtPriceX96
) internal pure returns (IPoolManager.ModifyLiquidityParams memory result) {
(result.tickLower, result.tickUpper) = boundTicks(key, params.tickLower, params.tickUpper);
int256 liquidityDelta = boundLiquidityDelta(key, params.liquidityDelta);
result.liquidityDelta = liquidityDelta;
int256 liquidityDeltaFromAmounts =
getLiquidityDeltaFromAmounts(result.tickLower, result.tickUpper, sqrtPriceX96);
result.liquidityDelta = boundLiquidityDelta(key, params.liquidityDelta, liquidityDeltaFromAmounts);
}

// Creates liquidity parameters with a stricter bound. Should be used if multiple positions being intitialized on the pool, with potential for tick overlap.
function createFuzzyLiquidityParamsWithTightBound(
PoolKey memory key,
IPoolManager.ModifyLiquidityParams memory params,
uint160 sqrtPriceX96
) internal pure returns (IPoolManager.ModifyLiquidityParams memory result) {
(result.tickLower, result.tickUpper) = boundTicks(key, params.tickLower, params.tickUpper);
int256 liquidityDeltaFromAmounts =
getLiquidityDeltaFromAmounts(result.tickLower, result.tickUpper, sqrtPriceX96);

result.liquidityDelta = boundLiquidityDeltaTightly(key, params.liquidityDelta, liquidityDeltaFromAmounts);
}

function createFuzzyLiquidity(
PoolModifyLiquidityTest modifyLiquidityRouter,
PoolKey memory key,
IPoolManager.ModifyLiquidityParams memory params,
uint160 sqrtPriceX96,
bytes memory hookData
) internal returns (IPoolManager.ModifyLiquidityParams memory result, BalanceDelta delta) {
result = createFuzzyLiquidityParams(key, params, sqrtPriceX96);
delta = modifyLiquidityRouter.modifyLiquidity(key, result, hookData);
}

// There exists possible positions in the pool, so we tighten the boundaries of liquidity.
function createFuzzyLiquidityWithTightBound(
PoolModifyLiquidityTest modifyLiquidityRouter,
PoolKey memory key,
IPoolManager.ModifyLiquidityParams memory params,
uint160 sqrtPriceX96,
bytes memory hookData
) internal returns (IPoolManager.ModifyLiquidityParams memory result, BalanceDelta delta) {
result = createFuzzyLiquidityParams(key, params);
result = createFuzzyLiquidityParamsWithTightBound(key, params, sqrtPriceX96);
delta = modifyLiquidityRouter.modifyLiquidity(key, result, hookData);
}
}
32 changes: 20 additions & 12 deletions test/libraries/StateLibrary.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ contract StateLibraryTest is Test, Deployers, Fuzzers, GasSnapshot {

function test_fuzz_getTickLiquidity(IPoolManager.ModifyLiquidityParams memory params) public {
(IPoolManager.ModifyLiquidityParams memory _params,) =
Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, params, ZERO_BYTES);
Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, params, SQRT_PRICE_1_1, ZERO_BYTES);
uint128 liquidityDelta = uint128(uint256(_params.liquidityDelta));

(uint128 liquidityGrossLower, int128 liquidityNetLower) =
Expand Down Expand Up @@ -107,9 +107,9 @@ contract StateLibraryTest is Test, Deployers, Fuzzers, GasSnapshot {
IPoolManager.ModifyLiquidityParams memory paramsB
) public {
(IPoolManager.ModifyLiquidityParams memory _paramsA,) =
Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, paramsA, ZERO_BYTES);
Fuzzers.createFuzzyLiquidityWithTightBound(modifyLiquidityRouter, key, paramsA, SQRT_PRICE_1_1, ZERO_BYTES);
(IPoolManager.ModifyLiquidityParams memory _paramsB,) =
Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, paramsB, ZERO_BYTES);
Fuzzers.createFuzzyLiquidityWithTightBound(modifyLiquidityRouter, key, paramsB, SQRT_PRICE_1_1, ZERO_BYTES);

uint128 liquidityDeltaA = uint128(uint256(_paramsA.liquidityDelta));
uint128 liquidityDeltaB = uint128(uint256(_paramsB.liquidityDelta));
Expand Down Expand Up @@ -225,7 +225,7 @@ contract StateLibraryTest is Test, Deployers, Fuzzers, GasSnapshot {

function test_fuzz_getLiquidity(IPoolManager.ModifyLiquidityParams memory params) public {
(IPoolManager.ModifyLiquidityParams memory _params,) =
Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, params, ZERO_BYTES);
Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, params, SQRT_PRICE_1_1, ZERO_BYTES);
(, int24 tick,,) = StateLibrary.getSlot0(manager, poolId);
uint128 liquidity = StateLibrary.getLiquidity(manager, poolId);

Expand Down Expand Up @@ -259,7 +259,7 @@ contract StateLibraryTest is Test, Deployers, Fuzzers, GasSnapshot {

function test_fuzz_getTickBitmap(IPoolManager.ModifyLiquidityParams memory params) public {
(IPoolManager.ModifyLiquidityParams memory _params,) =
Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, params, ZERO_BYTES);
Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, params, SQRT_PRICE_1_1, ZERO_BYTES);

(int16 wordPos, uint8 bitPos) = TickBitmap.position(_params.tickLower / key.tickSpacing);
(int16 wordPosUpper, uint8 bitPosUpper) = TickBitmap.position(_params.tickUpper / key.tickSpacing);
Expand Down Expand Up @@ -309,7 +309,7 @@ contract StateLibraryTest is Test, Deployers, Fuzzers, GasSnapshot {
bool zeroForOne
) public {
(IPoolManager.ModifyLiquidityParams memory _params, BalanceDelta delta) =
createFuzzyLiquidity(modifyLiquidityRouter, key, params, ZERO_BYTES);
createFuzzyLiquidity(modifyLiquidityRouter, key, params, SQRT_PRICE_1_1, ZERO_BYTES);

uint256 delta0 = uint256(int256(-delta.amount0()));
uint256 delta1 = uint256(int256(-delta.amount1()));
Expand Down Expand Up @@ -456,7 +456,7 @@ contract StateLibraryTest is Test, Deployers, Fuzzers, GasSnapshot {
);

(IPoolManager.ModifyLiquidityParams memory _params,) =
createFuzzyLiquidity(modifyLiquidityRouter, key, params, ZERO_BYTES);
createFuzzyLiquidity(modifyLiquidityRouter, key, params, SQRT_PRICE_1_1, ZERO_BYTES);

swap(key, zeroForOne, -int256(100e18), ZERO_BYTES);

Expand Down Expand Up @@ -498,12 +498,20 @@ contract StateLibraryTest is Test, Deployers, Fuzzers, GasSnapshot {
IPoolManager.ModifyLiquidityParams memory paramsA,
IPoolManager.ModifyLiquidityParams memory paramsB
) public {
(IPoolManager.ModifyLiquidityParams memory _paramsA,) =
Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, paramsA, ZERO_BYTES);
(IPoolManager.ModifyLiquidityParams memory _paramsB,) =
Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, paramsB, ZERO_BYTES);
(IPoolManager.ModifyLiquidityParams memory _paramsA) =
Fuzzers.createFuzzyLiquidityParams(key, paramsA, SQRT_PRICE_1_1);

(IPoolManager.ModifyLiquidityParams memory _paramsB) =
Fuzzers.createFuzzyLiquidityParams(key, paramsB, SQRT_PRICE_1_1);

// Assume there are no overlapping positions
vm.assume(
_paramsA.tickLower != _paramsB.tickLower && _paramsA.tickLower != _paramsB.tickUpper
&& _paramsB.tickLower != _paramsA.tickUpper && _paramsA.tickUpper != _paramsB.tickUpper
);

vm.assume(_paramsA.tickLower != _paramsB.tickLower && _paramsA.tickUpper != _paramsB.tickUpper);
modifyLiquidityRouter.modifyLiquidity(key, _paramsA, ZERO_BYTES);
modifyLiquidityRouter.modifyLiquidity(key, _paramsB, ZERO_BYTES);

bytes32 positionIdA = keccak256(
abi.encodePacked(address(modifyLiquidityRouter), _paramsA.tickLower, _paramsA.tickUpper, bytes32(0))
Expand Down

0 comments on commit e73be0a

Please sign in to comment.