Skip to content

Commit

Permalink
gas optimizations for the full range hook
Browse files Browse the repository at this point in the history
  • Loading branch information
atiselsts committed Dec 10, 2023
1 parent 8864031 commit df8a14b
Show file tree
Hide file tree
Showing 11 changed files with 94 additions and 58 deletions.
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeAddInitialLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
412696
411324
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeAddLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
206962
205590
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeFirstSwap.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
154763
151098
1 change: 1 addition & 0 deletions .forge-snapshots/FullRangeLargeSwap.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
150173
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeRemoveLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
200095
200678
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
379287
374608
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeSecondSwap.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
112303
112638
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeSwap.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
153038
149373
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ If you’re interested in contributing please see the [contribution guidelines](
contracts/
----hooks/
----examples/
| FullRange.sol
| GeomeanOracle.sol
| LimitOrder.sol
| TWAMM.sol
Expand Down
31 changes: 20 additions & 11 deletions contracts/hooks/examples/FullRange.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,19 @@ contract FullRange is BaseHook, ILockCallback {

bytes internal constant ZERO_BYTES = bytes("");

/// @dev Min tick for full range with tick spacing of 60
int24 internal constant MIN_TICK = -887220;
/// @dev Max tick for full range with tick spacing of 60
/// @dev Set tick spacing to a large number that's <= type(int16).max
int24 internal constant TICK_SPACING = 0x7000;

/// @dev Min tick for full range with tick spacing of TICK_SPACING
int24 internal constant MIN_TICK = (TickMath.MIN_TICK / TICK_SPACING + 1) * TICK_SPACING;
/// @dev Max tick for full range with tick spacing of TICK_SPACING
int24 internal constant MAX_TICK = -MIN_TICK;

/// @dev TickMath.getSqrtRatioAtTick(MIN_TICK), cached for optimization
uint160 internal immutable MIN_SQRT_RATIO = TickMath.getSqrtRatioAtTick(MIN_TICK);
/// @dev TickMath.getSqrtRatioAtTick(MIN_TICK), cached for optimization
uint160 internal immutable MAX_SQRT_RATIO = TickMath.getSqrtRatioAtTick(MAX_TICK);

int256 internal constant MAX_INT = type(int256).max;
uint16 internal constant MINIMUM_LIQUIDITY = 1000;

Expand Down Expand Up @@ -109,7 +117,7 @@ contract FullRange is BaseHook, ILockCallback {
currency0: params.currency0,
currency1: params.currency1,
fee: params.fee,
tickSpacing: 60,
tickSpacing: TICK_SPACING,
hooks: IHooks(address(this))
});

Expand All @@ -125,8 +133,8 @@ contract FullRange is BaseHook, ILockCallback {

liquidity = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtRatioAtTick(MIN_TICK),
TickMath.getSqrtRatioAtTick(MAX_TICK),
MIN_SQRT_RATIO,
MAX_SQRT_RATIO,
params.amount0Desired,
params.amount1Desired
);
Expand Down Expand Up @@ -166,7 +174,7 @@ contract FullRange is BaseHook, ILockCallback {
currency0: params.currency0,
currency1: params.currency1,
fee: params.fee,
tickSpacing: 60,
tickSpacing: TICK_SPACING,
hooks: IHooks(address(this))
});

