From 285b53e36a076f419160ec3bd5d7f657b5633bae Mon Sep 17 00:00:00 2001 From: Vladislav Volosnikov Date: Fri, 17 May 2024 21:23:54 +0200 Subject: [PATCH] Use packed stack-based version of Slot0 structure (#613) * Add stack-based Slot0 structure version * Optimize initialization * Update snapshots * Remove memory-based structure * Simplify initialization * Change types for offsets Co-authored-by: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> * Change types for masks Co-authored-by: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> * Use signextend instead of INT24_MASK Co-authored-by: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> * Update snapshots * Update Slot0 type spec * Enhance comments for Slot0 * Add line with layout to Slot0 comments * Add tests for Slot0 * Add tests * Remove INT24_MASK * Update snapshots * Run fmt * Rename test * Rename masks * Remove Slot0 import in PoolManager --------- Co-authored-by: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> --- ...o already existing position with salt.snap | 2 +- .forge-snapshots/addLiquidity CA fee.snap | 2 +- .../addLiquidity with empty hook.snap | 2 +- .../addLiquidity with native token.snap | 2 +- ...new liquidity to a position with salt.snap | 2 +- .forge-snapshots/initialize.snap | 2 +- .../poolManager bytecode size.snap | 2 +- .forge-snapshots/removeLiquidity CA fee.snap | 2 +- .../removeLiquidity with empty hook.snap | 2 +- .../removeLiquidity with native token.snap | 2 +- ...dLiquidity second addition same range.snap | 2 +- .forge-snapshots/simple addLiquidity.snap | 2 +- ...emoveLiquidity some liquidity remains.snap | 2 +- .forge-snapshots/simple removeLiquidity.snap | 2 +- .forge-snapshots/simple swap with native.snap | 2 +- .forge-snapshots/simple swap.snap | 2 +- .../swap CA custom curve + swap noop.snap | 2 +- .../swap CA fee on unspecified.snap | 2 +- ...p against liquidity with native token.snap | 2 +- .forge-snapshots/swap against liquidity.snap | 2 +- .../swap burn 6909 for input.snap | 2 +- .../swap burn native 6909 for input.snap | 2 +- .../swap mint native output as 6909.snap | 2 +- .../swap mint output as 6909.snap | 2 +- ...wap skips hook call if hook is caller.snap | 2 +- .forge-snapshots/swap with dynamic fee.snap | 2 +- .forge-snapshots/swap with hooks.snap | 2 +- .../swap with lp fee and protocol fee.snap | 2 +- .../swap with return dynamic fee.snap | 2 +- .../update dynamic fee in before swap.snap | 2 +- src/libraries/Pool.sol | 60 +++++------ src/types/Slot0.sol | 101 ++++++++++++++++++ test/Tick.t.sol | 4 +- test/libraries/Pool.t.sol | 29 ++--- test/types/Slot0.t.sol | 36 +++++++ 35 files changed, 210 insertions(+), 80 deletions(-) create mode 100644 src/types/Slot0.sol create mode 100644 test/types/Slot0.t.sol diff --git a/.forge-snapshots/add liquidity to already existing position with salt.snap b/.forge-snapshots/add liquidity to already existing position with salt.snap index 3afdc7935..ff76e7f86 100644 --- a/.forge-snapshots/add liquidity to already existing position with salt.snap +++ b/.forge-snapshots/add liquidity to already existing position with salt.snap @@ -1 +1 @@ -153625 \ No newline at end of file +153643 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity CA fee.snap b/.forge-snapshots/addLiquidity CA fee.snap index 4fb0138e9..feb6a4a97 100644 --- a/.forge-snapshots/addLiquidity CA fee.snap +++ b/.forge-snapshots/addLiquidity CA fee.snap @@ -1 +1 @@ -331641 \ No newline at end of file +331635 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity with empty hook.snap b/.forge-snapshots/addLiquidity with empty hook.snap index 43ee3ebd8..a16c59cb1 100644 --- a/.forge-snapshots/addLiquidity with empty hook.snap +++ b/.forge-snapshots/addLiquidity with empty hook.snap @@ -1 +1 @@ -286634 \ No newline at end of file +286628 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity with native token.snap b/.forge-snapshots/addLiquidity with native token.snap index fda825709..c5bed833e 100644 --- a/.forge-snapshots/addLiquidity with native token.snap +++ b/.forge-snapshots/addLiquidity with native token.snap @@ -1 +1 @@ -143835 \ No newline at end of file +143853 \ No newline at end of file diff --git a/.forge-snapshots/create new liquidity to a position with salt.snap b/.forge-snapshots/create new liquidity to a position with salt.snap index d29431ac0..e8e6c2b6c 100644 --- a/.forge-snapshots/create new liquidity to a position with salt.snap +++ b/.forge-snapshots/create new liquidity to a position with salt.snap @@ -1 +1 @@ -302153 \ No newline at end of file +302147 \ No newline at end of file diff --git a/.forge-snapshots/initialize.snap b/.forge-snapshots/initialize.snap index 1e2144a88..489d5247b 100644 --- a/.forge-snapshots/initialize.snap +++ b/.forge-snapshots/initialize.snap @@ -1 +1 @@ -62194 \ No newline at end of file +61902 \ No newline at end of file diff --git a/.forge-snapshots/poolManager bytecode size.snap b/.forge-snapshots/poolManager bytecode size.snap index 5c1ff90c7..3be5b305b 100644 --- a/.forge-snapshots/poolManager bytecode size.snap +++ b/.forge-snapshots/poolManager bytecode size.snap @@ -1 +1 @@ -22083 \ No newline at end of file +21934 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity CA fee.snap b/.forge-snapshots/removeLiquidity CA fee.snap index cbba100bf..2a647bb5c 100644 --- a/.forge-snapshots/removeLiquidity CA fee.snap +++ b/.forge-snapshots/removeLiquidity CA fee.snap @@ -1 +1 @@ -187083 \ No newline at end of file +187101 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity with empty hook.snap b/.forge-snapshots/removeLiquidity with empty hook.snap index 7f928a82f..4c25874fc 100644 --- a/.forge-snapshots/removeLiquidity with empty hook.snap +++ b/.forge-snapshots/removeLiquidity with empty hook.snap @@ -1 +1 @@ -123451 \ No newline at end of file +123469 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity with native token.snap b/.forge-snapshots/removeLiquidity with native token.snap index 4eb8e2676..70dcdabf5 100644 --- a/.forge-snapshots/removeLiquidity with native token.snap +++ b/.forge-snapshots/removeLiquidity with native token.snap @@ -1 +1 @@ -120238 \ No newline at end of file +120256 \ No newline at end of file diff --git a/.forge-snapshots/simple addLiquidity second addition same range.snap b/.forge-snapshots/simple addLiquidity second addition same range.snap index 6273edbd1..ccf9f3450 100644 --- a/.forge-snapshots/simple addLiquidity second addition same range.snap +++ b/.forge-snapshots/simple addLiquidity second addition same range.snap @@ -1 +1 @@ -104914 \ No newline at end of file +104932 \ No newline at end of file diff --git a/.forge-snapshots/simple addLiquidity.snap b/.forge-snapshots/simple addLiquidity.snap index 724250539..54ae54908 100644 --- a/.forge-snapshots/simple addLiquidity.snap +++ b/.forge-snapshots/simple addLiquidity.snap @@ -1 +1 @@ -167756 \ No newline at end of file +167750 \ No newline at end of file diff --git a/.forge-snapshots/simple removeLiquidity some liquidity remains.snap b/.forge-snapshots/simple removeLiquidity some liquidity remains.snap index 4a0c6b1f7..fc72404ca 100644 --- a/.forge-snapshots/simple removeLiquidity some liquidity remains.snap +++ b/.forge-snapshots/simple removeLiquidity some liquidity remains.snap @@ -1 +1 @@ -98511 \ No newline at end of file +98529 \ No newline at end of file diff --git a/.forge-snapshots/simple removeLiquidity.snap b/.forge-snapshots/simple removeLiquidity.snap index d7d043a4d..c304eb826 100644 --- a/.forge-snapshots/simple removeLiquidity.snap +++ b/.forge-snapshots/simple removeLiquidity.snap @@ -1 +1 @@ -90877 \ No newline at end of file +90895 \ No newline at end of file diff --git a/.forge-snapshots/simple swap with native.snap b/.forge-snapshots/simple swap with native.snap index 93e8fcf28..6499256c3 100644 --- a/.forge-snapshots/simple swap with native.snap +++ b/.forge-snapshots/simple swap with native.snap @@ -1 +1 @@ -117344 \ No newline at end of file +117057 \ No newline at end of file diff --git a/.forge-snapshots/simple swap.snap b/.forge-snapshots/simple swap.snap index e3f7a21e3..e611b41ec 100644 --- a/.forge-snapshots/simple swap.snap +++ b/.forge-snapshots/simple swap.snap @@ -1 +1 @@ -132505 \ No newline at end of file +132218 \ No newline at end of file diff --git a/.forge-snapshots/swap CA custom curve + swap noop.snap b/.forge-snapshots/swap CA custom curve + swap noop.snap index 897d34ef9..eecbcb0de 100644 --- a/.forge-snapshots/swap CA custom curve + swap noop.snap +++ b/.forge-snapshots/swap CA custom curve + swap noop.snap @@ -1 +1 @@ -134660 \ No newline at end of file +134432 \ No newline at end of file diff --git a/.forge-snapshots/swap CA fee on unspecified.snap b/.forge-snapshots/swap CA fee on unspecified.snap index 9bffbcef6..4c3f02f95 100644 --- a/.forge-snapshots/swap CA fee on unspecified.snap +++ b/.forge-snapshots/swap CA fee on unspecified.snap @@ -1 +1 @@ -183643 \ No newline at end of file +183356 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity with native token.snap b/.forge-snapshots/swap against liquidity with native token.snap index cd90ee488..fcd98c992 100644 --- a/.forge-snapshots/swap against liquidity with native token.snap +++ b/.forge-snapshots/swap against liquidity with native token.snap @@ -1 +1 @@ -112984 \ No newline at end of file +112697 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity.snap b/.forge-snapshots/swap against liquidity.snap index 8a8afa76d..da8234ae7 100644 --- a/.forge-snapshots/swap against liquidity.snap +++ b/.forge-snapshots/swap against liquidity.snap @@ -1 +1 @@ -124327 \ No newline at end of file +124040 \ No newline at end of file diff --git a/.forge-snapshots/swap burn 6909 for input.snap b/.forge-snapshots/swap burn 6909 for input.snap index 0504199fb..27607b283 100644 --- a/.forge-snapshots/swap burn 6909 for input.snap +++ b/.forge-snapshots/swap burn 6909 for input.snap @@ -1 +1 @@ -136380 \ No newline at end of file +136081 \ No newline at end of file diff --git a/.forge-snapshots/swap burn native 6909 for input.snap b/.forge-snapshots/swap burn native 6909 for input.snap index 0c610d439..0372bd1c3 100644 --- a/.forge-snapshots/swap burn native 6909 for input.snap +++ b/.forge-snapshots/swap burn native 6909 for input.snap @@ -1 +1 @@ -125508 \ No newline at end of file +125221 \ No newline at end of file diff --git a/.forge-snapshots/swap mint native output as 6909.snap b/.forge-snapshots/swap mint native output as 6909.snap index 162121eb0..cccd898e8 100644 --- a/.forge-snapshots/swap mint native output as 6909.snap +++ b/.forge-snapshots/swap mint native output as 6909.snap @@ -1 +1 @@ -147569 \ No newline at end of file +147270 \ No newline at end of file diff --git a/.forge-snapshots/swap mint output as 6909.snap b/.forge-snapshots/swap mint output as 6909.snap index 51e6abe34..bb34139a6 100644 --- a/.forge-snapshots/swap mint output as 6909.snap +++ b/.forge-snapshots/swap mint output as 6909.snap @@ -1 +1 @@ -164359 \ No newline at end of file +164072 \ No newline at end of file diff --git a/.forge-snapshots/swap skips hook call if hook is caller.snap b/.forge-snapshots/swap skips hook call if hook is caller.snap index 681c76109..245a0fddd 100644 --- a/.forge-snapshots/swap skips hook call if hook is caller.snap +++ b/.forge-snapshots/swap skips hook call if hook is caller.snap @@ -1 +1 @@ -223152 \ No newline at end of file +222578 \ No newline at end of file diff --git a/.forge-snapshots/swap with dynamic fee.snap b/.forge-snapshots/swap with dynamic fee.snap index c9fb73488..cb8073a51 100644 --- a/.forge-snapshots/swap with dynamic fee.snap +++ b/.forge-snapshots/swap with dynamic fee.snap @@ -1 +1 @@ -148560 \ No newline at end of file +148273 \ No newline at end of file diff --git a/.forge-snapshots/swap with hooks.snap b/.forge-snapshots/swap with hooks.snap index 4d5b01435..0830d9291 100644 --- a/.forge-snapshots/swap with hooks.snap +++ b/.forge-snapshots/swap with hooks.snap @@ -1 +1 @@ -124339 \ No newline at end of file +124052 \ No newline at end of file diff --git a/.forge-snapshots/swap with lp fee and protocol fee.snap b/.forge-snapshots/swap with lp fee and protocol fee.snap index f799686d2..ac33fdee8 100644 --- a/.forge-snapshots/swap with lp fee and protocol fee.snap +++ b/.forge-snapshots/swap with lp fee and protocol fee.snap @@ -1 +1 @@ -180754 \ No newline at end of file +180456 \ No newline at end of file diff --git a/.forge-snapshots/swap with return dynamic fee.snap b/.forge-snapshots/swap with return dynamic fee.snap index 0527eb8ee..663192c4c 100644 --- a/.forge-snapshots/swap with return dynamic fee.snap +++ b/.forge-snapshots/swap with return dynamic fee.snap @@ -1 +1 @@ -156425 \ No newline at end of file +156132 \ No newline at end of file diff --git a/.forge-snapshots/update dynamic fee in before swap.snap b/.forge-snapshots/update dynamic fee in before swap.snap index 8800975f5..f01d0684c 100644 --- a/.forge-snapshots/update dynamic fee in before swap.snap +++ b/.forge-snapshots/update dynamic fee in before swap.snap @@ -1 +1 @@ -159033 \ No newline at end of file +158735 \ No newline at end of file diff --git a/src/libraries/Pool.sol b/src/libraries/Pool.sol index 91d571e94..0b5e3d944 100644 --- a/src/libraries/Pool.sol +++ b/src/libraries/Pool.sol @@ -10,6 +10,7 @@ import {TickMath} from "./TickMath.sol"; import {SqrtPriceMath} from "./SqrtPriceMath.sol"; import {SwapMath} from "./SwapMath.sol"; import {BalanceDelta, toBalanceDelta, BalanceDeltaLibrary} from "../types/BalanceDelta.sol"; +import {Slot0} from "../types/Slot0.sol"; import {ProtocolFeeLibrary} from "./ProtocolFeeLibrary.sol"; import {LiquidityMath} from "./LiquidityMath.sol"; import {LPFeeLibrary} from "./LPFeeLibrary.sol"; @@ -64,20 +65,6 @@ library Pool { /// @notice Thrown when trying to swap with max lp fee and specifying an output amount error InvalidFeeForExactOut(); - struct Slot0 { - // the current price - uint160 sqrtPriceX96; - // the current tick - int24 tick; - // protocol fee, expressed in hundredths of a bip - // upper 12 bits are for 1->0, and the lower 12 are for 0->1 - // the maximum is 1000 - meaning the maximum protocol fee is 0.1% - // the protocolFee is taken from the input first, then the lpFee is taken from the remaining input - uint24 protocolFee; - // used for the lp fee, either static at initialize or dynamic via hook - uint24 lpFee; - } - // info stored for each initialized individual tick struct TickInfo { // the total position liquidity that references this tick @@ -112,23 +99,24 @@ library Pool { internal returns (int24 tick) { - if (self.slot0.sqrtPriceX96 != 0) revert PoolAlreadyInitialized(); + if (self.slot0.sqrtPriceX96() != 0) revert PoolAlreadyInitialized(); tick = TickMath.getTickAtSqrtPrice(sqrtPriceX96); - self.slot0 = Slot0({sqrtPriceX96: sqrtPriceX96, tick: tick, protocolFee: protocolFee, lpFee: lpFee}); + self.slot0 = Slot0.wrap(bytes32(0)).setSqrtPriceX96(sqrtPriceX96).setTick(tick).setProtocolFee(protocolFee) + .setLpFee(lpFee); } function setProtocolFee(State storage self, uint24 protocolFee) internal { if (self.isNotInitialized()) revert PoolNotInitialized(); - self.slot0.protocolFee = protocolFee; + self.slot0 = self.slot0.setProtocolFee(protocolFee); } /// @notice Only dynamic fee pools may update the lp fee. function setLPFee(State storage self, uint24 lpFee) internal { if (self.isNotInitialized()) revert PoolNotInitialized(); - self.slot0.lpFee = lpFee; + self.slot0 = self.slot0.setLpFee(lpFee); } struct ModifyLiquidityParams { @@ -218,7 +206,8 @@ library Pool { } if (liquidityDelta != 0) { - (int24 tick, uint160 sqrtPriceX96) = (self.slot0.tick, self.slot0.sqrtPriceX96); + Slot0 _slot0 = self.slot0; + (int24 tick, uint160 sqrtPriceX96) = (_slot0.tick(), _slot0.sqrtPriceX96()); if (tick < tickLower) { // current tick is below the passed range; liquidity can only become in range by crossing from left to // right, when we'll need _more_ currency0 (it's becoming more valuable) so user must provide it @@ -297,25 +286,28 @@ library Pool { internal returns (BalanceDelta result, uint256 feeForProtocol, uint24 swapFee, SwapState memory state) { - Slot0 memory slot0Start = self.slot0; + Slot0 slot0Start = self.slot0; bool zeroForOne = params.zeroForOne; uint128 liquidityStart = self.liquidity; uint256 protocolFee = - zeroForOne ? slot0Start.protocolFee.getZeroForOneFee() : slot0Start.protocolFee.getOneForZeroFee(); + zeroForOne ? slot0Start.protocolFee().getZeroForOneFee() : slot0Start.protocolFee().getOneForZeroFee(); state.amountSpecifiedRemaining = params.amountSpecified; state.amountCalculated = 0; - state.sqrtPriceX96 = slot0Start.sqrtPriceX96; - state.tick = slot0Start.tick; + state.sqrtPriceX96 = slot0Start.sqrtPriceX96(); + state.tick = slot0Start.tick(); state.feeGrowthGlobalX128 = zeroForOne ? self.feeGrowthGlobal0X128 : self.feeGrowthGlobal1X128; state.liquidity = liquidityStart; // if the beforeSwap hook returned a valid fee override, use that as the LP fee, otherwise load from storage - slot0Start.lpFee = - params.lpFeeOverride.isOverride() ? params.lpFeeOverride.removeOverrideAndValidate() : slot0Start.lpFee; + { + uint24 lpFee = params.lpFeeOverride.isOverride() + ? params.lpFeeOverride.removeOverrideAndValidate() + : slot0Start.lpFee(); - swapFee = protocolFee == 0 ? slot0Start.lpFee : uint24(protocolFee).calculateSwapFee(slot0Start.lpFee); + swapFee = protocolFee == 0 ? lpFee : uint24(protocolFee).calculateSwapFee(lpFee); + } bool exactInput = params.amountSpecified < 0; @@ -326,15 +318,15 @@ library Pool { if (params.amountSpecified == 0) return (BalanceDeltaLibrary.ZERO_DELTA, 0, swapFee, state); if (zeroForOne) { - if (params.sqrtPriceLimitX96 >= slot0Start.sqrtPriceX96) { - revert PriceLimitAlreadyExceeded(slot0Start.sqrtPriceX96, params.sqrtPriceLimitX96); + if (params.sqrtPriceLimitX96 >= slot0Start.sqrtPriceX96()) { + revert PriceLimitAlreadyExceeded(slot0Start.sqrtPriceX96(), params.sqrtPriceLimitX96); } if (params.sqrtPriceLimitX96 <= TickMath.MIN_SQRT_PRICE) { revert PriceLimitOutOfBounds(params.sqrtPriceLimitX96); } } else { - if (params.sqrtPriceLimitX96 <= slot0Start.sqrtPriceX96) { - revert PriceLimitAlreadyExceeded(slot0Start.sqrtPriceX96, params.sqrtPriceLimitX96); + if (params.sqrtPriceLimitX96 <= slot0Start.sqrtPriceX96()) { + revert PriceLimitAlreadyExceeded(slot0Start.sqrtPriceX96(), params.sqrtPriceLimitX96); } if (params.sqrtPriceLimitX96 >= TickMath.MAX_SQRT_PRICE) { revert PriceLimitOutOfBounds(params.sqrtPriceLimitX96); @@ -439,7 +431,7 @@ library Pool { } } - (self.slot0.tick, self.slot0.sqrtPriceX96) = (state.tick, state.sqrtPriceX96); + self.slot0 = slot0Start.setTick(state.tick).setSqrtPriceX96(state.sqrtPriceX96); // update liquidity if it changed if (liquidityStart != state.liquidity) self.liquidity = state.liquidity; @@ -495,7 +487,7 @@ library Pool { { TickInfo storage lower = self.ticks[tickLower]; TickInfo storage upper = self.ticks[tickUpper]; - int24 tickCurrent = self.slot0.tick; + int24 tickCurrent = self.slot0.tick(); unchecked { if (tickCurrent < tickLower) { @@ -544,7 +536,7 @@ library Pool { if (liquidityGrossBefore == 0) { // by convention, we assume that all growth before a tick was initialized happened _below_ the tick - if (tick <= self.slot0.tick) { + if (tick <= self.slot0.tick()) { info.feeGrowthOutside0X128 = self.feeGrowthGlobal0X128; info.feeGrowthOutside1X128 = self.feeGrowthGlobal1X128; } @@ -583,7 +575,7 @@ library Pool { } function isNotInitialized(State storage self) internal view returns (bool) { - return self.slot0.sqrtPriceX96 == 0; + return self.slot0.sqrtPriceX96() == 0; } /// @notice Clears tick data diff --git a/src/types/Slot0.sol b/src/types/Slot0.sol new file mode 100644 index 000000000..9a7eee665 --- /dev/null +++ b/src/types/Slot0.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @dev Slot0 is a packed version of solidity structure. + * Using the packaged version saves gas by not storing the structure fields in memory slots. + * + * Layout: + * 24 bits empty | 24 bits lpFee | 24 bits protocolFee | 24 bits tick | 160 bits sqrtPriceX96 + * + * Fields in the direction from the least significant bit: + * + * The current price + * uint160 sqrtPriceX96; + * + * The current tick + * int24 tick; + * + * Protocol fee, expressed in hundredths of a bip, upper 12 bits are for 1->0, and the lower 12 are for 0->1 + * the maximum is 1000 - meaning the maximum protocol fee is 0.1% + * the protocolFee is taken from the input first, then the lpFee is taken from the remaining input + * uint24 protocolFee; + * + * Used for the lp fee, either static at initialize or dynamic via hook + * uint24 lpFee; + */ +type Slot0 is bytes32; + +using Slot0Library for Slot0 global; + +library Slot0Library { + uint160 internal constant MASK_160_BITS = 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + uint24 internal constant MASK_24_BITS = 0xFFFFFF; + + uint8 internal constant TICK_OFFSET = 160; + uint8 internal constant PROTOCOL_FEE_OFFSET = 184; + uint8 internal constant LP_FEE_OFFSET = 208; + + // #### GETTERS #### + function sqrtPriceX96(Slot0 _packed) internal pure returns (uint160 _sqrtPriceX96) { + /// @solidity memory-safe-assembly + assembly { + _sqrtPriceX96 := and(MASK_160_BITS, _packed) + } + } + + function tick(Slot0 _packed) internal pure returns (int24 _tick) { + /// @solidity memory-safe-assembly + assembly { + _tick := signextend(2, shr(TICK_OFFSET, _packed)) + } + } + + function protocolFee(Slot0 _packed) internal pure returns (uint24 _protocolFee) { + /// @solidity memory-safe-assembly + assembly { + _protocolFee := and(MASK_24_BITS, shr(PROTOCOL_FEE_OFFSET, _packed)) + } + } + + function lpFee(Slot0 _packed) internal pure returns (uint24 _lpFee) { + /// @solidity memory-safe-assembly + assembly { + _lpFee := and(MASK_24_BITS, shr(LP_FEE_OFFSET, _packed)) + } + } + + // #### SETTERS #### + function setSqrtPriceX96(Slot0 _packed, uint160 _sqrtPriceX96) internal pure returns (Slot0 _result) { + /// @solidity memory-safe-assembly + assembly { + _result := or(and(not(MASK_160_BITS), _packed), and(MASK_160_BITS, _sqrtPriceX96)) + } + } + + function setTick(Slot0 _packed, int24 _tick) internal pure returns (Slot0 _result) { + /// @solidity memory-safe-assembly + assembly { + _result := or(and(not(shl(TICK_OFFSET, MASK_24_BITS)), _packed), shl(TICK_OFFSET, and(MASK_24_BITS, _tick))) + } + } + + function setProtocolFee(Slot0 _packed, uint24 _protocolFee) internal pure returns (Slot0 _result) { + /// @solidity memory-safe-assembly + assembly { + _result := + or( + and(not(shl(PROTOCOL_FEE_OFFSET, MASK_24_BITS)), _packed), + shl(PROTOCOL_FEE_OFFSET, and(MASK_24_BITS, _protocolFee)) + ) + } + } + + function setLpFee(Slot0 _packed, uint24 _lpFee) internal pure returns (Slot0 _result) { + /// @solidity memory-safe-assembly + assembly { + _result := + or(and(not(shl(LP_FEE_OFFSET, MASK_24_BITS)), _packed), shl(LP_FEE_OFFSET, and(MASK_24_BITS, _lpFee))) + } + } +} diff --git a/test/Tick.t.sol b/test/Tick.t.sol index 205f8cb84..451781cba 100644 --- a/test/Tick.t.sol +++ b/test/Tick.t.sol @@ -62,7 +62,7 @@ contract TickTest is Test, GasSnapshot { uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128 ) internal returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) { - pool.slot0.tick = tickCurrent; + pool.slot0 = pool.slot0.setTick(tickCurrent); pool.feeGrowthGlobal0X128 = feeGrowthGlobal0X128; pool.feeGrowthGlobal1X128 = feeGrowthGlobal1X128; return pool.getFeeGrowthInside(tickLower, tickUpper); @@ -76,7 +76,7 @@ contract TickTest is Test, GasSnapshot { uint256 feeGrowthGlobal1X128, bool upper ) internal returns (bool flipped, uint128 liquidityGrossAfter) { - pool.slot0.tick = tickCurrent; + pool.slot0 = pool.slot0.setTick(tickCurrent); pool.feeGrowthGlobal0X128 = feeGrowthGlobal0X128; pool.feeGrowthGlobal1X128 = feeGrowthGlobal1X128; return pool.updateTick(tick, liquidityDelta, upper); diff --git a/test/libraries/Pool.t.sol b/test/libraries/Pool.t.sol index 3375e44fd..45f539a09 100644 --- a/test/libraries/Pool.t.sol +++ b/test/libraries/Pool.t.sol @@ -11,6 +11,7 @@ import {TickBitmap} from "src/libraries/TickBitmap.sol"; import {LiquidityAmounts} from "test/utils/LiquidityAmounts.sol"; import {Constants} from "test/utils/Constants.sol"; import {BalanceDelta} from "src/types/BalanceDelta.sol"; +import {Slot0} from "src/types/Slot0.sol"; import {SafeCast} from "src/libraries/SafeCast.sol"; import {ProtocolFeeLibrary} from "src/libraries/ProtocolFeeLibrary.sol"; import {LPFeeLibrary} from "src/libraries/LPFeeLibrary.sol"; @@ -31,11 +32,11 @@ contract PoolTest is Test { state.initialize(sqrtPriceX96, protocolFee, swapFee); } else { state.initialize(sqrtPriceX96, protocolFee, swapFee); - assertEq(state.slot0.sqrtPriceX96, sqrtPriceX96); - assertEq(state.slot0.protocolFee, protocolFee); - assertEq(state.slot0.tick, TickMath.getTickAtSqrtPrice(sqrtPriceX96)); - assertLt(state.slot0.tick, TickMath.MAX_TICK); - assertGt(state.slot0.tick, TickMath.MIN_TICK - 1); + assertEq(state.slot0.sqrtPriceX96(), sqrtPriceX96); + assertEq(state.slot0.protocolFee(), protocolFee); + assertEq(state.slot0.tick(), TickMath.getTickAtSqrtPrice(sqrtPriceX96)); + assertLt(state.slot0.tick(), TickMath.MAX_TICK); + assertGt(state.slot0.tick(), TickMath.MIN_TICK - 1); } } @@ -117,7 +118,7 @@ contract PoolTest is Test { salt: 0 }) ); - Pool.Slot0 memory slot0 = state.slot0; + Slot0 slot0 = state.slot0; uint24 _lpFee = params.lpFeeOverride.isOverride() ? params.lpFeeOverride.removeOverrideFlag() : lpFee; uint24 swapFee = protocolFee == 0 ? _lpFee : uint24(protocolFee).calculateSwapFee(_lpFee); @@ -129,10 +130,10 @@ contract PoolTest is Test { vm.expectRevert(LPFeeLibrary.FeeTooLarge.selector); state.swap(params); } else if (params.zeroForOne && params.amountSpecified != 0) { - if (params.sqrtPriceLimitX96 >= slot0.sqrtPriceX96) { + if (params.sqrtPriceLimitX96 >= slot0.sqrtPriceX96()) { vm.expectRevert( abi.encodeWithSelector( - Pool.PriceLimitAlreadyExceeded.selector, slot0.sqrtPriceX96, params.sqrtPriceLimitX96 + Pool.PriceLimitAlreadyExceeded.selector, slot0.sqrtPriceX96(), params.sqrtPriceLimitX96 ) ); state.swap(params); @@ -141,10 +142,10 @@ contract PoolTest is Test { state.swap(params); } } else if (!params.zeroForOne && params.amountSpecified != 0) { - if (params.sqrtPriceLimitX96 <= slot0.sqrtPriceX96) { + if (params.sqrtPriceLimitX96 <= slot0.sqrtPriceX96()) { vm.expectRevert( abi.encodeWithSelector( - Pool.PriceLimitAlreadyExceeded.selector, slot0.sqrtPriceX96, params.sqrtPriceLimitX96 + Pool.PriceLimitAlreadyExceeded.selector, slot0.sqrtPriceX96(), params.sqrtPriceLimitX96 ) ); state.swap(params); @@ -153,15 +154,15 @@ contract PoolTest is Test { state.swap(params); } } else { - uint160 sqrtPriceBefore = state.slot0.sqrtPriceX96; + uint160 sqrtPriceBefore = state.slot0.sqrtPriceX96(); state.swap(params); if (params.amountSpecified == 0) { - assertEq(sqrtPriceBefore, state.slot0.sqrtPriceX96, "amountSpecified == 0"); + assertEq(sqrtPriceBefore, state.slot0.sqrtPriceX96(), "amountSpecified == 0"); } else if (params.zeroForOne) { - assertGe(state.slot0.sqrtPriceX96, params.sqrtPriceLimitX96, "zeroForOne"); + assertGe(state.slot0.sqrtPriceX96(), params.sqrtPriceLimitX96, "zeroForOne"); } else { - assertLe(state.slot0.sqrtPriceX96, params.sqrtPriceLimitX96, "oneForZero"); + assertLe(state.slot0.sqrtPriceX96(), params.sqrtPriceLimitX96, "oneForZero"); } } } diff --git a/test/types/Slot0.t.sol b/test/types/Slot0.t.sol new file mode 100644 index 000000000..29c353a50 --- /dev/null +++ b/test/types/Slot0.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Slot0, Slot0Library} from "../../src/types/Slot0.sol"; + +contract TestSlot0 is Test { + function test_slot0_constants_masks() public pure { + assertEq(Slot0Library.MASK_160_BITS, type(uint160).max); + assertEq(Slot0Library.MASK_24_BITS, type(uint24).max); + } + + function test_fuzz_slot0_pack_unpack(uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) + public + pure + { + // pack starting from "lowest" field + Slot0 _slot0 = Slot0.wrap(bytes32(0)).setSqrtPriceX96(sqrtPriceX96).setTick(tick).setProtocolFee(protocolFee) + .setLpFee(lpFee); + + assertEq(_slot0.sqrtPriceX96(), sqrtPriceX96); + assertEq(_slot0.tick(), tick); + assertEq(_slot0.protocolFee(), protocolFee); + assertEq(_slot0.lpFee(), lpFee); + + // pack starting from "highest" field + _slot0 = Slot0.wrap(bytes32(0)).setLpFee(lpFee).setProtocolFee(protocolFee).setTick(tick).setSqrtPriceX96( + sqrtPriceX96 + ); + + assertEq(_slot0.sqrtPriceX96(), sqrtPriceX96); + assertEq(_slot0.tick(), tick); + assertEq(_slot0.protocolFee(), protocolFee); + assertEq(_slot0.lpFee(), lpFee); + } +}