From e4271089c5188009f320f527b404e7679e6e89cd Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Tue, 14 May 2024 13:53:23 -0500 Subject: [PATCH 1/3] Optimize `SafeCast` (#576) * Add `toUint128` to `SafeCast` and fuzz test In the `SafeCast` library, a new function `toUint128` has been added to support casting uint256 to uint128. Accompanying the update, a new fuzz test for `toUint128` is also added to verify the behavior when `toUint128` encounters a potential overflow condition. The test ensures its correct functionality and robustness against edge cases. * Optimize `SafeCast` with inline assembly Refactor `SafeCast.sol` library using inline assembly for better memory-safe operations. By using inline assembly, the code optimizes casting operations which result in reduced gas usage. These changes affect numerous `.forge-snapshot` files, demonstrating an overall reduction in gas usage across multiple contracts. * Refactor `SafeCast` for better readability The SafeCast library code has been refactored to increase readability. Specifically, repetition of in-line assembly code has been eliminated in favour of a more abstracted '_revertOverflow()' function. This not only simplifies multiple number casting functions but also helps reducing the bytecode size of the contract subtly. * Added `test_toUint128` function to SafeCast tests The new function tests the casting method `toUint128` in the `SafeCast` library. It checks that the casting correctly handles 0, maximum uint128 values, and overflows. * Refactor SafeCast.sol for enhanced code readability The modification simplifies the SafeCast.sol functions to return types directly rather than using a separate variable. Changes have been made to parameter naming for better understanding. Additionally, an overflow check has been added to the toInt128 function for greater accuracy in casting data types. * Refactor and optimize `toInt256` --- .../SwapMath_oneForZero_exactInPartial.snap | 2 +- .../SwapMath_oneForZero_exactOutPartial.snap | 2 +- ...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 +- .forge-snapshots/addLiquidity.snap | 2 +- ...new liquidity to a position with salt.snap | 2 +- .forge-snapshots/donate gas with 1 token.snap | 2 +- .../donate gas with 2 tokens.snap | 2 +- ...iceFromInput_zeroForOneEqualsFalseGas.snap | 2 +- ...ceFromOutput_zeroForOneEqualsFalseGas.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 +- .forge-snapshots/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 +- .../update dynamic fee in before swap.snap | 2 +- src/libraries/SafeCast.sol | 60 ++++++++++++------- test/libraries/SafeCast.t.sol | 16 +++++ 34 files changed, 88 insertions(+), 52 deletions(-) diff --git a/.forge-snapshots/SwapMath_oneForZero_exactInPartial.snap b/.forge-snapshots/SwapMath_oneForZero_exactInPartial.snap index 38ef83141..43e6c3379 100644 --- a/.forge-snapshots/SwapMath_oneForZero_exactInPartial.snap +++ b/.forge-snapshots/SwapMath_oneForZero_exactInPartial.snap @@ -1 +1 @@ -3038 \ No newline at end of file +3016 \ No newline at end of file diff --git a/.forge-snapshots/SwapMath_oneForZero_exactOutPartial.snap b/.forge-snapshots/SwapMath_oneForZero_exactOutPartial.snap index e4da9ac19..72cd61fe0 100644 --- a/.forge-snapshots/SwapMath_oneForZero_exactOutPartial.snap +++ b/.forge-snapshots/SwapMath_oneForZero_exactOutPartial.snap @@ -1 +1 @@ -3278 \ No newline at end of file +3256 \ No newline at end of file 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 f2eceebf0..17f6e7771 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 @@ -151174 \ No newline at end of file +151104 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity CA fee.snap b/.forge-snapshots/addLiquidity CA fee.snap index 66f302cfc..6a9f64f2f 100644 --- a/.forge-snapshots/addLiquidity CA fee.snap +++ b/.forge-snapshots/addLiquidity CA fee.snap @@ -1 +1 @@ -329540 \ No newline at end of file +329444 \ 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 a880d226c..8ce2b9562 100644 --- a/.forge-snapshots/addLiquidity with empty hook.snap +++ b/.forge-snapshots/addLiquidity with empty hook.snap @@ -1 +1 @@ -284175 \ No newline at end of file +284085 \ 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 e2a7016e7..b78768771 100644 --- a/.forge-snapshots/addLiquidity with native token.snap +++ b/.forge-snapshots/addLiquidity with native token.snap @@ -1 +1 @@ -141307 \ No newline at end of file +141237 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity.snap b/.forge-snapshots/addLiquidity.snap index 0957ca694..b40672a13 100644 --- a/.forge-snapshots/addLiquidity.snap +++ b/.forge-snapshots/addLiquidity.snap @@ -1 +1 @@ -151150 \ No newline at end of file +151080 \ 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 d058bcee5..ab53d56de 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 @@ -299702 \ No newline at end of file +299632 \ No newline at end of file diff --git a/.forge-snapshots/donate gas with 1 token.snap b/.forge-snapshots/donate gas with 1 token.snap index 5c41fcb36..efe7a735d 100644 --- a/.forge-snapshots/donate gas with 1 token.snap +++ b/.forge-snapshots/donate gas with 1 token.snap @@ -1 +1 @@ -108696 \ No newline at end of file +108687 \ No newline at end of file diff --git a/.forge-snapshots/donate gas with 2 tokens.snap b/.forge-snapshots/donate gas with 2 tokens.snap index 889ec57a3..6a6f4c603 100644 --- a/.forge-snapshots/donate gas with 2 tokens.snap +++ b/.forge-snapshots/donate gas with 2 tokens.snap @@ -1 +1 @@ -149234 \ No newline at end of file +149222 \ No newline at end of file diff --git a/.forge-snapshots/getNextSqrtPriceFromInput_zeroForOneEqualsFalseGas.snap b/.forge-snapshots/getNextSqrtPriceFromInput_zeroForOneEqualsFalseGas.snap index 7dfce3516..23c5f49dc 100644 --- a/.forge-snapshots/getNextSqrtPriceFromInput_zeroForOneEqualsFalseGas.snap +++ b/.forge-snapshots/getNextSqrtPriceFromInput_zeroForOneEqualsFalseGas.snap @@ -1 +1 @@ -594 \ No newline at end of file +572 \ No newline at end of file diff --git a/.forge-snapshots/getNextSqrtPriceFromOutput_zeroForOneEqualsFalseGas.snap b/.forge-snapshots/getNextSqrtPriceFromOutput_zeroForOneEqualsFalseGas.snap index f8f450742..c5befbc75 100644 --- a/.forge-snapshots/getNextSqrtPriceFromOutput_zeroForOneEqualsFalseGas.snap +++ b/.forge-snapshots/getNextSqrtPriceFromOutput_zeroForOneEqualsFalseGas.snap @@ -1 +1 @@ -878 \ No newline at end of file +856 \ No newline at end of file diff --git a/.forge-snapshots/poolManager bytecode size.snap b/.forge-snapshots/poolManager bytecode size.snap index 927367108..367a81773 100644 --- a/.forge-snapshots/poolManager bytecode size.snap +++ b/.forge-snapshots/poolManager bytecode size.snap @@ -1 +1 @@ -23817 \ No newline at end of file +23738 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity CA fee.snap b/.forge-snapshots/removeLiquidity CA fee.snap index ccf2ce447..dcc25a494 100644 --- a/.forge-snapshots/removeLiquidity CA fee.snap +++ b/.forge-snapshots/removeLiquidity CA fee.snap @@ -1 +1 @@ -185027 \ No newline at end of file +184931 \ 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 32f98efe0..372159cc7 100644 --- a/.forge-snapshots/removeLiquidity with empty hook.snap +++ b/.forge-snapshots/removeLiquidity with empty hook.snap @@ -1 +1 @@ -121045 \ No newline at end of file +120975 \ 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 93c0f3d2d..0b5f4b02c 100644 --- a/.forge-snapshots/removeLiquidity with native token.snap +++ b/.forge-snapshots/removeLiquidity with native token.snap @@ -1 +1 @@ -117820 \ No newline at end of file +117750 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity.snap b/.forge-snapshots/removeLiquidity.snap index 837de0999..53dfe042c 100644 --- a/.forge-snapshots/removeLiquidity.snap +++ b/.forge-snapshots/removeLiquidity.snap @@ -1 +1 @@ -121033 \ No newline at end of file +120963 \ 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 5267f17bf..41735a4dd 100644 --- a/.forge-snapshots/simple swap with native.snap +++ b/.forge-snapshots/simple swap with native.snap @@ -1 +1 @@ -117381 \ No newline at end of file +117339 \ No newline at end of file diff --git a/.forge-snapshots/simple swap.snap b/.forge-snapshots/simple swap.snap index bc35833dc..4719fd039 100644 --- a/.forge-snapshots/simple swap.snap +++ b/.forge-snapshots/simple swap.snap @@ -1 +1 @@ -132620 \ No newline at end of file +132578 \ 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 33ed96a1a..612b68dd0 100644 --- a/.forge-snapshots/swap CA custom curve + swap noop.snap +++ b/.forge-snapshots/swap CA custom curve + swap noop.snap @@ -1 +1 @@ -135892 \ No newline at end of file +135860 \ 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 473c75dcb..08f6f6124 100644 --- a/.forge-snapshots/swap CA fee on unspecified.snap +++ b/.forge-snapshots/swap CA fee on unspecified.snap @@ -1 +1 @@ -184887 \ No newline at end of file +184809 \ 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 df619ebc1..8b56c7d74 100644 --- a/.forge-snapshots/swap against liquidity with native token.snap +++ b/.forge-snapshots/swap against liquidity with native token.snap @@ -1 +1 @@ -113834 \ No newline at end of file +113800 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity.snap b/.forge-snapshots/swap against liquidity.snap index 42ec59cb4..0372bd1c3 100644 --- a/.forge-snapshots/swap against liquidity.snap +++ b/.forge-snapshots/swap against liquidity.snap @@ -1 +1 @@ -125255 \ No newline at end of file +125221 \ 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 0663fd6c5..9ff1f5c77 100644 --- a/.forge-snapshots/swap burn 6909 for input.snap +++ b/.forge-snapshots/swap burn 6909 for input.snap @@ -1 +1 @@ -137257 \ No newline at end of file +137207 \ 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 75d6c8585..4228ebd6b 100644 --- a/.forge-snapshots/swap burn native 6909 for input.snap +++ b/.forge-snapshots/swap burn native 6909 for input.snap @@ -1 +1 @@ -126357 \ No newline at end of file +126323 \ 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 2f0f3d4b4..3de9f0d73 100644 --- a/.forge-snapshots/swap mint native output as 6909.snap +++ b/.forge-snapshots/swap mint native output as 6909.snap @@ -1 +1 @@ -148435 \ No newline at end of file +148385 \ 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 d765b4c6a..f6f049473 100644 --- a/.forge-snapshots/swap mint output as 6909.snap +++ b/.forge-snapshots/swap mint output as 6909.snap @@ -1 +1 @@ -165244 \ No newline at end of file +165202 \ 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 11b96cf97..95777a5a8 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 @@ -224777 \ No newline at end of file +224701 \ 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 1f6dfff04..72f4ce3f4 100644 --- a/.forge-snapshots/swap with dynamic fee.snap +++ b/.forge-snapshots/swap with dynamic fee.snap @@ -1 +1 @@ -149511 \ No newline at end of file +149469 \ No newline at end of file diff --git a/.forge-snapshots/swap with hooks.snap b/.forge-snapshots/swap with hooks.snap index 8eab8b1e5..9f4a834a8 100644 --- a/.forge-snapshots/swap with hooks.snap +++ b/.forge-snapshots/swap with hooks.snap @@ -1 +1 @@ -125267 \ No newline at end of file +125233 \ 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 fa7dfe554..7d3d777bd 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 @@ -181833 \ No newline at end of file +181791 \ 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 a2b21541f..cf4bf7d64 100644 --- a/.forge-snapshots/update dynamic fee in before swap.snap +++ b/.forge-snapshots/update dynamic fee in before swap.snap @@ -1 +1 @@ -160064 \ No newline at end of file +160022 \ No newline at end of file diff --git a/src/libraries/SafeCast.sol b/src/libraries/SafeCast.sol index 5eb5bd474..8f22bed45 100644 --- a/src/libraries/SafeCast.sol +++ b/src/libraries/SafeCast.sol @@ -6,35 +6,55 @@ pragma solidity ^0.8.20; library SafeCast { error SafeCastOverflow(); + function _revertOverflow() private pure { + /// @solidity memory-safe-assembly + assembly { + // Store the function selector of `SafeCastOverflow()`. + mstore(0x00, 0x93dafdf1) + // Revert with (offset, size). + revert(0x1c, 0x04) + } + } + /// @notice Cast a uint256 to a uint160, revert on overflow - /// @param y The uint256 to be downcasted - /// @return z The downcasted integer, now type uint160 - function toUint160(uint256 y) internal pure returns (uint160 z) { - z = uint160(y); - if (z != y) revert SafeCastOverflow(); + /// @param x The uint256 to be downcasted + /// @return The downcasted integer, now type uint160 + function toUint160(uint256 x) internal pure returns (uint160) { + if (x >= 1 << 160) _revertOverflow(); + return uint160(x); + } + + /// @notice Cast a uint256 to a uint128, revert on overflow + /// @param x The uint256 to be downcasted + /// @return The downcasted integer, now type uint128 + function toUint128(uint256 x) internal pure returns (uint128) { + if (x >= 1 << 128) _revertOverflow(); + return uint128(x); } /// @notice Cast a int256 to a int128, revert on overflow or underflow - /// @param y The int256 to be downcasted - /// @return z The downcasted integer, now type int128 - function toInt128(int256 y) internal pure returns (int128 z) { - z = int128(y); - if (z != y) revert SafeCastOverflow(); + /// @param x The int256 to be downcasted + /// @return The downcasted integer, now type int128 + function toInt128(int256 x) internal pure returns (int128) { + unchecked { + if (((1 << 127) + uint256(x)) >> 128 == uint256(0)) return int128(x); + _revertOverflow(); + } } /// @notice Cast a uint256 to a int256, revert on overflow - /// @param y The uint256 to be casted - /// @return z The casted integer, now type int256 - function toInt256(uint256 y) internal pure returns (int256 z) { - if (y > uint256(type(int256).max)) revert SafeCastOverflow(); - z = int256(y); + /// @param x The uint256 to be casted + /// @return The casted integer, now type int256 + function toInt256(uint256 x) internal pure returns (int256) { + if (int256(x) >= 0) return int256(x); + _revertOverflow(); } /// @notice Cast a uint256 to a int128, revert on overflow - /// @param y The uint256 to be downcasted - /// @return z The downcasted integer, now type int128 - function toInt128(uint256 y) internal pure returns (int128 z) { - if (y > uint128(type(int128).max)) revert SafeCastOverflow(); - z = int128(int256(y)); + /// @param x The uint256 to be downcasted + /// @return The downcasted integer, now type int128 + function toInt128(uint256 x) internal pure returns (int128) { + if (x >= 1 << 127) _revertOverflow(); + return int128(int256(x)); } } diff --git a/test/libraries/SafeCast.t.sol b/test/libraries/SafeCast.t.sol index bc18e2970..40d873f03 100644 --- a/test/libraries/SafeCast.t.sol +++ b/test/libraries/SafeCast.t.sol @@ -22,6 +22,22 @@ contract SafeCastTest is Test { SafeCast.toUint160(type(uint160).max + uint256(1)); } + function test_fuzz_toUint128(uint256 x) public { + if (x <= type(uint128).max) { + assertEq(uint256(SafeCast.toUint128(x)), x); + } else { + vm.expectRevert(SafeCast.SafeCastOverflow.selector); + SafeCast.toUint128(x); + } + } + + function test_toUint128() public { + assertEq(uint256(SafeCast.toUint128(0)), 0); + assertEq(uint256(SafeCast.toUint128(type(uint128).max)), type(uint128).max); + vm.expectRevert(SafeCast.SafeCastOverflow.selector); + SafeCast.toUint128(type(uint128).max + uint256(1)); + } + function test_fuzz_toInt128_fromInt256(int256 x) public { if (x <= type(int128).max && x >= type(int128).min) { assertEq(int256(SafeCast.toInt128(x)), x); From 81a69c6ffe83e4f27fd4d4c686a129292f5d28ab Mon Sep 17 00:00:00 2001 From: Daniel Gretzke Date: Wed, 15 May 2024 15:44:02 +0200 Subject: [PATCH 2/3] Accurate liquidity metering (#655) * add router for more accurate gas metering for adding and removing liquidity * get amounts from delta directly * Remove second manager * remove initPool from no checks router * move liquidity to a unique range for simple tests --------- Co-authored-by: Alice Henshaw --- .../addLiquidity with native token.snap | 2 +- .forge-snapshots/addLiquidity.snap | 1 - .../erc20 collect protocol fees.snap | 2 +- .../removeLiquidity with native token.snap | 2 +- .forge-snapshots/removeLiquidity.snap | 1 - ...dLiquidity second addition same range.snap | 1 + .forge-snapshots/simple addLiquidity.snap | 1 + ...emoveLiquidity some liquidity remains.snap | 1 + .forge-snapshots/simple removeLiquidity.snap | 1 + .forge-snapshots/simple swap with native.snap | 2 +- ...p against liquidity with native token.snap | 2 +- .../swap burn native 6909 for input.snap | 2 +- .../swap mint native output as 6909.snap | 2 +- src/test/PoolModifyLiquidityTestNoChecks.sol | 77 +++++++++++++++++++ test/PoolManager.t.sol | 41 +++++++++- test/utils/Deployers.sol | 6 +- 16 files changed, 131 insertions(+), 13 deletions(-) delete mode 100644 .forge-snapshots/addLiquidity.snap delete mode 100644 .forge-snapshots/removeLiquidity.snap create mode 100644 .forge-snapshots/simple addLiquidity second addition same range.snap create mode 100644 .forge-snapshots/simple addLiquidity.snap create mode 100644 .forge-snapshots/simple removeLiquidity some liquidity remains.snap create mode 100644 .forge-snapshots/simple removeLiquidity.snap create mode 100644 src/test/PoolModifyLiquidityTestNoChecks.sol diff --git a/.forge-snapshots/addLiquidity with native token.snap b/.forge-snapshots/addLiquidity with native token.snap index b78768771..0e0056940 100644 --- a/.forge-snapshots/addLiquidity with native token.snap +++ b/.forge-snapshots/addLiquidity with native token.snap @@ -1 +1 @@ -141237 \ No newline at end of file +141249 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity.snap b/.forge-snapshots/addLiquidity.snap deleted file mode 100644 index b40672a13..000000000 --- a/.forge-snapshots/addLiquidity.snap +++ /dev/null @@ -1 +0,0 @@ -151080 \ No newline at end of file diff --git a/.forge-snapshots/erc20 collect protocol fees.snap b/.forge-snapshots/erc20 collect protocol fees.snap index 975b23465..b54081a54 100644 --- a/.forge-snapshots/erc20 collect protocol fees.snap +++ b/.forge-snapshots/erc20 collect protocol fees.snap @@ -1 +1 @@ -57354 \ No newline at end of file +57342 \ 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 0b5f4b02c..5f8090dd2 100644 --- a/.forge-snapshots/removeLiquidity with native token.snap +++ b/.forge-snapshots/removeLiquidity with native token.snap @@ -1 +1 @@ -117750 \ No newline at end of file +117762 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity.snap b/.forge-snapshots/removeLiquidity.snap deleted file mode 100644 index 53dfe042c..000000000 --- a/.forge-snapshots/removeLiquidity.snap +++ /dev/null @@ -1 +0,0 @@ -120963 \ 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 new file mode 100644 index 000000000..2ae596964 --- /dev/null +++ b/.forge-snapshots/simple addLiquidity second addition same range.snap @@ -0,0 +1 @@ +104916 \ No newline at end of file diff --git a/.forge-snapshots/simple addLiquidity.snap b/.forge-snapshots/simple addLiquidity.snap new file mode 100644 index 000000000..cc24ac4fc --- /dev/null +++ b/.forge-snapshots/simple addLiquidity.snap @@ -0,0 +1 @@ +167758 \ 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 new file mode 100644 index 000000000..3eecd3120 --- /dev/null +++ b/.forge-snapshots/simple removeLiquidity some liquidity remains.snap @@ -0,0 +1 @@ +98535 \ No newline at end of file diff --git a/.forge-snapshots/simple removeLiquidity.snap b/.forge-snapshots/simple removeLiquidity.snap new file mode 100644 index 000000000..5c5b7f37c --- /dev/null +++ b/.forge-snapshots/simple removeLiquidity.snap @@ -0,0 +1 @@ +90901 \ 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 41735a4dd..e66a3badf 100644 --- a/.forge-snapshots/simple swap with native.snap +++ b/.forge-snapshots/simple swap with native.snap @@ -1 +1 @@ -117339 \ No newline at end of file +117351 \ 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 8b56c7d74..3affc012e 100644 --- a/.forge-snapshots/swap against liquidity with native token.snap +++ b/.forge-snapshots/swap against liquidity with native token.snap @@ -1 +1 @@ -113800 \ No newline at end of file +113812 \ 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 4228ebd6b..d54d7456c 100644 --- a/.forge-snapshots/swap burn native 6909 for input.snap +++ b/.forge-snapshots/swap burn native 6909 for input.snap @@ -1 +1 @@ -126323 \ No newline at end of file +126335 \ 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 3de9f0d73..cf2a52496 100644 --- a/.forge-snapshots/swap mint native output as 6909.snap +++ b/.forge-snapshots/swap mint native output as 6909.snap @@ -1 +1 @@ -148385 \ No newline at end of file +148397 \ No newline at end of file diff --git a/src/test/PoolModifyLiquidityTestNoChecks.sol b/src/test/PoolModifyLiquidityTestNoChecks.sol new file mode 100644 index 000000000..ebd700258 --- /dev/null +++ b/src/test/PoolModifyLiquidityTestNoChecks.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {CurrencyLibrary, Currency} from "../types/Currency.sol"; +import {IPoolManager} from "../interfaces/IPoolManager.sol"; +import {BalanceDelta} from "../types/BalanceDelta.sol"; +import {PoolKey} from "../types/PoolKey.sol"; +import {PoolIdLibrary} from "../types/PoolId.sol"; +import {PoolTestBase} from "./PoolTestBase.sol"; +import {IHooks} from "../interfaces/IHooks.sol"; +import {Hooks} from "../libraries/Hooks.sol"; +import {LPFeeLibrary} from "../libraries/LPFeeLibrary.sol"; +import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; +import {Constants} from "../../test/utils/Constants.sol"; + +contract PoolModifyLiquidityTestNoChecks is PoolTestBase { + using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; + using Hooks for IHooks; + using LPFeeLibrary for uint24; + using PoolIdLibrary for PoolKey; + + constructor(IPoolManager _manager) PoolTestBase(_manager) {} + + struct CallbackData { + address sender; + PoolKey key; + IPoolManager.ModifyLiquidityParams params; + bytes hookData; + bool settleUsingBurn; + bool takeClaims; + } + + function modifyLiquidity( + PoolKey memory key, + IPoolManager.ModifyLiquidityParams memory params, + bytes memory hookData + ) external payable returns (BalanceDelta delta) { + delta = modifyLiquidity(key, params, hookData, false, false); + } + + function modifyLiquidity( + PoolKey memory key, + IPoolManager.ModifyLiquidityParams memory params, + bytes memory hookData, + bool settleUsingBurn, + bool takeClaims + ) public payable returns (BalanceDelta delta) { + delta = abi.decode( + manager.unlock(abi.encode(CallbackData(msg.sender, key, params, hookData, settleUsingBurn, takeClaims))), + (BalanceDelta) + ); + + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) { + CurrencyLibrary.NATIVE.transfer(msg.sender, ethBalance); + } + } + + function unlockCallback(bytes calldata rawData) external returns (bytes memory) { + require(msg.sender == address(manager)); + + CallbackData memory data = abi.decode(rawData, (CallbackData)); + + (BalanceDelta delta,) = manager.modifyLiquidity(data.key, data.params, data.hookData); + + int256 delta0 = delta.amount0(); + int256 delta1 = delta.amount1(); + + if (delta0 < 0) data.key.currency0.settle(manager, data.sender, uint256(-delta0), data.settleUsingBurn); + if (delta1 < 0) data.key.currency1.settle(manager, data.sender, uint256(-delta1), data.settleUsingBurn); + if (delta0 > 0) data.key.currency0.take(manager, data.sender, uint256(delta0), data.takeClaims); + if (delta1 > 0) data.key.currency1.take(manager, data.sender, uint256(delta1), data.takeClaims); + + return abi.encode(delta); + } +} diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index 3d063462c..188389c66 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -451,13 +451,48 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { } function test_addLiquidity_gas() public { - modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES); - snapLastCall("addLiquidity"); + IPoolManager.ModifyLiquidityParams memory uniqueParams = + IPoolManager.ModifyLiquidityParams({tickLower: -300, tickUpper: -180, liquidityDelta: 1e18, salt: 0}); + modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES); + snapLastCall("simple addLiquidity"); + } + + function test_addLiquidity_secondAdditionSameRange_gas() public { + IPoolManager.ModifyLiquidityParams memory uniqueParams = + IPoolManager.ModifyLiquidityParams({tickLower: -300, tickUpper: -180, liquidityDelta: 1e18, salt: 0}); + modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES); + modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES); + snapLastCall("simple addLiquidity second addition same range"); } function test_removeLiquidity_gas() public { + IPoolManager.ModifyLiquidityParams memory uniqueParams = + IPoolManager.ModifyLiquidityParams({tickLower: -300, tickUpper: -180, liquidityDelta: 1e18, salt: 0}); + // add some liquidity to remove + modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES); + + uniqueParams.liquidityDelta *= -1; + modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES); + snapLastCall("simple removeLiquidity"); + } + + function test_removeLiquidity_someLiquidityRemains_gas() public { + // add double the liquidity to remove + IPoolManager.ModifyLiquidityParams memory uniqueParams = + IPoolManager.ModifyLiquidityParams({tickLower: -300, tickUpper: -180, liquidityDelta: 1e18, salt: 0}); + modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES); + + uniqueParams.liquidityDelta /= -2; + modifyLiquidityNoChecks.modifyLiquidity(key, uniqueParams, ZERO_BYTES); + snapLastCall("simple removeLiquidity some liquidity remains"); + } + + function test_addLiquidity_succeeds() public { + modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES); + } + + function test_removeLiquidity_succeeds() public { modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); - snapLastCall("removeLiquidity"); } function test_addLiquidity_withNative_gas() public { diff --git a/test/utils/Deployers.sol b/test/utils/Deployers.sol index 9a0df5ed4..7a941ab57 100644 --- a/test/utils/Deployers.sol +++ b/test/utils/Deployers.sol @@ -15,6 +15,7 @@ import {TickMath} from "../../src/libraries/TickMath.sol"; import {Constants} from "../utils/Constants.sol"; import {SortTokens} from "./SortTokens.sol"; import {PoolModifyLiquidityTest} from "../../src/test/PoolModifyLiquidityTest.sol"; +import {PoolModifyLiquidityTestNoChecks} from "../../src/test/PoolModifyLiquidityTestNoChecks.sol"; import {PoolSwapTest} from "../../src/test/PoolSwapTest.sol"; import {SwapRouterNoChecks} from "../../src/test/SwapRouterNoChecks.sol"; import {PoolDonateTest} from "../../src/test/PoolDonateTest.sol"; @@ -56,6 +57,7 @@ contract Deployers { Currency internal currency1; IPoolManager manager; PoolModifyLiquidityTest modifyLiquidityRouter; + PoolModifyLiquidityTestNoChecks modifyLiquidityNoChecks; SwapRouterNoChecks swapRouterNoChecks; PoolSwapTest swapRouter; PoolDonateTest donateRouter; @@ -97,6 +99,7 @@ contract Deployers { swapRouter = new PoolSwapTest(manager); swapRouterNoChecks = new SwapRouterNoChecks(manager); modifyLiquidityRouter = new PoolModifyLiquidityTest(manager); + modifyLiquidityNoChecks = new PoolModifyLiquidityTestNoChecks(manager); donateRouter = new PoolDonateTest(manager); takeRouter = new PoolTakeTest(manager); settleRouter = new PoolSettleTest(manager); @@ -125,10 +128,11 @@ contract Deployers { function deployMintAndApproveCurrency() internal returns (Currency currency) { MockERC20 token = deployTokens(1, 2 ** 255)[0]; - address[7] memory toApprove = [ + address[8] memory toApprove = [ address(swapRouter), address(swapRouterNoChecks), address(modifyLiquidityRouter), + address(modifyLiquidityNoChecks), address(donateRouter), address(takeRouter), address(claimsRouter), From 7d970265c3cbada60158ae1e67816c045cf039b6 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Wed, 15 May 2024 09:55:58 -0400 Subject: [PATCH 3/3] Pool.State Library (extsload) (#579) * initial extsload library for Pool.State * example use of PoolStateLibrary in test * formatting * cleanup * wip liquidity fuzzers * more testing * round out fuzz testing * remove native getters from PoolManager; update tests to use lib * resync deps; regenerate snapshots * use syntatic sugar * gas benchmarks * regenerate gas snapshots * add a gas snapshot for getting reserves * cleanup * remove legacy getters * regenerate gas snapshots * move repeated code into helper functions * make pools internal * fix fuzz rejections * Add exttload to read transient storage from outside the contract (#635) * Add exttload to read transient storage from outside the contract * regenerate snapshots * Separate Extsload and Exttload * reduce amount of cases where vm.assume is used to reduce rejected inputs in high intensity fuzzing * make variable public * adjust natspec for exttload * Move isUnlocked, getNonzeroDeltaCount and currencyDelta view functions to pool state library * Don't bound swap amount to liquidity * Don't revert within the view function * regenerate gas snapshots * regenerate gas snapshots * regenerate gas snapshot for getting reserves * adjust comment to reflect new behaviour * include library code in gas snapshot * make slots bytes32 * rename PoolStateLibrary to StateLibrary * Separate transient and storage state libraries * adjust naming of test file * adjust comments to reflect new behaviour * Add sparse exttload * regenerate gas snapshots * code comments * move natspec comment to inline * review comments; fix getTickInfo out-of-order return; add test for getTickInfo == getTickLiquidity * renaming * move code comment to function * regenerate gas snapshots --------- Signed-off-by: saucepoint Co-authored-by: gretzke --- ...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/donate gas with 1 token.snap | 2 +- .../donate gas with 2 tokens.snap | 2 +- .../erc20 collect protocol fees.snap | 2 +- .../extsload getFeeGrowthGlobals.snap | 1 + .../extsload getFeeGrowthInside.snap | 1 + .forge-snapshots/extsload getLiquidity.snap | 1 + .../extsload getPositionInfo.snap | 1 + .../extsload getPositionLiquidity.snap | 1 + .forge-snapshots/extsload getSlot0.snap | 1 + .forge-snapshots/extsload getTickBitmap.snap | 1 + .../extsload getTickFeeGrowthOutside.snap | 1 + .forge-snapshots/extsload getTickInfo.snap | 1 + .../extsload getTickLiquidity.snap | 1 + .forge-snapshots/getReserves.snap | 1 + .forge-snapshots/initialize.snap | 2 +- .../native collect protocol fees.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 +- .../update dynamic fee in before swap.snap | 2 +- src/Extsload.sol | 7 +- src/Exttload.sol | 38 ++ src/PoolManager.sol | 77 +-- src/interfaces/IExttload.sol | 14 + src/interfaces/IPoolManager.sol | 49 +- src/libraries/StateLibrary.sol | 352 ++++++++++++ src/libraries/TransientStateLibrary.sol | 59 ++ src/test/ActionsRouter.sol | 4 + src/test/Fuzzers.sol | 72 +++ src/test/PoolModifyLiquidityTest.sol | 2 + src/test/PoolNestedActionsTest.sol | 7 + src/test/PoolTestBase.sol | 5 + src/test/SkipCallsTestHook.sol | 4 + test/DynamicFees.t.sol | 2 + test/ModifyLiquidity.t.sol | 3 + test/PoolManager.t.sol | 2 + test/PoolManagerInitialize.t.sol | 2 + test/Sync.t.sol | 35 +- test/libraries/Hooks.t.sol | 2 + test/libraries/StateLibrary.t.sol | 520 ++++++++++++++++++ test/utils/AmountHelpers.sol | 5 +- 65 files changed, 1165 insertions(+), 173 deletions(-) create mode 100644 .forge-snapshots/extsload getFeeGrowthGlobals.snap create mode 100644 .forge-snapshots/extsload getFeeGrowthInside.snap create mode 100644 .forge-snapshots/extsload getLiquidity.snap create mode 100644 .forge-snapshots/extsload getPositionInfo.snap create mode 100644 .forge-snapshots/extsload getPositionLiquidity.snap create mode 100644 .forge-snapshots/extsload getSlot0.snap create mode 100644 .forge-snapshots/extsload getTickBitmap.snap create mode 100644 .forge-snapshots/extsload getTickFeeGrowthOutside.snap create mode 100644 .forge-snapshots/extsload getTickInfo.snap create mode 100644 .forge-snapshots/extsload getTickLiquidity.snap create mode 100644 .forge-snapshots/getReserves.snap create mode 100644 src/Exttload.sol create mode 100644 src/interfaces/IExttload.sol create mode 100644 src/libraries/StateLibrary.sol create mode 100644 src/libraries/TransientStateLibrary.sol create mode 100644 src/test/Fuzzers.sol create mode 100644 test/libraries/StateLibrary.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 17f6e7771..3afdc7935 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 @@ -151104 \ No newline at end of file +153625 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity CA fee.snap b/.forge-snapshots/addLiquidity CA fee.snap index 6a9f64f2f..00c38545c 100644 --- a/.forge-snapshots/addLiquidity CA fee.snap +++ b/.forge-snapshots/addLiquidity CA fee.snap @@ -1 +1 @@ -329444 \ No newline at end of file +331831 \ 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 8ce2b9562..b7a0159fe 100644 --- a/.forge-snapshots/addLiquidity with empty hook.snap +++ b/.forge-snapshots/addLiquidity with empty hook.snap @@ -1 +1 @@ -284085 \ No newline at end of file +286606 \ 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 0e0056940..fda825709 100644 --- a/.forge-snapshots/addLiquidity with native token.snap +++ b/.forge-snapshots/addLiquidity with native token.snap @@ -1 +1 @@ -141249 \ No newline at end of file +143835 \ 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 ab53d56de..d29431ac0 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 @@ -299632 \ No newline at end of file +302153 \ No newline at end of file diff --git a/.forge-snapshots/donate gas with 1 token.snap b/.forge-snapshots/donate gas with 1 token.snap index efe7a735d..73919dff7 100644 --- a/.forge-snapshots/donate gas with 1 token.snap +++ b/.forge-snapshots/donate gas with 1 token.snap @@ -1 +1 @@ -108687 \ No newline at end of file +107894 \ No newline at end of file diff --git a/.forge-snapshots/donate gas with 2 tokens.snap b/.forge-snapshots/donate gas with 2 tokens.snap index 6a6f4c603..1b78c96c4 100644 --- a/.forge-snapshots/donate gas with 2 tokens.snap +++ b/.forge-snapshots/donate gas with 2 tokens.snap @@ -1 +1 @@ -149222 \ No newline at end of file +148384 \ No newline at end of file diff --git a/.forge-snapshots/erc20 collect protocol fees.snap b/.forge-snapshots/erc20 collect protocol fees.snap index b54081a54..55214724a 100644 --- a/.forge-snapshots/erc20 collect protocol fees.snap +++ b/.forge-snapshots/erc20 collect protocol fees.snap @@ -1 +1 @@ -57342 \ No newline at end of file +57275 \ No newline at end of file diff --git a/.forge-snapshots/extsload getFeeGrowthGlobals.snap b/.forge-snapshots/extsload getFeeGrowthGlobals.snap new file mode 100644 index 000000000..4e98696eb --- /dev/null +++ b/.forge-snapshots/extsload getFeeGrowthGlobals.snap @@ -0,0 +1 @@ +1344 \ No newline at end of file diff --git a/.forge-snapshots/extsload getFeeGrowthInside.snap b/.forge-snapshots/extsload getFeeGrowthInside.snap new file mode 100644 index 000000000..95bae2dc2 --- /dev/null +++ b/.forge-snapshots/extsload getFeeGrowthInside.snap @@ -0,0 +1 @@ +446 \ No newline at end of file diff --git a/.forge-snapshots/extsload getLiquidity.snap b/.forge-snapshots/extsload getLiquidity.snap new file mode 100644 index 000000000..95bae2dc2 --- /dev/null +++ b/.forge-snapshots/extsload getLiquidity.snap @@ -0,0 +1 @@ +446 \ No newline at end of file diff --git a/.forge-snapshots/extsload getPositionInfo.snap b/.forge-snapshots/extsload getPositionInfo.snap new file mode 100644 index 000000000..38ca8416e --- /dev/null +++ b/.forge-snapshots/extsload getPositionInfo.snap @@ -0,0 +1 @@ +1606 \ No newline at end of file diff --git a/.forge-snapshots/extsload getPositionLiquidity.snap b/.forge-snapshots/extsload getPositionLiquidity.snap new file mode 100644 index 000000000..95bae2dc2 --- /dev/null +++ b/.forge-snapshots/extsload getPositionLiquidity.snap @@ -0,0 +1 @@ +446 \ No newline at end of file diff --git a/.forge-snapshots/extsload getSlot0.snap b/.forge-snapshots/extsload getSlot0.snap new file mode 100644 index 000000000..95bae2dc2 --- /dev/null +++ b/.forge-snapshots/extsload getSlot0.snap @@ -0,0 +1 @@ +446 \ No newline at end of file diff --git a/.forge-snapshots/extsload getTickBitmap.snap b/.forge-snapshots/extsload getTickBitmap.snap new file mode 100644 index 000000000..95bae2dc2 --- /dev/null +++ b/.forge-snapshots/extsload getTickBitmap.snap @@ -0,0 +1 @@ +446 \ No newline at end of file diff --git a/.forge-snapshots/extsload getTickFeeGrowthOutside.snap b/.forge-snapshots/extsload getTickFeeGrowthOutside.snap new file mode 100644 index 000000000..4e98696eb --- /dev/null +++ b/.forge-snapshots/extsload getTickFeeGrowthOutside.snap @@ -0,0 +1 @@ +1344 \ No newline at end of file diff --git a/.forge-snapshots/extsload getTickInfo.snap b/.forge-snapshots/extsload getTickInfo.snap new file mode 100644 index 000000000..38ca8416e --- /dev/null +++ b/.forge-snapshots/extsload getTickInfo.snap @@ -0,0 +1 @@ +1606 \ No newline at end of file diff --git a/.forge-snapshots/extsload getTickLiquidity.snap b/.forge-snapshots/extsload getTickLiquidity.snap new file mode 100644 index 000000000..95bae2dc2 --- /dev/null +++ b/.forge-snapshots/extsload getTickLiquidity.snap @@ -0,0 +1 @@ +446 \ No newline at end of file diff --git a/.forge-snapshots/getReserves.snap b/.forge-snapshots/getReserves.snap new file mode 100644 index 000000000..039b4cd3e --- /dev/null +++ b/.forge-snapshots/getReserves.snap @@ -0,0 +1 @@ +1198 \ No newline at end of file diff --git a/.forge-snapshots/initialize.snap b/.forge-snapshots/initialize.snap index 811a2ae0c..844cfc7d4 100644 --- a/.forge-snapshots/initialize.snap +++ b/.forge-snapshots/initialize.snap @@ -1 +1 @@ -62245 \ No newline at end of file +62202 \ No newline at end of file diff --git a/.forge-snapshots/native collect protocol fees.snap b/.forge-snapshots/native collect protocol fees.snap index 86d096ded..03f835fb2 100644 --- a/.forge-snapshots/native collect protocol fees.snap +++ b/.forge-snapshots/native collect protocol fees.snap @@ -1 +1 @@ -59587 \ No newline at end of file +59520 \ No newline at end of file diff --git a/.forge-snapshots/poolManager bytecode size.snap b/.forge-snapshots/poolManager bytecode size.snap index 367a81773..7c6b7bfe5 100644 --- a/.forge-snapshots/poolManager bytecode size.snap +++ b/.forge-snapshots/poolManager bytecode size.snap @@ -1 +1 @@ -23738 \ No newline at end of file +22099 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity CA fee.snap b/.forge-snapshots/removeLiquidity CA fee.snap index dcc25a494..65e625418 100644 --- a/.forge-snapshots/removeLiquidity CA fee.snap +++ b/.forge-snapshots/removeLiquidity CA fee.snap @@ -1 +1 @@ -184931 \ No newline at end of file +187273 \ 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 372159cc7..7f928a82f 100644 --- a/.forge-snapshots/removeLiquidity with empty hook.snap +++ b/.forge-snapshots/removeLiquidity with empty hook.snap @@ -1 +1 @@ -120975 \ No newline at end of file +123451 \ 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 5f8090dd2..4eb8e2676 100644 --- a/.forge-snapshots/removeLiquidity with native token.snap +++ b/.forge-snapshots/removeLiquidity with native token.snap @@ -1 +1 @@ -117762 \ No newline at end of file +120238 \ 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 2ae596964..6273edbd1 100644 --- a/.forge-snapshots/simple addLiquidity second addition same range.snap +++ b/.forge-snapshots/simple addLiquidity second addition same range.snap @@ -1 +1 @@ -104916 \ No newline at end of file +104914 \ No newline at end of file diff --git a/.forge-snapshots/simple addLiquidity.snap b/.forge-snapshots/simple addLiquidity.snap index cc24ac4fc..724250539 100644 --- a/.forge-snapshots/simple addLiquidity.snap +++ b/.forge-snapshots/simple addLiquidity.snap @@ -1 +1 @@ -167758 \ No newline at end of file +167756 \ 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 3eecd3120..4a0c6b1f7 100644 --- a/.forge-snapshots/simple removeLiquidity some liquidity remains.snap +++ b/.forge-snapshots/simple removeLiquidity some liquidity remains.snap @@ -1 +1 @@ -98535 \ No newline at end of file +98511 \ No newline at end of file diff --git a/.forge-snapshots/simple removeLiquidity.snap b/.forge-snapshots/simple removeLiquidity.snap index 5c5b7f37c..d7d043a4d 100644 --- a/.forge-snapshots/simple removeLiquidity.snap +++ b/.forge-snapshots/simple removeLiquidity.snap @@ -1 +1 @@ -90901 \ No newline at end of file +90877 \ 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 e66a3badf..28df0d4b6 100644 --- a/.forge-snapshots/simple swap with native.snap +++ b/.forge-snapshots/simple swap with native.snap @@ -1 +1 @@ -117351 \ No newline at end of file +117304 \ No newline at end of file diff --git a/.forge-snapshots/simple swap.snap b/.forge-snapshots/simple swap.snap index 4719fd039..8de1894a3 100644 --- a/.forge-snapshots/simple swap.snap +++ b/.forge-snapshots/simple swap.snap @@ -1 +1 @@ -132578 \ No newline at end of file +132465 \ 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 612b68dd0..7e712bbc3 100644 --- a/.forge-snapshots/swap CA custom curve + swap noop.snap +++ b/.forge-snapshots/swap CA custom curve + swap noop.snap @@ -1 +1 @@ -135860 \ No newline at end of file +134799 \ 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 08f6f6124..6ff32412a 100644 --- a/.forge-snapshots/swap CA fee on unspecified.snap +++ b/.forge-snapshots/swap CA fee on unspecified.snap @@ -1 +1 @@ -184809 \ No newline at end of file +183793 \ 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 3affc012e..5c66927fb 100644 --- a/.forge-snapshots/swap against liquidity with native token.snap +++ b/.forge-snapshots/swap against liquidity with native token.snap @@ -1 +1 @@ -113812 \ No newline at end of file +112929 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity.snap b/.forge-snapshots/swap against liquidity.snap index 0372bd1c3..b3ea785f4 100644 --- a/.forge-snapshots/swap against liquidity.snap +++ b/.forge-snapshots/swap against liquidity.snap @@ -1 +1 @@ -125221 \ No newline at end of file +124272 \ 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 9ff1f5c77..082779ca5 100644 --- a/.forge-snapshots/swap burn 6909 for input.snap +++ b/.forge-snapshots/swap burn 6909 for input.snap @@ -1 +1 @@ -137207 \ No newline at end of file +136325 \ 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 d54d7456c..d84033810 100644 --- a/.forge-snapshots/swap burn native 6909 for input.snap +++ b/.forge-snapshots/swap burn native 6909 for input.snap @@ -1 +1 @@ -126335 \ No newline at end of file +125453 \ 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 cf2a52496..0f96f3667 100644 --- a/.forge-snapshots/swap mint native output as 6909.snap +++ b/.forge-snapshots/swap mint native output as 6909.snap @@ -1 +1 @@ -148397 \ No newline at end of file +147514 \ 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 f6f049473..b11435347 100644 --- a/.forge-snapshots/swap mint output as 6909.snap +++ b/.forge-snapshots/swap mint output as 6909.snap @@ -1 +1 @@ -165202 \ No newline at end of file +164319 \ 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 95777a5a8..82a6702da 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 @@ -224701 \ No newline at end of file +223093 \ 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 72f4ce3f4..67215f470 100644 --- a/.forge-snapshots/swap with dynamic fee.snap +++ b/.forge-snapshots/swap with dynamic fee.snap @@ -1 +1 @@ -149469 \ No newline at end of file +148520 \ No newline at end of file diff --git a/.forge-snapshots/swap with hooks.snap b/.forge-snapshots/swap with hooks.snap index 9f4a834a8..c7c0a3673 100644 --- a/.forge-snapshots/swap with hooks.snap +++ b/.forge-snapshots/swap with hooks.snap @@ -1 +1 @@ -125233 \ No newline at end of file +124284 \ 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 7d3d777bd..bfe68b518 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 @@ -181791 \ No newline at end of file +180775 \ 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 cf4bf7d64..93a958ceb 100644 --- a/.forge-snapshots/update dynamic fee in before swap.snap +++ b/.forge-snapshots/update dynamic fee in before swap.snap @@ -1 +1 @@ -160022 \ No newline at end of file +159006 \ No newline at end of file diff --git a/src/Extsload.sol b/src/Extsload.sol index 7dd04284f..d030e1fc3 100644 --- a/src/Extsload.sol +++ b/src/Extsload.sol @@ -6,6 +6,7 @@ import {IExtsload} from "./interfaces/IExtsload.sol"; /// @notice Enables public storage access for efficient state retrieval by external contracts. /// https://eips.ethereum.org/EIPS/eip-2330#rationale abstract contract Extsload is IExtsload { + /// @inheritdoc IExtsload function extsload(bytes32 slot) external view returns (bytes32 value) { /// @solidity memory-safe-assembly assembly { @@ -13,6 +14,7 @@ abstract contract Extsload is IExtsload { } } + /// @inheritdoc IExtsload function extsload(bytes32 startSlot, uint256 nSlots) external view returns (bytes memory) { bytes memory value = new bytes(32 * nSlots); @@ -26,8 +28,11 @@ abstract contract Extsload is IExtsload { return value; } - /// @dev since the function is external and enters a new call context and exits right after execution, Solidity's memory management convention can be disregarded and a direct slice of memory can be returned + /// @inheritdoc IExtsload function extsload(bytes32[] calldata slots) external view returns (bytes32[] memory) { + // since the function is external and enters a new call context and exits right + // after execution, Solidity's memory management convention can be disregarded + // and a direct slice of memory can be returned assembly ("memory-safe") { // abi offset for dynamic array mstore(0, 0x20) diff --git a/src/Exttload.sol b/src/Exttload.sol new file mode 100644 index 000000000..44d46aabe --- /dev/null +++ b/src/Exttload.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import {IExttload} from "./interfaces/IExttload.sol"; + +/// @notice Enables public transient storage access for efficient state retrieval by external contracts. +/// https://eips.ethereum.org/EIPS/eip-2330#rationale +abstract contract Exttload is IExttload { + /// @inheritdoc IExttload + function exttload(bytes32 slot) external view returns (bytes32 value) { + /// @solidity memory-safe-assembly + assembly { + value := tload(slot) + } + } + + /// @inheritdoc IExttload + function exttload(bytes32[] calldata slots) external view returns (bytes32[] memory) { + // since the function is external and enters a new call context and exits right + // after execution, Solidity's memory management convention can be disregarded + // and a direct slice of memory can be returned + assembly ("memory-safe") { + // abi offset for dynamic array + mstore(0, 0x20) + mstore(0x20, slots.length) + let end := add(0x40, shl(5, slots.length)) + let memptr := 0x40 + let calldataptr := slots.offset + for {} 1 {} { + mstore(memptr, tload(calldataload(calldataptr))) + memptr := add(memptr, 0x20) + calldataptr := add(calldataptr, 0x20) + if iszero(lt(memptr, end)) { break } + } + return(0, end) + } + } +} diff --git a/src/PoolManager.sol b/src/PoolManager.sol index d70dce273..131e5a451 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -24,6 +24,7 @@ import {NonZeroDeltaCount} from "./libraries/NonZeroDeltaCount.sol"; import {PoolGetters} from "./libraries/PoolGetters.sol"; import {Reserves} from "./libraries/Reserves.sol"; import {Extsload} from "./Extsload.sol"; +import {Exttload} from "./Exttload.sol"; // 4 // 44 @@ -73,7 +74,8 @@ import {Extsload} from "./Extsload.sol"; // 44444 444 // 444 /// @notice Holds the state for all pools -contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claims, Extsload { + +contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claims, Extsload, Exttload { using PoolIdLibrary for PoolKey; using SafeCast for *; using Pool for *; @@ -91,7 +93,7 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim /// @inheritdoc IPoolManager int24 public constant MIN_TICK_SPACING = TickMath.MIN_TICK_SPACING; - mapping(PoolId id => Pool.State) public pools; + mapping(PoolId id => Pool.State) internal pools; constructor(uint256 controllerGasLimit) ProtocolFees(controllerGasLimit) {} @@ -99,52 +101,6 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim return pools[id]; } - /// @inheritdoc IPoolManager - function getSlot0(PoolId id) - external - view - override - returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) - { - Pool.Slot0 memory slot0 = pools[id].slot0; - - return (slot0.sqrtPriceX96, slot0.tick, slot0.protocolFee, slot0.lpFee); - } - - /// @inheritdoc IPoolManager - function getLiquidity(PoolId id) external view override returns (uint128 liquidity) { - return pools[id].liquidity; - } - - /// @inheritdoc IPoolManager - function getLiquidity(PoolId id, address _owner, int24 tickLower, int24 tickUpper, bytes32 salt) - external - view - override - returns (uint128 liquidity) - { - return pools[id].positions.get(_owner, tickLower, tickUpper, salt).liquidity; - } - - function getPosition(PoolId id, address _owner, int24 tickLower, int24 tickUpper, bytes32 salt) - external - view - override - returns (Position.Info memory position) - { - return pools[id].positions.get(_owner, tickLower, tickUpper, salt); - } - - /// @inheritdoc IPoolManager - function currencyDelta(address caller, Currency currency) external view returns (int256) { - return currency.getDelta(caller); - } - - /// @inheritdoc IPoolManager - function isUnlocked() external view override returns (bool) { - return Lock.isUnlocked(); - } - /// @notice This will revert if the contract is locked modifier onlyWhenUnlocked() { if (!Lock.isUnlocked()) revert ManagerLocked(); @@ -372,29 +328,4 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim PoolId id = key.toId(); pools[id].setLPFee(newDynamicLPFee); } - - function getNonzeroDeltaCount() external view returns (uint256 _nonzeroDeltaCount) { - return NonZeroDeltaCount.read(); - } - - function getPoolTickInfo(PoolId id, int24 tick) external view returns (Pool.TickInfo memory) { - return pools[id].getPoolTickInfo(tick); - } - - function getPoolBitmapInfo(PoolId id, int16 word) external view returns (uint256 tickBitmap) { - return pools[id].getPoolBitmapInfo(word); - } - - /// @notice Temporary view function. Replaceable by transient EXTSLOAD. - function getReserves(Currency currency) external view returns (uint256 balance) { - return currency.getReserves(); - } - - function getFeeGrowthGlobals(PoolId id) - external - view - returns (uint256 feeGrowthGlobal0x128, uint256 feeGrowthGlobal1x128) - { - return pools[id].getFeeGrowthGlobals(); - } } diff --git a/src/interfaces/IExttload.sol b/src/interfaces/IExttload.sol new file mode 100644 index 000000000..ff01c27b9 --- /dev/null +++ b/src/interfaces/IExttload.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +interface IExttload { + /// @notice Called by external contracts to access transient storage of the contract + /// @param slot Key of slot to tload + /// @return value The value of the slot as bytes32 + function exttload(bytes32 slot) external view returns (bytes32 value); + + /// @notice Called by external contracts to access sparse transient pool state + /// @param slots List of slots to tload + /// @return values List of loaded values + function exttload(bytes32[] calldata slots) external view returns (bytes32[] memory values); +} diff --git a/src/interfaces/IPoolManager.sol b/src/interfaces/IPoolManager.sol index 04138aa65..c006d34ab 100644 --- a/src/interfaces/IPoolManager.sol +++ b/src/interfaces/IPoolManager.sol @@ -11,8 +11,9 @@ import {BalanceDelta} from "../types/BalanceDelta.sol"; import {PoolId} from "../types/PoolId.sol"; import {Position} from "../libraries/Position.sol"; import {IExtsload} from "./IExtsload.sol"; +import {IExttload} from "./IExttload.sol"; -interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload { +interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload, IExttload { /// @notice Thrown when a currency is not netted out after the contract is unlocked error CurrencyNotSettled(); @@ -91,60 +92,16 @@ interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload { /// @notice Returns the constant representing the minimum tickSpacing for an initialized pool key function MIN_TICK_SPACING() external view returns (int24); - /// @notice Get the current value in slot0 of the given pool - function getSlot0(PoolId id) - external - view - returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee); - - /// @notice Get the current value of liquidity of the given pool - function getLiquidity(PoolId id) external view returns (uint128 liquidity); - - /// @notice Get the current value of liquidity for the specified pool and position - function getLiquidity(PoolId id, address owner, int24 tickLower, int24 tickUpper, bytes32 salt) - external - view - returns (uint128 liquidity); - - /// @notice Getter for TickInfo for the given poolId and tick - function getPoolTickInfo(PoolId id, int24 tick) external view returns (Pool.TickInfo memory); - - /// @notice Getter for the bitmap given the poolId and word position - function getPoolBitmapInfo(PoolId id, int16 word) external view returns (uint256 tickBitmap); - - /// @notice Getter for the fee growth globals for the given poolId - function getFeeGrowthGlobals(PoolId id) - external - view - returns (uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1); - - /// @notice Get the position struct for a specified pool and position - function getPosition(PoolId id, address owner, int24 tickLower, int24 tickUpper, bytes32 salt) - external - view - returns (Position.Info memory position); - /// @notice Writes the current ERC20 balance of the specified currency to transient storage /// This is used to checkpoint balances for the manager and derive deltas for the caller. /// @dev This MUST be called before any ERC20 tokens are sent into the contract. function sync(Currency currency) external returns (uint256 balance); - /// @notice Returns whether the contract is unlocked or not - function isUnlocked() external view returns (bool); - - /// @notice Returns the number of nonzero deltas open on the PoolManager that must be zerod out before the contract is locked - function getNonzeroDeltaCount() external view returns (uint256 _nonzeroDeltaCount); - /// @notice Initialize the state for a given pool ID function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) external returns (int24 tick); - /// @notice Get the current delta for a caller in the given currency - /// @param caller The address of the caller - /// @param currency The currency for which to lookup the delta - function currencyDelta(address caller, Currency currency) external view returns (int256); - /// @notice All operations go through this function /// @param data Any data to pass to the callback, via `IUnlockCallback(msg.sender).unlockCallback(data)` /// @return The data returned by the call to `IUnlockCallback(msg.sender).unlockCallback(data)` @@ -209,6 +166,4 @@ interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload { /// @notice Updates the pools lp fees for the a pool that has enabled dynamic lp fees. function updateDynamicLPFee(PoolKey memory key, uint24 newDynamicLPFee) external; - - function getReserves(Currency currency) external view returns (uint256 balance); } diff --git a/src/libraries/StateLibrary.sol b/src/libraries/StateLibrary.sol new file mode 100644 index 000000000..9ce49d105 --- /dev/null +++ b/src/libraries/StateLibrary.sol @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {PoolId} from "../types/PoolId.sol"; +import {IPoolManager} from "../interfaces/IPoolManager.sol"; +import {Currency} from "../types/Currency.sol"; +import {Position} from "./Position.sol"; + +library StateLibrary { + // forge inspect src/PoolManager.sol:PoolManager storage --pretty + // | Name | Type | Slot | Offset | Bytes | Contract | + // |-----------------------|-----------------------------------------|------|--------|-------|---------------------------------| + // | pools | mapping(PoolId => struct Pool.State) | 6 | 0 | 32 | src/PoolManager.sol:PoolManager | + uint256 public constant POOLS_SLOT = 6; + + // index of feeGrowthGlobal0X128 in Pool.State + uint256 public constant FEE_GROWTH_GLOBAL0_OFFSET = 1; + // index of feeGrowthGlobal1X128 in Pool.State + uint256 public constant FEE_GROWTH_GLOBAL1_OFFSET = 2; + + // index of liquidity in Pool.State + uint256 public constant LIQUIDITY_OFFSET = 3; + + // index of TicksInfo mapping in Pool.State: mapping(int24 => TickInfo) ticks; + uint256 public constant TICKS_OFFSET = 4; + + // index of tickBitmap mapping in Pool.State + uint256 public constant TICK_BITMAP_OFFSET = 5; + + // index of Position.Info mapping in Pool.State: mapping(bytes32 => Position.Info) positions; + uint256 public constant POSITIONS_OFFSET = 6; + + /** + * @notice Get Slot0 of the pool: sqrtPriceX96, tick, protocolFee, lpFee + * @dev Corresponds to pools[poolId].slot0 + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @return sqrtPriceX96 The square root of the price of the pool, in Q96 precision. + * @return tick The current tick of the pool. + * @return protocolFee The protocol fee of the pool. + * @return lpFee The swap fee of the pool. + */ + function getSlot0(IPoolManager manager, PoolId poolId) + internal + view + returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) + { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = _getPoolStateSlot(poolId); + + bytes32 data = manager.extsload(stateSlot); + + // 24 bits |24bits|24bits |24 bits|160 bits + // 0x000000 |000bb8|000000 |ffff75 |0000000000000000fe3aa841ba359daa0ea9eff7 + // ---------- | fee |protocolfee | tick | sqrtPriceX96 + assembly { + // bottom 160 bits of data + sqrtPriceX96 := and(data, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + // next 24 bits of data + tick := signextend(2, shr(160, data)) + // next 24 bits of data + protocolFee := and(shr(184, data), 0xFFFFFF) + // last 24 bits of data + lpFee := and(shr(208, data), 0xFFFFFF) + } + } + + /** + * @notice Retrieves the tick information of a pool at a specific tick. + * @dev Corresponds to pools[poolId].ticks[tick] + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param tick The tick to retrieve information for. + * @return liquidityGross The total position liquidity that references this tick + * @return liquidityNet The amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left) + * @return feeGrowthOutside0X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + * @return feeGrowthOutside1X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + */ + function getTickInfo(IPoolManager manager, PoolId poolId, int24 tick) + internal + view + returns ( + uint128 liquidityGross, + int128 liquidityNet, + uint256 feeGrowthOutside0X128, + uint256 feeGrowthOutside1X128 + ) + { + bytes32 slot = _getTickInfoSlot(poolId, tick); + + // read all 3 words of the TickInfo struct + bytes memory data = manager.extsload(slot, 3); + assembly { + let firstWord := mload(add(data, 32)) + liquidityNet := sar(128, firstWord) + liquidityGross := and(firstWord, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + feeGrowthOutside0X128 := mload(add(data, 64)) + feeGrowthOutside1X128 := mload(add(data, 96)) + } + } + + /** + * @notice Retrieves the liquidity information of a pool at a specific tick. + * @dev Corresponds to pools[poolId].ticks[tick].liquidityGross and pools[poolId].ticks[tick].liquidityNet. A more gas efficient version of getTickInfo + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param tick The tick to retrieve liquidity for. + * @return liquidityGross The total position liquidity that references this tick + * @return liquidityNet The amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left) + */ + function getTickLiquidity(IPoolManager manager, PoolId poolId, int24 tick) + internal + view + returns (uint128 liquidityGross, int128 liquidityNet) + { + bytes32 slot = _getTickInfoSlot(poolId, tick); + + bytes32 value = manager.extsload(slot); + assembly { + liquidityNet := sar(128, value) + liquidityGross := and(value, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + } + } + + /** + * @notice Retrieves the fee growth outside a tick range of a pool + * @dev Corresponds to pools[poolId].ticks[tick].feeGrowthOutside0X128 and pools[poolId].ticks[tick].feeGrowthOutside1X128. A more gas efficient version of getTickInfo + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param tick The tick to retrieve fee growth for. + * @return feeGrowthOutside0X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + * @return feeGrowthOutside1X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + */ + function getTickFeeGrowthOutside(IPoolManager manager, PoolId poolId, int24 tick) + internal + view + returns (uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128) + { + bytes32 slot = _getTickInfoSlot(poolId, tick); + + // offset by 1 word, since the first word is liquidityGross + liquidityNet + bytes memory data = manager.extsload(bytes32(uint256(slot) + 1), 2); + assembly { + feeGrowthOutside0X128 := mload(add(data, 32)) + feeGrowthOutside1X128 := mload(add(data, 64)) + } + } + + /** + * @notice Retrieves the global fee growth of a pool. + * @dev Corresponds to pools[poolId].feeGrowthGlobal0X128 and pools[poolId].feeGrowthGlobal1X128 + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @return feeGrowthGlobal0 The global fee growth for token0. + * @return feeGrowthGlobal1 The global fee growth for token1. + */ + function getFeeGrowthGlobals(IPoolManager manager, PoolId poolId) + internal + view + returns (uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1) + { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = _getPoolStateSlot(poolId); + + // Pool.State, `uint256 feeGrowthGlobal0X128` + bytes32 slot_feeGrowthGlobal0X128 = bytes32(uint256(stateSlot) + FEE_GROWTH_GLOBAL0_OFFSET); + + // read the 2 words of feeGrowthGlobal + bytes memory data = manager.extsload(slot_feeGrowthGlobal0X128, 2); + assembly { + feeGrowthGlobal0 := mload(add(data, 32)) + feeGrowthGlobal1 := mload(add(data, 64)) + } + } + + /** + * @notice Retrieves total the liquidity of a pool. + * @dev Corresponds to pools[poolId].liquidity + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @return liquidity The liquidity of the pool. + */ + function getLiquidity(IPoolManager manager, PoolId poolId) internal view returns (uint128 liquidity) { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = _getPoolStateSlot(poolId); + + // Pool.State: `uint128 liquidity` + bytes32 slot = bytes32(uint256(stateSlot) + LIQUIDITY_OFFSET); + + liquidity = uint128(uint256(manager.extsload(slot))); + } + + /** + * @notice Retrieves the tick bitmap of a pool at a specific tick. + * @dev Corresponds to pools[poolId].tickBitmap[tick] + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param tick The tick to retrieve the bitmap for. + * @return tickBitmap The bitmap of the tick. + */ + function getTickBitmap(IPoolManager manager, PoolId poolId, int16 tick) + internal + view + returns (uint256 tickBitmap) + { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = _getPoolStateSlot(poolId); + + // Pool.State: `mapping(int16 => uint256) tickBitmap;` + bytes32 tickBitmapMapping = bytes32(uint256(stateSlot) + TICK_BITMAP_OFFSET); + + // slot id of the mapping key: `pools[poolId].tickBitmap[tick] + bytes32 slot = keccak256(abi.encodePacked(int256(tick), tickBitmapMapping)); + + tickBitmap = uint256(manager.extsload(slot)); + } + + /** + * @notice Retrieves the position information of a pool at a specific position ID. + * @dev Corresponds to pools[poolId].positions[positionId] + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param positionId The ID of the position. + * @return liquidity The liquidity of the position. + * @return feeGrowthInside0LastX128 The fee growth inside the position for token0. + * @return feeGrowthInside1LastX128 The fee growth inside the position for token1. + */ + function getPositionInfo(IPoolManager manager, PoolId poolId, bytes32 positionId) + internal + view + returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) + { + bytes32 slot = _getPositionInfoSlot(poolId, positionId); + + // read all 3 words of the Position.Info struct + bytes memory data = manager.extsload(slot, 3); + + assembly { + liquidity := mload(add(data, 32)) + feeGrowthInside0LastX128 := mload(add(data, 64)) + feeGrowthInside1LastX128 := mload(add(data, 96)) + } + } + + function getPosition( + IPoolManager manager, + PoolId poolId, + address owner, + int24 tickLower, + int24 tickUpper, + bytes32 salt + ) internal view returns (Position.Info memory) { + // positionKey = keccak256(abi.encodePacked(owner, tickLower, tickUpper, salt)) + bytes32 positionKey; + + /// @solidity memory-safe-assembly + assembly { + mstore(0x26, salt) // [0x26, 0x46) + mstore(0x06, tickUpper) // [0x23, 0x26) + mstore(0x03, tickLower) // [0x20, 0x23) + mstore(0, owner) // [0x0c, 0x20) + positionKey := keccak256(0x0c, 0x3a) // len is 58 bytes + mstore(0x26, 0) // rewrite 0x26 to 0 + } + (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) = + getPositionInfo(manager, poolId, positionKey); + return Position.Info({ + liquidity: liquidity, + feeGrowthInside0LastX128: feeGrowthInside0LastX128, + feeGrowthInside1LastX128: feeGrowthInside1LastX128 + }); + } + + /** + * @notice Retrieves the liquidity of a position. + * @dev Corresponds to pools[poolId].positions[positionId].liquidity. A more gas efficient version of getPositionInfo + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param positionId The ID of the position. + * @return liquidity The liquidity of the position. + */ + function getPositionLiquidity(IPoolManager manager, PoolId poolId, bytes32 positionId) + internal + view + returns (uint128 liquidity) + { + bytes32 slot = _getPositionInfoSlot(poolId, positionId); + liquidity = uint128(uint256(manager.extsload(slot))); + } + + /** + * @notice Live calculate the fee growth inside a tick range of a pool + * @dev pools[poolId].feeGrowthInside0LastX128 in Position.Info is cached and can become stale. This function will live calculate the feeGrowthInside + * @param manager The pool manager contract. + * @param poolId The ID of the pool. + * @param tickLower The lower tick of the range. + * @param tickUpper The upper tick of the range. + * @return feeGrowthInside0X128 The fee growth inside the tick range for token0. + * @return feeGrowthInside1X128 The fee growth inside the tick range for token1. + */ + function getFeeGrowthInside(IPoolManager manager, PoolId poolId, int24 tickLower, int24 tickUpper) + internal + view + returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) + { + (uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = getFeeGrowthGlobals(manager, poolId); + + (uint256 lowerFeeGrowthOutside0X128, uint256 lowerFeeGrowthOutside1X128) = + getTickFeeGrowthOutside(manager, poolId, tickLower); + (uint256 upperFeeGrowthOutside0X128, uint256 upperFeeGrowthOutside1X128) = + getTickFeeGrowthOutside(manager, poolId, tickUpper); + (, int24 tickCurrent,,) = getSlot0(manager, poolId); + unchecked { + if (tickCurrent < tickLower) { + feeGrowthInside0X128 = lowerFeeGrowthOutside0X128 - upperFeeGrowthOutside0X128; + feeGrowthInside1X128 = lowerFeeGrowthOutside1X128 - upperFeeGrowthOutside1X128; + } else if (tickCurrent >= tickUpper) { + feeGrowthInside0X128 = upperFeeGrowthOutside0X128 - lowerFeeGrowthOutside0X128; + feeGrowthInside1X128 = upperFeeGrowthOutside1X128 - lowerFeeGrowthOutside1X128; + } else { + feeGrowthInside0X128 = feeGrowthGlobal0X128 - lowerFeeGrowthOutside0X128 - upperFeeGrowthOutside0X128; + feeGrowthInside1X128 = feeGrowthGlobal1X128 - lowerFeeGrowthOutside1X128 - upperFeeGrowthOutside1X128; + } + } + } + + function _getPoolStateSlot(PoolId poolId) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(PoolId.unwrap(poolId), bytes32(POOLS_SLOT))); + } + + function _getTickInfoSlot(PoolId poolId, int24 tick) internal pure returns (bytes32) { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = _getPoolStateSlot(poolId); + + // Pool.State: `mapping(int24 => TickInfo) ticks` + bytes32 ticksMappingSlot = bytes32(uint256(stateSlot) + TICKS_OFFSET); + + // slot key of the tick key: `pools[poolId].ticks[tick] + return keccak256(abi.encodePacked(int256(tick), ticksMappingSlot)); + } + + function _getPositionInfoSlot(PoolId poolId, bytes32 positionId) internal pure returns (bytes32 slot) { + // slot key of Pool.State value: `pools[poolId]` + bytes32 stateSlot = _getPoolStateSlot(poolId); + + // Pool.State: `mapping(bytes32 => Position.Info) positions;` + bytes32 positionMapping = bytes32(uint256(stateSlot) + POSITIONS_OFFSET); + + // slot of the mapping key: `pools[poolId].positions[positionId] + return keccak256(abi.encodePacked(positionId, positionMapping)); + } +} diff --git a/src/libraries/TransientStateLibrary.sol b/src/libraries/TransientStateLibrary.sol new file mode 100644 index 000000000..6d746c913 --- /dev/null +++ b/src/libraries/TransientStateLibrary.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {PoolId} from "../types/PoolId.sol"; +import {IPoolManager} from "../interfaces/IPoolManager.sol"; +import {Currency} from "../types/Currency.sol"; +import {Position} from "./Position.sol"; + +library TransientStateLibrary { + /// bytes32(uint256(keccak256("ReservesOf")) - 1) + bytes32 public constant RESERVES_OF_SLOT = 0x1e0745a7db1623981f0b2a5d4232364c00787266eb75ad546f190e6cebe9bd95; + + // The slot holding the number of nonzero deltas. bytes32(uint256(keccak256("NonzeroDeltaCount")) - 1) + bytes32 public constant NONZERO_DELTA_COUNT_SLOT = + 0x7d4b3164c6e45b97e7d87b7125a44c5828d005af88f9d751cfd78729c5d99a0b; + + // The slot holding the unlocked state, transiently. bytes32(uint256(keccak256("Unlocked")) - 1) + bytes32 public constant IS_UNLOCKED_SLOT = 0xc090fc4683624cfc3884e9d8de5eca132f2d0ec062aff75d43c0465d5ceeab23; + + /// @notice returns the reserves of a currency + /// @param manager The pool manager contract. + /// @param currency The currency to get the reserves for. + /// @return value The reserves of the currency. + /// @dev returns 0 if the reserves are not synced + /// @dev returns type(uint256).max if the reserves are synced but the value is 0 + function getReserves(IPoolManager manager, Currency currency) internal view returns (uint256) { + bytes32 slot = RESERVES_OF_SLOT; + bytes32 key; + assembly { + mstore(0, slot) + mstore(32, currency) + key := keccak256(0, 64) + } + return uint256(manager.exttload(key)); + } + + /// @notice Returns the number of nonzero deltas open on the PoolManager that must be zerod out before the contract is locked + function getNonzeroDeltaCount(IPoolManager manager) internal view returns (uint256) { + return uint256(manager.exttload(NONZERO_DELTA_COUNT_SLOT)); + } + + /// @notice Get the current delta for a caller in the given currency + /// @param caller_ The address of the caller + /// @param currency The currency for which to lookup the delta + function currencyDelta(IPoolManager manager, address caller_, Currency currency) internal view returns (int256) { + bytes32 key; + assembly { + mstore(0, caller_) + mstore(32, currency) + key := keccak256(0, 64) + } + return int256(uint256(manager.exttload(key))); + } + + /// @notice Returns whether the contract is unlocked or not + function isUnlocked(IPoolManager manager) internal view returns (bool) { + return manager.exttload(IS_UNLOCKED_SLOT) != 0x0; + } +} diff --git a/src/test/ActionsRouter.sol b/src/test/ActionsRouter.sol index b79ae7c3d..71b40f383 100644 --- a/src/test/ActionsRouter.sol +++ b/src/test/ActionsRouter.sol @@ -7,6 +7,8 @@ import {IUnlockCallback} from "../interfaces/callback/IUnlockCallback.sol"; import {Currency, CurrencyLibrary} from "../types/Currency.sol"; import {PoolKey} from "../types/PoolKey.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {StateLibrary} from "../libraries/StateLibrary.sol"; +import {TransientStateLibrary} from "../libraries/TransientStateLibrary.sol"; // Supported Actions. enum Actions { @@ -29,6 +31,8 @@ enum Actions { /// TODO: Can continue to add functions per action. contract ActionsRouter is IUnlockCallback, Test { using CurrencyLibrary for Currency; + using StateLibrary for IPoolManager; + using TransientStateLibrary for IPoolManager; error ActionNotSupported(); diff --git a/src/test/Fuzzers.sol b/src/test/Fuzzers.sol new file mode 100644 index 000000000..4ad5e9cb8 --- /dev/null +++ b/src/test/Fuzzers.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Vm} from "forge-std/Vm.sol"; +import {StdUtils} from "forge-std/StdUtils.sol"; + +import {IPoolManager} from "../interfaces/IPoolManager.sol"; +import {PoolKey} from "../types/PoolKey.sol"; +import {BalanceDelta} from "../types/BalanceDelta.sol"; +import {TickMath} from "../libraries/TickMath.sol"; +import {Pool} from "../libraries/Pool.sol"; +import {PoolModifyLiquidityTest} from "./PoolModifyLiquidityTest.sol"; + +contract Fuzzers is StdUtils { + 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 boundTicks(PoolKey memory key, int24 tickLower, int24 tickUpper) internal pure returns (int24, int24) { + tickLower = int24( + bound( + int256(tickLower), + int256(TickMath.minUsableTick(key.tickSpacing)), + int256(TickMath.maxUsableTick(key.tickSpacing)) + ) + ); + tickUpper = int24( + bound( + int256(tickUpper), + int256(TickMath.minUsableTick(key.tickSpacing)), + int256(TickMath.maxUsableTick(key.tickSpacing)) + ) + ); + + // round down ticks + tickLower = (tickLower / key.tickSpacing) * key.tickSpacing; + tickUpper = (tickUpper / key.tickSpacing) * key.tickSpacing; + + (tickLower, tickUpper) = tickLower < tickUpper ? (tickLower, tickUpper) : (tickUpper, tickLower); + + _vm.assume(tickLower != tickUpper); + + return (tickLower, tickUpper); + } + + /// @dev Obtain fuzzed 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) + { + (result.tickLower, result.tickUpper) = boundTicks(key, params.tickLower, params.tickUpper); + int256 liquidityDelta = boundLiquidityDelta(key, params.liquidityDelta); + result.liquidityDelta = liquidityDelta; + } + + function createFuzzyLiquidity( + PoolModifyLiquidityTest modifyLiquidityRouter, + PoolKey memory key, + IPoolManager.ModifyLiquidityParams memory params, + bytes memory hookData + ) internal returns (IPoolManager.ModifyLiquidityParams memory result, BalanceDelta delta) { + result = createFuzzyLiquidityParams(key, params); + delta = modifyLiquidityRouter.modifyLiquidity(key, result, hookData); + } +} diff --git a/src/test/PoolModifyLiquidityTest.sol b/src/test/PoolModifyLiquidityTest.sol index fe290272b..8e9decb35 100644 --- a/src/test/PoolModifyLiquidityTest.sol +++ b/src/test/PoolModifyLiquidityTest.sol @@ -11,6 +11,7 @@ import {IHooks} from "../interfaces/IHooks.sol"; import {Hooks} from "../libraries/Hooks.sol"; import {LPFeeLibrary} from "../libraries/LPFeeLibrary.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; +import {StateLibrary} from "../libraries/StateLibrary.sol"; contract PoolModifyLiquidityTest is PoolTestBase { using CurrencyLibrary for Currency; @@ -18,6 +19,7 @@ contract PoolModifyLiquidityTest is PoolTestBase { using Hooks for IHooks; using LPFeeLibrary for uint24; using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; constructor(IPoolManager _manager) PoolTestBase(_manager) {} diff --git a/src/test/PoolNestedActionsTest.sol b/src/test/PoolNestedActionsTest.sol index d7e065162..57ffe33fb 100644 --- a/src/test/PoolNestedActionsTest.sol +++ b/src/test/PoolNestedActionsTest.sol @@ -11,6 +11,8 @@ import {BalanceDelta} from "../types/BalanceDelta.sol"; import {Currency} from "../types/Currency.sol"; import {PoolId, PoolIdLibrary} from "../types/PoolId.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; +import {StateLibrary} from "../libraries/StateLibrary.sol"; +import {TransientStateLibrary} from "../libraries/TransientStateLibrary.sol"; enum Action { NESTED_SELF_UNLOCK, @@ -23,6 +25,9 @@ enum Action { } contract PoolNestedActionsTest is Test, IUnlockCallback { + using StateLibrary for IPoolManager; + using TransientStateLibrary for IPoolManager; + IPoolManager manager; NestedActionExecutor public executor; address user; @@ -59,6 +64,8 @@ contract PoolNestedActionsTest is Test, IUnlockCallback { } contract NestedActionExecutor is Test, PoolTestBase { + using StateLibrary for IPoolManager; + using TransientStateLibrary for IPoolManager; using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; diff --git a/src/test/PoolTestBase.sol b/src/test/PoolTestBase.sol index c52a5f046..4fb7d1ad1 100644 --- a/src/test/PoolTestBase.sol +++ b/src/test/PoolTestBase.sol @@ -7,8 +7,13 @@ import {IERC20Minimal} from "../interfaces/external/IERC20Minimal.sol"; import {IUnlockCallback} from "../interfaces/callback/IUnlockCallback.sol"; import {IPoolManager} from "../interfaces/IPoolManager.sol"; +import {StateLibrary} from "../libraries/StateLibrary.sol"; +import {TransientStateLibrary} from "../libraries/TransientStateLibrary.sol"; + abstract contract PoolTestBase is IUnlockCallback { using CurrencyLibrary for Currency; + using StateLibrary for IPoolManager; + using TransientStateLibrary for IPoolManager; IPoolManager public immutable manager; diff --git a/src/test/SkipCallsTestHook.sol b/src/test/SkipCallsTestHook.sol index 50035ca96..cc42cdeb8 100644 --- a/src/test/SkipCallsTestHook.sol +++ b/src/test/SkipCallsTestHook.sol @@ -14,12 +14,16 @@ import {PoolTestBase} from "./PoolTestBase.sol"; import {Constants} from "../../test/utils/Constants.sol"; import {Test} from "forge-std/Test.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; +import {StateLibrary} from "../libraries/StateLibrary.sol"; +import {TransientStateLibrary} from "../libraries/TransientStateLibrary.sol"; import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../types/BeforeSwapDelta.sol"; contract SkipCallsTestHook is BaseTestHooks, Test { using CurrencySettleTake for Currency; using PoolIdLibrary for PoolKey; using Hooks for IHooks; + using StateLibrary for IPoolManager; + using TransientStateLibrary for IPoolManager; uint256 public counter; IPoolManager manager; diff --git a/test/DynamicFees.t.sol b/test/DynamicFees.t.sol index 6d72a3b86..863daadc6 100644 --- a/test/DynamicFees.t.sol +++ b/test/DynamicFees.t.sol @@ -19,8 +19,10 @@ import {Currency, CurrencyLibrary} from "../src/types/Currency.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; import {Pool} from "../src/libraries/Pool.sol"; import {BalanceDelta, BalanceDeltaLibrary} from "../src/types/BalanceDelta.sol"; +import {StateLibrary} from "../src/libraries/StateLibrary.sol"; contract TestDynamicFees is Test, Deployers, GasSnapshot { + using StateLibrary for IPoolManager; using PoolIdLibrary for PoolKey; DynamicFeesTestHook dynamicFeesHooks = DynamicFeesTestHook( diff --git a/test/ModifyLiquidity.t.sol b/test/ModifyLiquidity.t.sol index b0007ec00..2b55bf135 100644 --- a/test/ModifyLiquidity.t.sol +++ b/test/ModifyLiquidity.t.sol @@ -13,8 +13,11 @@ import {PoolModifyLiquidityTest} from "../src/test/PoolModifyLiquidityTest.sol"; import {Constants} from "./utils/Constants.sol"; import {Currency} from "src/types/Currency.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {StateLibrary} from "src/libraries/StateLibrary.sol"; contract ModifyLiquidityTest is Test, Deployers, GasSnapshot { + using StateLibrary for IPoolManager; + PoolKey simpleKey; // vanilla pool key PoolId simplePoolId; // id for vanilla pool key diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index 188389c66..8173b665b 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -35,6 +35,7 @@ import {SafeCast} from "../src/libraries/SafeCast.sol"; import {AmountHelpers} from "./utils/AmountHelpers.sol"; import {ProtocolFeeLibrary} from "../src/libraries/ProtocolFeeLibrary.sol"; import {IProtocolFees} from "../src/interfaces/IProtocolFees.sol"; +import {StateLibrary} from "../src/libraries/StateLibrary.sol"; contract PoolManagerTest is Test, Deployers, GasSnapshot { using Hooks for IHooks; @@ -44,6 +45,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { using SafeCast for uint256; using SafeCast for uint128; using ProtocolFeeLibrary for uint24; + using StateLibrary for IPoolManager; event UnlockCallback(); event ProtocolFeeControllerUpdated(address feeController); diff --git a/test/PoolManagerInitialize.t.sol b/test/PoolManagerInitialize.t.sol index 1669aa57b..b2ac350f6 100644 --- a/test/PoolManagerInitialize.t.sol +++ b/test/PoolManagerInitialize.t.sol @@ -22,12 +22,14 @@ import {LPFeeLibrary} from "../src/libraries/LPFeeLibrary.sol"; import {ProtocolFeeControllerTest} from "../src/test/ProtocolFeeControllerTest.sol"; import {IProtocolFeeController} from "../src/interfaces/IProtocolFeeController.sol"; import {ProtocolFeeLibrary} from "../src/libraries/ProtocolFeeLibrary.sol"; +import {StateLibrary} from "../src/libraries/StateLibrary.sol"; contract PoolManagerInitializeTest is Test, Deployers, GasSnapshot { using Hooks for IHooks; using PoolIdLibrary for PoolKey; using LPFeeLibrary for uint24; using ProtocolFeeLibrary for uint24; + using StateLibrary for IPoolManager; event Initialize( PoolId poolId, diff --git a/test/Sync.t.sol b/test/Sync.t.sol index dddffef9f..968d34918 100644 --- a/test/Sync.t.sol +++ b/test/Sync.t.sol @@ -15,9 +15,13 @@ import {PoolKey} from "../src/types/PoolKey.sol"; import {ActionsRouter, Actions} from "../src/test/ActionsRouter.sol"; import {SafeCast} from "../src/libraries/SafeCast.sol"; import {Reserves} from "../src/libraries/Reserves.sol"; +import {StateLibrary} from "../src/libraries/StateLibrary.sol"; +import {TransientStateLibrary} from "../src/libraries/TransientStateLibrary.sol"; contract SyncTest is Test, Deployers, GasSnapshot { using CurrencyLibrary for Currency; + using StateLibrary for IPoolManager; + using TransientStateLibrary for IPoolManager; // PoolManager has no balance of currency2. Currency currency2; @@ -34,16 +38,15 @@ contract SyncTest is Test, Deployers, GasSnapshot { uint256 balance = manager.sync(currency2); assertEq(uint256(balance), 0); - assertEq(manager.getReserves(currency2), 0); + assertEq(manager.getReserves(currency2), type(uint256).max); } function test_sync_balanceIsNonZero() public noIsolate { uint256 currency0Balance = currency0.balanceOf(address(manager)); assertGt(currency0Balance, uint256(0)); - // Without calling sync, getReserves should revert. - vm.expectRevert(Reserves.ReservesMustBeSynced.selector); - manager.getReserves(currency0); + // Without calling sync, getReserves should return 0. + assertEq(manager.getReserves(currency0), 0); uint256 balance = manager.sync(currency0); assertEq(balance, currency0Balance, "balance not equal"); @@ -60,8 +63,7 @@ contract SyncTest is Test, Deployers, GasSnapshot { PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); // Sync has not been called. - vm.expectRevert(Reserves.ReservesMustBeSynced.selector); - manager.getReserves(currency0); + assertEq(manager.getReserves(currency0), 0); swapRouter.swap(key, params, testSettings, new bytes(0)); (uint256 balanceCurrency0) = currency0.balanceOf(address(manager)); @@ -78,8 +80,7 @@ contract SyncTest is Test, Deployers, GasSnapshot { manager.initialize(key2, SQRT_PRICE_1_1, new bytes(0)); // Sync has not been called. - vm.expectRevert(Reserves.ReservesMustBeSynced.selector); - manager.getReserves(currency2); + assertEq(manager.getReserves(currency2), 0); modifyLiquidityRouter.modifyLiquidity(key2, IPoolManager.ModifyLiquidityParams(-60, 60, 100, 0), new bytes(0)); (uint256 balanceCurrency2) = currency2.balanceOf(address(manager)); assertEq(manager.getReserves(currency2), balanceCurrency2); @@ -101,11 +102,10 @@ contract SyncTest is Test, Deployers, GasSnapshot { assertEq(currency2.balanceOf(address(manager)), uint256(0)); // Sync has not been called. - vm.expectRevert(Reserves.ReservesMustBeSynced.selector); - manager.getReserves(currency2); + assertEq(manager.getReserves(currency2), 0); manager.sync(currency2); - assertEq(manager.getReserves(currency2), 0); + assertEq(manager.getReserves(currency2), type(uint256).max); Actions[] memory actions = new Actions[](2); bytes[] memory params = new bytes[](2); @@ -124,8 +124,7 @@ contract SyncTest is Test, Deployers, GasSnapshot { uint256 currency0Balance = currency0.balanceOf(address(manager)); // Sync has not been called. - vm.expectRevert(Reserves.ReservesMustBeSynced.selector); - manager.getReserves(currency0); + assertEq(manager.getReserves(currency0), 0); manager.sync(currency0); assertEq(manager.getReserves(currency0), currency0Balance); @@ -153,12 +152,11 @@ contract SyncTest is Test, Deployers, GasSnapshot { MockERC20(Currency.unwrap(currency3)).approve(address(router), type(uint256).max); // Sync has not been called on currency3. - vm.expectRevert(Reserves.ReservesMustBeSynced.selector); - manager.getReserves(currency3); + assertEq(manager.getReserves(currency3), 0); manager.sync(currency3); // Sync has been called. - assertEq(manager.getReserves(currency3), 0); + assertEq(manager.getReserves(currency3), type(uint256).max); uint256 maxBalanceCurrency3 = uint256(int256(type(int128).max)); @@ -230,7 +228,10 @@ contract SyncTest is Test, Deployers, GasSnapshot { bytes[] memory params = new bytes[](8); manager.sync(currency0); - assertEq(manager.getReserves(currency0), managerCurrency0BalanceBefore); // reserves are 100. + snapStart("getReserves"); + uint256 reserves = manager.getReserves(currency0); + snapEnd(); + assertEq(reserves, managerCurrency0BalanceBefore); // reserves are 100. actions[0] = Actions.TAKE; params[0] = abi.encode(currency0, address(this), 10); diff --git a/test/libraries/Hooks.t.sol b/test/libraries/Hooks.t.sol index 7dca256e0..9682d59b8 100644 --- a/test/libraries/Hooks.t.sol +++ b/test/libraries/Hooks.t.sol @@ -20,10 +20,12 @@ import {PoolId, PoolIdLibrary} from "src/types/PoolId.sol"; import {PoolKey} from "src/types/PoolKey.sol"; import {IERC20Minimal} from "src/interfaces/external/IERC20Minimal.sol"; import {BalanceDelta} from "src/types/BalanceDelta.sol"; +import {StateLibrary} from "src/libraries/StateLibrary.sol"; contract HooksTest is Test, Deployers, GasSnapshot { using PoolIdLibrary for PoolKey; using Hooks for IHooks; + using StateLibrary for IPoolManager; /// 1111 1111 1111 1100 address payable ALL_HOOKS_ADDRESS = payable(0xFffC000000000000000000000000000000000000); diff --git a/test/libraries/StateLibrary.t.sol b/test/libraries/StateLibrary.t.sol new file mode 100644 index 000000000..d54e4a304 --- /dev/null +++ b/test/libraries/StateLibrary.t.sol @@ -0,0 +1,520 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {IHooks} from "../../src/interfaces/IHooks.sol"; +import {Hooks} from "../../src/libraries/Hooks.sol"; +import {TickMath} from "../../src/libraries/TickMath.sol"; +import {IPoolManager} from "../../src/interfaces/IPoolManager.sol"; +import {PoolKey} from "../../src/types/PoolKey.sol"; +import {BalanceDelta} from "../../src/types/BalanceDelta.sol"; +import {PoolId, PoolIdLibrary} from "../../src/types/PoolId.sol"; +import {CurrencyLibrary, Currency} from "../../src/types/Currency.sol"; +import {Deployers} from "../utils/Deployers.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {Pool} from "../../src/libraries/Pool.sol"; +import {TickBitmap} from "../../src/libraries/TickBitmap.sol"; +import {FixedPoint128} from "../../src/libraries/FixedPoint128.sol"; + +import {StateLibrary} from "../../src/libraries/StateLibrary.sol"; +import {Fuzzers} from "../../src/test/Fuzzers.sol"; + +contract StateLibraryTest is Test, Deployers, Fuzzers, GasSnapshot { + using FixedPointMathLib for uint256; + using PoolIdLibrary for PoolKey; + using CurrencyLibrary for Currency; + + PoolId poolId; + + function setUp() public { + // creates the pool manager, utility routers, and test tokens + Deployers.deployFreshManagerAndRouters(); + (currency0, currency1) = Deployers.deployMintAndApprove2Currencies(); + + // Create the pool + key = PoolKey(currency0, currency1, 3000, 60, IHooks(address(0x0))); + poolId = key.toId(); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); + } + + function test_getSlot0() public { + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, 10_000 ether, 0), ZERO_BYTES + ); + + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-600, 600, 10_000 ether, 0), ZERO_BYTES + ); + + // swap to create fees, crossing a tick + uint256 swapAmount = 100 ether; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 swapFee) = StateLibrary.getSlot0(manager, poolId); + snapLastCall("extsload getSlot0"); + assertEq(tick, -139); + + // magic number verified against a native getter + assertEq(sqrtPriceX96, 78680104762184586858280382455); + assertEq(tick, -139); + assertEq(protocolFee, 0); // tested in protocol fee tests + assertEq(swapFee, 3000); + } + + function test_getTickLiquidity() public { + modifyLiquidityRouter.modifyLiquidity(key, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), ZERO_BYTES); + + (uint128 liquidityGrossLower, int128 liquidityNetLower) = StateLibrary.getTickLiquidity(manager, poolId, -60); + snapLastCall("extsload getTickLiquidity"); + assertEq(liquidityGrossLower, 10 ether); + assertEq(liquidityNetLower, 10 ether); + + (uint128 liquidityGrossUpper, int128 liquidityNetUpper) = StateLibrary.getTickLiquidity(manager, poolId, 60); + assertEq(liquidityGrossUpper, 10 ether); + assertEq(liquidityNetUpper, -10 ether); + } + + function test_fuzz_getTickLiquidity(IPoolManager.ModifyLiquidityParams memory params) public { + (IPoolManager.ModifyLiquidityParams memory _params,) = + Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, params, ZERO_BYTES); + uint128 liquidityDelta = uint128(uint256(_params.liquidityDelta)); + + (uint128 liquidityGrossLower, int128 liquidityNetLower) = + StateLibrary.getTickLiquidity(manager, poolId, _params.tickLower); + assertEq(liquidityGrossLower, liquidityDelta); + assertEq(liquidityNetLower, int128(_params.liquidityDelta)); + + (uint128 liquidityGrossUpper, int128 liquidityNetUpper) = + StateLibrary.getTickLiquidity(manager, poolId, _params.tickUpper); + assertEq(liquidityGrossUpper, liquidityDelta); + assertEq(liquidityNetUpper, -int128(_params.liquidityDelta)); + + // confirm agreement with getTickInfo() + (uint128 _liquidityGrossLower, int128 _liquidityNetLower,,) = + StateLibrary.getTickInfo(manager, poolId, _params.tickLower); + assertEq(_liquidityGrossLower, liquidityGrossLower); + assertEq(_liquidityNetLower, liquidityNetLower); + + (uint128 _liquidityGrossUpper, int128 _liquidityNetUpper,,) = + StateLibrary.getTickInfo(manager, poolId, _params.tickUpper); + assertEq(_liquidityGrossUpper, liquidityGrossUpper); + assertEq(_liquidityNetUpper, liquidityNetUpper); + } + + function test_fuzz_getTickLiquidity_two_positions( + 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); + + uint128 liquidityDeltaA = uint128(uint256(_paramsA.liquidityDelta)); + uint128 liquidityDeltaB = uint128(uint256(_paramsB.liquidityDelta)); + + (uint128 liquidityGrossLowerA, int128 liquidityNetLowerA) = + StateLibrary.getTickLiquidity(manager, poolId, _paramsA.tickLower); + (uint128 liquidityGrossLowerB, int128 liquidityNetLowerB) = + StateLibrary.getTickLiquidity(manager, poolId, _paramsB.tickLower); + (uint256 liquidityGrossUpperA, int256 liquidityNetUpperA) = + StateLibrary.getTickLiquidity(manager, poolId, _paramsA.tickUpper); + (uint256 liquidityGrossUpperB, int256 liquidityNetUpperB) = + StateLibrary.getTickLiquidity(manager, poolId, _paramsB.tickUpper); + + // when tick lower is shared between two positions, the gross liquidity is the sum + if (_paramsA.tickLower == _paramsB.tickLower || _paramsA.tickLower == _paramsB.tickUpper) { + assertEq(liquidityGrossLowerA, liquidityDeltaA + liquidityDeltaB); + + // when tick lower is shared with an upper tick, the net liquidity is the difference + (_paramsA.tickLower == _paramsB.tickLower) + ? assertEq(liquidityNetLowerA, int128(liquidityDeltaA + liquidityDeltaB)) + : assertApproxEqAbs(liquidityNetLowerA, int128(liquidityDeltaA) - int128(liquidityDeltaB), 1 wei); + } else { + assertEq(liquidityGrossLowerA, liquidityDeltaA); + assertEq(liquidityNetLowerA, int128(liquidityDeltaA)); + } + + if (_paramsA.tickUpper == _paramsB.tickLower || _paramsA.tickUpper == _paramsB.tickUpper) { + assertEq(liquidityGrossUpperA, liquidityDeltaA + liquidityDeltaB); + (_paramsA.tickUpper == _paramsB.tickUpper) + ? assertEq(liquidityNetUpperA, -int128(liquidityDeltaA + liquidityDeltaB)) + : assertApproxEqAbs(liquidityNetUpperA, int128(liquidityDeltaB) - int128(liquidityDeltaA), 2 wei); + } else { + assertEq(liquidityGrossUpperA, liquidityDeltaA); + assertEq(liquidityNetUpperA, -int128(liquidityDeltaA)); + } + + if (_paramsB.tickLower == _paramsA.tickLower || _paramsB.tickLower == _paramsA.tickUpper) { + assertEq(liquidityGrossLowerB, liquidityDeltaA + liquidityDeltaB); + (_paramsB.tickLower == _paramsA.tickLower) + ? assertEq(liquidityNetLowerB, int128(liquidityDeltaA + liquidityDeltaB)) + : assertApproxEqAbs(liquidityNetLowerB, int128(liquidityDeltaB) - int128(liquidityDeltaA), 1 wei); + } else { + assertEq(liquidityGrossLowerB, liquidityDeltaB); + assertEq(liquidityNetLowerB, int128(liquidityDeltaB)); + } + + if (_paramsB.tickUpper == _paramsA.tickLower || _paramsB.tickUpper == _paramsA.tickUpper) { + assertEq(liquidityGrossUpperB, liquidityDeltaA + liquidityDeltaB); + (_paramsB.tickUpper == _paramsA.tickUpper) + ? assertEq(liquidityNetUpperB, -int128(liquidityDeltaA + liquidityDeltaB)) + : assertApproxEqAbs(liquidityNetUpperB, int128(liquidityDeltaA) - int128(liquidityDeltaB), 2 wei); + } else { + assertEq(liquidityGrossUpperB, liquidityDeltaB); + assertEq(liquidityNetUpperB, -int128(liquidityDeltaB)); + } + } + + function test_getFeeGrowthGlobals0() public { + // create liquidity + uint256 liquidity = 10_000 ether; + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, int256(liquidity), 0), ZERO_BYTES + ); + + (uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1) = StateLibrary.getFeeGrowthGlobals(manager, poolId); + assertEq(feeGrowthGlobal0, 0); + assertEq(feeGrowthGlobal1, 0); + + // swap to create fees on the input token (currency0) + uint256 swapAmount = 10 ether; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + + (feeGrowthGlobal0, feeGrowthGlobal1) = StateLibrary.getFeeGrowthGlobals(manager, poolId); + snapLastCall("extsload getFeeGrowthGlobals"); + + uint256 feeGrowthGlobalCalc = swapAmount.mulWadDown(0.003e18).mulDivDown(FixedPoint128.Q128, liquidity); + assertEq(feeGrowthGlobal0, feeGrowthGlobalCalc); + assertEq(feeGrowthGlobal1, 0); + } + + function test_getFeeGrowthGlobals1() public { + // create liquidity + uint256 liquidity = 10_000 ether; + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, int256(liquidity), 0), ZERO_BYTES + ); + + (uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1) = StateLibrary.getFeeGrowthGlobals(manager, poolId); + assertEq(feeGrowthGlobal0, 0); + assertEq(feeGrowthGlobal1, 0); + + // swap to create fees on the input token (currency1) + uint256 swapAmount = 10 ether; + swap(key, false, -int256(swapAmount), ZERO_BYTES); + + (feeGrowthGlobal0, feeGrowthGlobal1) = StateLibrary.getFeeGrowthGlobals(manager, poolId); + + assertEq(feeGrowthGlobal0, 0); + uint256 feeGrowthGlobalCalc = swapAmount.mulWadDown(0.003e18).mulDivDown(FixedPoint128.Q128, liquidity); + assertEq(feeGrowthGlobal1, feeGrowthGlobalCalc); + } + + function test_getLiquidity() public { + modifyLiquidityRouter.modifyLiquidity(key, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), ZERO_BYTES); + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0), ZERO_BYTES + ); + + uint128 liquidity = StateLibrary.getLiquidity(manager, poolId); + snapLastCall("extsload getLiquidity"); + assertEq(liquidity, 20 ether); + } + + function test_fuzz_getLiquidity(IPoolManager.ModifyLiquidityParams memory params) public { + (IPoolManager.ModifyLiquidityParams memory _params,) = + Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, params, ZERO_BYTES); + (, int24 tick,,) = StateLibrary.getSlot0(manager, poolId); + uint128 liquidity = StateLibrary.getLiquidity(manager, poolId); + + // out of range liquidity is not added to Pool.State.liquidity + if (tick < _params.tickLower || tick >= _params.tickUpper) { + assertEq(liquidity, 0); + } else { + assertEq(liquidity, uint128(uint256(_params.liquidityDelta))); + } + } + + function test_getTickBitmap() public { + int24 tickLower = -300; + int24 tickUpper = 300; + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(tickLower, tickUpper, 10_000 ether, 0), ZERO_BYTES + ); + + (int16 wordPos, uint8 bitPos) = TickBitmap.position(tickLower / key.tickSpacing); + uint256 tickBitmap = StateLibrary.getTickBitmap(manager, poolId, wordPos); + snapLastCall("extsload getTickBitmap"); + assertNotEq(tickBitmap, 0); + assertEq(tickBitmap, 1 << bitPos); + + (wordPos, bitPos) = TickBitmap.position(tickUpper / key.tickSpacing); + tickBitmap = StateLibrary.getTickBitmap(manager, poolId, wordPos); + assertNotEq(tickBitmap, 0); + assertEq(tickBitmap, 1 << bitPos); + } + + function test_fuzz_getTickBitmap(IPoolManager.ModifyLiquidityParams memory params) public { + (IPoolManager.ModifyLiquidityParams memory _params,) = + Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, params, ZERO_BYTES); + + (int16 wordPos, uint8 bitPos) = TickBitmap.position(_params.tickLower / key.tickSpacing); + (int16 wordPosUpper, uint8 bitPosUpper) = TickBitmap.position(_params.tickUpper / key.tickSpacing); + + uint256 tickBitmap = StateLibrary.getTickBitmap(manager, poolId, wordPos); + assertNotEq(tickBitmap, 0); + + // in fuzz tests, the tickLower and tickUpper might exist on the same word + if (wordPos == wordPosUpper) { + assertEq(tickBitmap, (1 << bitPos) | (1 << bitPosUpper)); + } else { + assertEq(tickBitmap, 1 << bitPos); + } + } + + function test_getPositionInfo() public { + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, 10_000 ether, 0), ZERO_BYTES + ); + + // swap to create fees, crossing a tick + uint256 swapAmount = 10 ether; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + (, int24 currentTick,,) = StateLibrary.getSlot0(manager, poolId); + assertNotEq(currentTick, -139); + + // poke the LP so that fees are updated + modifyLiquidityRouter.modifyLiquidity(key, IPoolManager.ModifyLiquidityParams(-60, 60, 0, 0), ZERO_BYTES); + + bytes32 positionId = + keccak256(abi.encodePacked(address(modifyLiquidityRouter), int24(-60), int24(60), bytes32(0))); + + (uint128 liquidity, uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + StateLibrary.getPositionInfo(manager, poolId, positionId); + snapLastCall("extsload getPositionInfo"); + + assertEq(liquidity, 10_000 ether); + + assertNotEq(feeGrowthInside0X128, 0); + assertEq(feeGrowthInside1X128, 0); + } + + function test_fuzz_getPositionInfo( + IPoolManager.ModifyLiquidityParams memory params, + uint256 swapAmount, + bool zeroForOne + ) public { + (IPoolManager.ModifyLiquidityParams memory _params, BalanceDelta delta) = + createFuzzyLiquidity(modifyLiquidityRouter, key, params, ZERO_BYTES); + + uint256 delta0 = uint256(int256(-delta.amount0())); + uint256 delta1 = uint256(int256(-delta.amount1())); + // if one of the deltas is zero, ensure to swap in the right direction + if (delta0 == 0) { + zeroForOne = true; + } else if (delta1 == 0) { + zeroForOne = false; + } + swapAmount = bound(swapAmount, 1, uint256(int256(type(int128).max))); + swap(key, zeroForOne, -int256(swapAmount), ZERO_BYTES); + + // poke the LP so that fees are updated + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(_params.tickLower, _params.tickUpper, 0, 0), ZERO_BYTES + ); + + bytes32 positionId = keccak256( + abi.encodePacked(address(modifyLiquidityRouter), _params.tickLower, _params.tickUpper, bytes32(0)) + ); + + (uint128 liquidity, uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + StateLibrary.getPositionInfo(manager, poolId, positionId); + + assertEq(liquidity, uint128(uint256(_params.liquidityDelta))); + if (zeroForOne) { + assertNotEq(feeGrowthInside0X128, 0); + assertEq(feeGrowthInside1X128, 0); + } else { + assertEq(feeGrowthInside0X128, 0); + assertNotEq(feeGrowthInside1X128, 0); + } + } + + function test_getTickFeeGrowthOutside() public { + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, 10_000 ether, 0), ZERO_BYTES + ); + + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-600, 600, 10_000 ether, 0), ZERO_BYTES + ); + + // swap to create fees, crossing a tick + uint256 swapAmount = 100 ether; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + (, int24 currentTick,,) = StateLibrary.getSlot0(manager, poolId); + assertEq(currentTick, -139); + + int24 tick = -60; + (uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128) = + StateLibrary.getTickFeeGrowthOutside(manager, poolId, tick); + snapLastCall("extsload getTickFeeGrowthOutside"); + + // magic number verified against a native getter on PoolManager + assertEq(feeGrowthOutside0X128, 3076214778951936192155253373200636); + assertEq(feeGrowthOutside1X128, 0); + + tick = 60; + (feeGrowthOutside0X128, feeGrowthOutside1X128) = StateLibrary.getTickFeeGrowthOutside(manager, poolId, tick); + assertEq(feeGrowthOutside0X128, 0); + assertEq(feeGrowthOutside1X128, 0); + } + + // also hard to fuzz because of feeGrowthOutside + function test_getTickInfo() public { + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, 10_000 ether, 0), ZERO_BYTES + ); + + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-600, 600, 10_000 ether, 0), ZERO_BYTES + ); + + // swap to create fees, crossing a tick + uint256 swapAmount = 100 ether; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + (, int24 currentTick,,) = StateLibrary.getSlot0(manager, poolId); + assertEq(currentTick, -139); + + int24 tick = -60; + (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128) = + StateLibrary.getTickInfo(manager, poolId, tick); + snapLastCall("extsload getTickInfo"); + + (uint128 liquidityGross_, int128 liquidityNet_) = StateLibrary.getTickLiquidity(manager, poolId, tick); + (uint256 feeGrowthOutside0X128_, uint256 feeGrowthOutside1X128_) = + StateLibrary.getTickFeeGrowthOutside(manager, poolId, tick); + + assertEq(liquidityGross, 10_000 ether); + assertEq(liquidityGross, liquidityGross_); + assertEq(liquidityNet, liquidityNet_); + + assertNotEq(feeGrowthOutside0X128, 0); + assertEq(feeGrowthOutside1X128, 0); + assertEq(feeGrowthOutside0X128, feeGrowthOutside0X128_); + assertEq(feeGrowthOutside1X128, feeGrowthOutside1X128_); + } + + function test_getFeeGrowthInside() public { + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, 10_000 ether, 0), ZERO_BYTES + ); + + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-600, 600, 10_000 ether, 0), ZERO_BYTES + ); + + // swap to create fees, crossing a tick + uint256 swapAmount = 100 ether; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + (, int24 currentTick,,) = StateLibrary.getSlot0(manager, poolId); + assertEq(currentTick, -139); + + // calculated live + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + StateLibrary.getFeeGrowthInside(manager, poolId, -60, 60); + snapLastCall("extsload getFeeGrowthInside"); + + // poke the LP so that fees are updated + modifyLiquidityRouter.modifyLiquidity(key, IPoolManager.ModifyLiquidityParams(-60, 60, 0, 0), ZERO_BYTES); + + bytes32 positionId = + keccak256(abi.encodePacked(address(modifyLiquidityRouter), int24(-60), int24(60), bytes32(0))); + + (, uint256 feeGrowthInside0X128_, uint256 feeGrowthInside1X128_) = + StateLibrary.getPositionInfo(manager, poolId, positionId); + + assertNotEq(feeGrowthInside0X128, 0); + assertEq(feeGrowthInside0X128, feeGrowthInside0X128_); + assertEq(feeGrowthInside1X128, feeGrowthInside1X128_); + } + + function test_fuzz_getFeeGrowthInside(IPoolManager.ModifyLiquidityParams memory params, bool zeroForOne) public { + modifyLiquidityRouter.modifyLiquidity( + key, + IPoolManager.ModifyLiquidityParams( + TickMath.minUsableTick(key.tickSpacing), TickMath.maxUsableTick(key.tickSpacing), 10_000 ether, 0 + ), + ZERO_BYTES + ); + + (IPoolManager.ModifyLiquidityParams memory _params,) = + createFuzzyLiquidity(modifyLiquidityRouter, key, params, ZERO_BYTES); + + swap(key, zeroForOne, -int256(100e18), ZERO_BYTES); + + // calculated live + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + StateLibrary.getFeeGrowthInside(manager, poolId, _params.tickLower, _params.tickUpper); + + // poke the LP so that fees are updated + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(_params.tickLower, _params.tickUpper, 0, 0), ZERO_BYTES + ); + bytes32 positionId = keccak256( + abi.encodePacked(address(modifyLiquidityRouter), _params.tickLower, _params.tickUpper, bytes32(0)) + ); + + (, uint256 feeGrowthInside0X128_, uint256 feeGrowthInside1X128_) = + StateLibrary.getPositionInfo(manager, poolId, positionId); + + assertEq(feeGrowthInside0X128, feeGrowthInside0X128_); + assertEq(feeGrowthInside1X128, feeGrowthInside1X128_); + } + + function test_getPositionLiquidity() public { + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, 10_000 ether, 0), ZERO_BYTES + ); + + bytes32 positionId = + keccak256(abi.encodePacked(address(modifyLiquidityRouter), int24(-60), int24(60), bytes32(0))); + + uint128 liquidity = StateLibrary.getPositionLiquidity(manager, poolId, positionId); + snapLastCall("extsload getPositionLiquidity"); + + assertEq(liquidity, 10_000 ether); + } + + function test_fuzz_getPositionLiquidity( + 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); + + vm.assume(_paramsA.tickLower != _paramsB.tickLower && _paramsA.tickUpper != _paramsB.tickUpper); + + bytes32 positionIdA = keccak256( + abi.encodePacked(address(modifyLiquidityRouter), _paramsA.tickLower, _paramsA.tickUpper, bytes32(0)) + ); + uint128 liquidityA = StateLibrary.getPositionLiquidity(manager, poolId, positionIdA); + assertEq(liquidityA, uint128(uint256(_paramsA.liquidityDelta))); + + bytes32 positionIdB = keccak256( + abi.encodePacked(address(modifyLiquidityRouter), _paramsB.tickLower, _paramsB.tickUpper, bytes32(0)) + ); + uint128 liquidityB = StateLibrary.getPositionLiquidity(manager, poolId, positionIdB); + assertEq(liquidityB, uint128(uint256(_paramsB.liquidityDelta))); + } +} diff --git a/test/utils/AmountHelpers.sol b/test/utils/AmountHelpers.sol index 6e4f6d761..371374ff6 100644 --- a/test/utils/AmountHelpers.sol +++ b/test/utils/AmountHelpers.sol @@ -6,6 +6,7 @@ import {IPoolManager} from "../../src/interfaces/IPoolManager.sol"; import {PoolId, PoolIdLibrary} from "../../src/types/PoolId.sol"; import {TickMath} from "../../src/libraries/TickMath.sol"; import {PoolKey} from "../../src/types/PoolKey.sol"; +import {StateLibrary} from "../../src/libraries/StateLibrary.sol"; /// @title Calculate token<>liquidity /// @notice Helps calculate amounts for bounding fuzz tests @@ -16,8 +17,8 @@ library AmountHelpers { PoolKey memory key ) public view returns (uint256 amount0, uint256 amount1) { PoolId id = PoolIdLibrary.toId(key); - uint128 liquidity = manager.getLiquidity(id); - (uint160 sqrtPriceX96,,,) = manager.getSlot0(id); + uint128 liquidity = StateLibrary.getLiquidity(manager, id); + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, id); uint160 sqrtPriceX96Lower = TickMath.getSqrtPriceAtTick(params.tickLower); uint160 sqrtPriceX96Upper = TickMath.getSqrtPriceAtTick(params.tickUpper);