Expand Down Expand Up @@ -195,7 +203,7 @@ contract FullRange is BaseHook, ILockCallback {
override
returns (bytes4)
{
if (key.tickSpacing != 60) revert TickSpacingNotDefault();
if (key.tickSpacing != TICK_SPACING) revert TickSpacingNotDefault();

PoolId poolId = key.toId();

Expand Down Expand Up @@ -326,10 +334,11 @@ contract FullRange is BaseHook, ILockCallback {
ZERO_BYTES
);

// The final shift by 48 is equal to multiplying by sqrt(Q96) using unchecked math
uint160 newSqrtPriceX96 = (
FixedPointMathLib.sqrt(
FullMath.mulDiv(uint128(-balanceDelta.amount1()), FixedPoint96.Q96, uint128(-balanceDelta.amount0()))
) * FixedPointMathLib.sqrt(FixedPoint96.Q96)
) << 48
).toUint160();

(uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolId);
Expand All @@ -346,8 +355,8 @@ contract FullRange is BaseHook, ILockCallback {

uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts(
newSqrtPriceX96,
TickMath.getSqrtRatioAtTick(MIN_TICK),
TickMath.getSqrtRatioAtTick(MAX_TICK),
MIN_SQRT_RATIO,
MAX_SQRT_RATIO,
uint256(uint128(-balanceDelta.amount0())),
uint256(uint128(-balanceDelta.amount1()))
);
Expand Down
105 changes: 65 additions & 40 deletions test/FullRange.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol";
import {UniswapV4ERC20} from "../contracts/libraries/UniswapV4ERC20.sol";
import {FullMath} from "@uniswap/v4-core/contracts/libraries/FullMath.sol";
import {SafeCast} from "@uniswap/v4-core/contracts/libraries/SafeCast.sol";
import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol";

contract TestFullRange is Test, Deployers, GasSnapshot {
using PoolIdLibrary for PoolKey;
Expand Down Expand Up @@ -47,17 +48,17 @@ contract TestFullRange is Test, Deployers, GasSnapshot {
uint24 fee
);

/// @dev Min tick for full range with tick spacing of 60
int24 internal constant MIN_TICK = -887220;
/// @dev Max tick for full range with tick spacing of 60
int24 internal constant MAX_TICK = -MIN_TICK;

int24 constant TICK_SPACING = 60;
int24 constant TICK_SPACING = 0x7000;
uint16 constant LOCKED_LIQUIDITY = 1000;
uint256 constant MAX_DEADLINE = 12329839823;
uint256 constant MAX_TICK_LIQUIDITY = 11505069308564788430434325881101412;
uint8 constant DUST = 30;

/// @dev Min tick for full range with tick spacing of TICK_SPACING
int24 internal constant MIN_TICK = (TickMath.MIN_TICK / TICK_SPACING + 1) * TICK_SPACING;
/// @dev Max tick for full range with tick spacing of TICK_SPACING
int24 internal constant MAX_TICK = -MIN_TICK;

MockERC20 token0;
MockERC20 token1;
MockERC20 token2;
Expand Down Expand Up @@ -169,10 +170,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot {

assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY);

assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether);
assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 10 ether);
assertApproxEqAbs(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether, DUST);
assertApproxEqAbs(key.currency1.balanceOf(address(this)), prevBalance1 - 10 ether, DUST);

assertEq(liquidityTokenBal, 10 ether - LOCKED_LIQUIDITY);
assertApproxEqAbs(liquidityTokenBal, 10 ether - LOCKED_LIQUIDITY, DUST);
assertEq(hasAccruedFees, false);
}

Expand Down Expand Up @@ -235,10 +236,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot {

assertEq(manager.getLiquidity(idWithLiq), liquidityTokenBal + LOCKED_LIQUIDITY);

assertEq(keyWithLiq.currency0.balanceOfSelf(), prevBalance0 - 10 ether);
assertEq(keyWithLiq.currency1.balanceOfSelf(), prevBalance1 - 10 ether);
assertApproxEqAbs(keyWithLiq.currency0.balanceOfSelf(), prevBalance0 - 10 ether, DUST);
assertApproxEqAbs(keyWithLiq.currency1.balanceOfSelf(), prevBalance1 - 10 ether, DUST);

assertEq(liquidityTokenBal, prevLiquidityTokenBal + 10 ether);
assertApproxEqAbs(liquidityTokenBal, prevLiquidityTokenBal + 10 ether, DUST);
assertEq(hasAccruedFees, false);
}

Expand Down Expand Up @@ -266,14 +267,14 @@ contract TestFullRange is Test, Deployers, GasSnapshot {

uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this));

assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY);
assertEq(liquidityTokenBal, 10 ether - LOCKED_LIQUIDITY);
assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether);
assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 10 ether);
assertApproxEqAbs(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY, DUST);
assertApproxEqAbs(liquidityTokenBal, 10 ether - LOCKED_LIQUIDITY, DUST);
assertApproxEqAbs(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether, DUST);
assertApproxEqAbs(key.currency1.balanceOf(address(this)), prevBalance1 - 10 ether, DUST);

vm.expectEmit(true, true, true, true);
emit Swap(
id, address(swapRouter), 1 ether, -906610893880149131, 72045250990510446115798809072, 10 ether, -1901, 3000
id, address(swapRouter), 1 ether, -906610893880149131, 72045250990510446121024169824, 10 ether + 8, -1901, 3000
);

IPoolManager.SwapParams memory params =
Expand All @@ -287,7 +288,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot {

(bool hasAccruedFees,) = fullRange.poolInfo(id);

assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether - 1 ether);
assertApproxEqAbs(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether - 1 ether, DUST);
assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 9093389106119850869);
assertEq(hasAccruedFees, true);

Expand All @@ -301,7 +302,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot {
liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this));

assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY);
assertEq(liquidityTokenBal, 14546694553059925434 - LOCKED_LIQUIDITY);
assertEq(liquidityTokenBal, 14546694553059925446 - LOCKED_LIQUIDITY);
assertEq(hasAccruedFees, true);
}

Expand All @@ -310,7 +311,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot {

fullRange.addLiquidity(
FullRange.AddLiquidityParams(
key.currency0, key.currency1, 3000, 10 ether, 10 ether, 10 ether, 10 ether, address(this), MAX_DEADLINE
key.currency0, key.currency1, 3000, 10 ether, 10 ether, 10 ether - DUST, 10 ether - DUST, address(this), MAX_DEADLINE
)
);

Expand All @@ -324,7 +325,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot {
vm.expectRevert(FullRange.TooMuchSlippage.selector);
fullRange.addLiquidity(
FullRange.AddLiquidityParams(
key.currency0, key.currency1, 3000, 10 ether, 10 ether, 10 ether, 10 ether, address(this), MAX_DEADLINE
key.currency0, key.currency1, 3000, 10 ether, 10 ether, 10 ether - DUST, 10 ether - DUST, address(this), MAX_DEADLINE
)
);
}
Expand Down Expand Up @@ -390,6 +391,30 @@ contract TestFullRange is Test, Deployers, GasSnapshot {
assertEq(hasAccruedFees, true);
}

function testFullRange_swap_LargeSwap() public {
PoolKey memory testKey = key;
manager.initialize(testKey, SQRT_RATIO_1_1, ZERO_BYTES);

fullRange.addLiquidity(
FullRange.AddLiquidityParams(
key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE
)
);

IPoolManager.SwapParams memory params =
IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100 ether, sqrtPriceLimitX96: TickMath.MIN_SQRT_RATIO + 1});
PoolSwapTest.TestSettings memory settings =
PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true});

snapStart("FullRangeLargeSwap");
swapRouter.swap(testKey, params, settings, ZERO_BYTES);
snapEnd();

(bool hasAccruedFees,) = fullRange.poolInfo(id);
assertEq(hasAccruedFees, true);
}


function testFullRange_removeLiquidity_InitialRemoveSucceeds() public {
uint256 prevBalance0 = keyWithLiq.currency0.balanceOfSelf();
uint256 prevBalance1 = keyWithLiq.currency1.balanceOfSelf();
Expand All @@ -409,9 +434,9 @@ contract TestFullRange is Test, Deployers, GasSnapshot {
uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this));

assertEq(manager.getLiquidity(idWithLiq), liquidityTokenBal + LOCKED_LIQUIDITY);
assertEq(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 99 ether - LOCKED_LIQUIDITY + 5);
assertEq(keyWithLiq.currency0.balanceOfSelf(), prevBalance0 + 1 ether - 1);
assertEq(keyWithLiq.currency1.balanceOfSelf(), prevBalance1 + 1 ether - 1);
assertApproxEqAbs(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 99 ether - LOCKED_LIQUIDITY, 3 * DUST);
assertApproxEqAbs(keyWithLiq.currency0.balanceOfSelf(), prevBalance0 + 1 ether, DUST);
assertApproxEqAbs(keyWithLiq.currency1.balanceOfSelf(), prevBalance1 + 1 ether, DUST);
assertEq(hasAccruedFees, false);
}

Expand Down Expand Up @@ -489,10 +514,10 @@ contract TestFullRange is Test, Deployers, GasSnapshot {

(, address liquidityToken) = fullRange.poolInfo(id);

assertEq(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 10 ether - LOCKED_LIQUIDITY);
assertApproxEqAbs(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 10 ether - LOCKED_LIQUIDITY, DUST);

assertEq(key.currency0.balanceOfSelf(), prevBalance0 - 10 ether);
assertEq(key.currency1.balanceOfSelf(), prevBalance1 - 10 ether);
assertApproxEqAbs(key.currency0.balanceOfSelf(), prevBalance0 - 10 ether, DUST);
assertApproxEqAbs(key.currency1.balanceOfSelf(), prevBalance1 - 10 ether, DUST);

UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max);

Expand All @@ -504,9 +529,9 @@ contract TestFullRange is Test, Deployers, GasSnapshot {
uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this));

assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY);
assertEq(liquidityTokenBal, 5 ether - LOCKED_LIQUIDITY);
assertEq(key.currency0.balanceOfSelf(), prevBalance0 - 5 ether - 1);
assertEq(key.currency1.balanceOfSelf(), prevBalance1 - 5 ether - 1);
assertApproxEqAbs(liquidityTokenBal, 5 ether - LOCKED_LIQUIDITY, DUST);
assertApproxEqAbs(key.currency0.balanceOfSelf(), prevBalance0 - 5 ether, DUST);
assertApproxEqAbs(key.currency1.balanceOfSelf(), prevBalance1 - 5 ether, DUST);
assertEq(hasAccruedFees, false);
}

Expand All @@ -522,23 +547,23 @@ contract TestFullRange is Test, Deployers, GasSnapshot {
)
);

assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether);
assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 10 ether);
assertApproxEqAbs(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether, DUST);
assertApproxEqAbs(key.currency1.balanceOf(address(this)), prevBalance1 - 10 ether, DUST);

(, address liquidityToken) = fullRange.poolInfo(id);

assertEq(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 10 ether - LOCKED_LIQUIDITY);
assertApproxEqAbs(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 10 ether - LOCKED_LIQUIDITY, DUST);

fullRange.addLiquidity(
FullRange.AddLiquidityParams(
key.currency0, key.currency1, 3000, 5 ether, 2.5 ether, 2 ether, 2 ether, address(this), MAX_DEADLINE
)
);

assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 12.5 ether);
assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 12.5 ether);
assertApproxEqAbs(key.currency0.balanceOf(address(this)), prevBalance0 - 12.5 ether, DUST);
assertApproxEqAbs(key.currency1.balanceOf(address(this)), prevBalance1 - 12.5 ether, DUST);

assertEq(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 12.5 ether - LOCKED_LIQUIDITY);
assertApproxEqAbs(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 12.5 ether - LOCKED_LIQUIDITY, DUST);

UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max);

Expand All @@ -549,9 +574,9 @@ contract TestFullRange is Test, Deployers, GasSnapshot {
uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this));

assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY);
assertEq(liquidityTokenBal, 7.5 ether - LOCKED_LIQUIDITY);
assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 7.5 ether - 1);
assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 7.5 ether - 1);
assertApproxEqAbs(liquidityTokenBal, 7.5 ether - LOCKED_LIQUIDITY, DUST);
assertApproxEqAbs(key.currency0.balanceOf(address(this)), prevBalance0 - 7.5 ether, DUST);
assertApproxEqAbs(key.currency1.balanceOf(address(this)), prevBalance1 - 7.5 ether, DUST);
}

function testFullRange_removeLiquidity_SwapAndRebalance() public {
Expand Down Expand Up @@ -706,7 +731,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot {

// PoolManager does not have any liquidity left over
assertTrue(manager.getLiquidity(id) >= LOCKED_LIQUIDITY);
assertTrue(manager.getLiquidity(id) < LOCKED_LIQUIDITY + DUST);
assertTrue(manager.getLiquidity(id) < LOCKED_LIQUIDITY + 266);

assertEq(hasAccruedFees, false);
}
Expand Down

0 comments on commit df8a14b

Please sign in to comment.