From 47315dc9267359e08170179be2ac740ec3f72f1c Mon Sep 17 00:00:00 2001 From: chef-burger <137024020+chefburger@users.noreply.github.com> Date: Mon, 27 May 2024 14:16:31 +0800 Subject: [PATCH] [PART-1] enable hooks to return deltas to influence pool behavior (#44) * feat: enable hooks to return deltas to influence pool behavior * test: fix & add test cases hook returns delta * [PART-2] support same owner multiple positions (#46) * feat: add salt to distinguish positions for same owner in cl-pool * feat: add salt to distinguish positions for same owner in bin-pool * optimiazaiton: small tweaks per comments * test: added salt != 0 cases * [PART-3] allow `beforeSwap` to return temp lpFee & delta for both sides (#47) * feat: finish updating for cl-pool * feat: finish updating for bin-pool * chore: correct comments removed unnecessary isValid function --- .../BinHookTest#testBurnSucceedsWithHook.snap | 2 +- ...inHookTest#testDonateSucceedsWithHook.snap | 2 +- ...okTest#testInitializeSucceedsWithHook.snap | 2 +- .../BinHookTest#testMintSucceedsWithHook.snap | 2 +- .../BinHookTest#testSwapSucceedsWithHook.snap | 2 +- ...oolManagerTest#testBurnNativeCurrency.snap | 2 +- ...anagerTest#testFuzzUpdateDynamicLPFee.snap | 2 +- ...oolManagerTest#testFuzz_SetMaxBinStep.snap | 2 +- ...BinPoolManagerTest#testGasBurnHalfBin.snap | 2 +- ...inPoolManagerTest#testGasBurnNineBins.snap | 2 +- .../BinPoolManagerTest#testGasBurnOneBin.snap | 2 +- .../BinPoolManagerTest#testGasDonate.snap | 2 +- ...nPoolManagerTest#testGasMintNneBins-1.snap | 2 +- ...nPoolManagerTest#testGasMintNneBins-2.snap | 2 +- ...inPoolManagerTest#testGasMintOneBin-1.snap | 2 +- ...inPoolManagerTest#testGasMintOneBin-2.snap | 2 +- ...olManagerTest#testGasSwapMultipleBins.snap | 2 +- ...nagerTest#testGasSwapOverBigBinIdGate.snap | 2 +- ...nPoolManagerTest#testGasSwapSingleBin.snap | 2 +- ...oolManagerTest#testMintNativeCurrency.snap | 2 +- .../BinPoolManagerTest#testNoOpGas_Burn.snap | 1 - ...BinPoolManagerTest#testNoOpGas_Donate.snap | 1 - ...oolManagerTest#testNoOpGas_Initialize.snap | 1 - .../BinPoolManagerTest#testNoOpGas_Mint.snap | 1 - .../BinPoolManagerTest#testNoOpGas_Swap.snap | 1 - ...BinPoolManagerTest#testSetProtocolFee.snap | 2 +- ...oolManagerTest#addLiquidity_fromEmpty.snap | 2 +- ...ManagerTest#addLiquidity_fromNonEmpty.snap | 2 +- ...lManagerTest#addLiquidity_nativeToken.snap | 2 +- .../CLPoolManagerTest#donateBothTokens.snap | 2 +- .../CLPoolManagerTest#gasDonateOneToken.snap | 2 +- ...oolManagerTest#initializeWithoutHooks.snap | 2 +- ...anagerTest#removeLiquidity_toNonEmpty.snap | 2 +- ...PoolManagerTest#swap_againstLiquidity.snap | 2 +- ...gerTest#swap_leaveSurplusTokenInVault.snap | 2 +- ...oolManagerTest#swap_runOutOfLiquidity.snap | 2 +- .../CLPoolManagerTest#swap_simple.snap | 2 +- ...nagerTest#swap_useSurplusTokenAsInput.snap | 2 +- .../CLPoolManagerTest#swap_withHooks.snap | 2 +- .../CLPoolManagerTest#swap_withNative.snap | 2 +- ...anagerTest#testFuzzUpdateDynamicLPFee.snap | 2 +- ...CLPoolManagerTest#testNoOp_gas_Donate.snap | 1 - ...olManagerTest#testNoOp_gas_Initialize.snap | 1 - ...nagerTest#testNoOp_gas_ModifyPosition.snap | 1 - .../CLPoolManagerTest#testNoOp_gas_Swap.snap | 1 - .forge-snapshots/ExtsloadTest#extsload.snap | 2 +- src/libraries/Hooks.sol | 20 +- src/libraries/LPFeeLibrary.sol | 28 +- src/pool-bin/BinPoolManager.sol | 118 ++-- src/pool-bin/interfaces/IBinHooks.sol | 22 +- src/pool-bin/interfaces/IBinPoolManager.sol | 15 +- src/pool-bin/libraries/BinHooks.sol | 174 +++++ src/pool-bin/libraries/BinPool.sol | 53 +- src/pool-bin/libraries/BinPosition.sol | 9 +- src/pool-cl/CLPoolManager.sol | 113 ++-- src/pool-cl/interfaces/ICLHooks.sol | 22 +- src/pool-cl/interfaces/ICLPoolManager.sol | 22 +- src/pool-cl/libraries/CLHooks.sol | 158 +++++ src/pool-cl/libraries/CLPool.sol | 103 +-- src/pool-cl/libraries/CLPosition.sol | 9 +- src/test/pool-bin/MockBinHooks.sol | 23 +- src/types/BalanceDelta.sol | 9 +- src/types/BeforeSwapDelta.sol | 40 ++ test/libraries/Hooks/Hooks.t.sol | 17 - test/libraries/LPFeeLibrary.t.sol | 40 +- test/pool-bin/BinHook.t.sol | 6 +- test/pool-bin/BinHookReturnsDelta.t.sol | 280 ++++++++ test/pool-bin/BinHookReturnsFee.t.sol | 199 ++++++ test/pool-bin/BinPoolManager.t.sol | 320 +++++++--- test/pool-bin/helpers/BaseBinTestHook.sol | 37 +- .../helpers/BinDynamicReturnsFeeHook.sol | 58 ++ test/pool-bin/helpers/BinFeeManagerHook.sol | 5 +- test/pool-bin/helpers/BinLiquidityHelper.sol | 9 - test/pool-bin/helpers/BinNoOpTestHook.sol | 57 -- test/pool-bin/helpers/BinReturnsDeltaHook.sol | 167 +++++ test/pool-bin/helpers/BinSkipCallbackHook.sol | 28 +- test/pool-bin/helpers/BinSwapHelper.sol | 9 - test/pool-bin/helpers/BinTestHelper.sol | 75 ++- test/pool-bin/libraries/BinPoolDonate.t.sol | 6 +- test/pool-bin/libraries/BinPoolFee.t.sol | 11 +- .../pool-bin/libraries/BinPoolLiquidity.t.sol | 42 +- test/pool-bin/libraries/BinPoolSwap.t.sol | 4 +- test/pool-bin/libraries/BinPosition.t.sol | 28 +- test/pool-cl/CLFees.t.sol | 8 +- test/pool-cl/CLHookReturnsDelta.t.sol | 430 +++++++++++++ test/pool-cl/CLHookReturnsFee.sol | 230 +++++++ test/pool-cl/CLHookSkipCallback.t.sol | 32 +- test/pool-cl/CLPoolManager.t.sol | 599 +++++++++++++----- test/pool-cl/helpers/BaseCLTestHook.sol | 33 +- .../helpers/CLDynamicReturnsFeeHook.sol | 58 ++ test/pool-cl/helpers/CLFeeManagerHook.sol | 5 +- test/pool-cl/helpers/CLNoOpTestHook.sol | 56 -- test/pool-cl/helpers/CLPoolManagerRouter.sol | 49 +- test/pool-cl/helpers/CLReturnsDeltaHook.sol | 161 +++++ test/pool-cl/helpers/CLSkipCallbackHook.sol | 25 +- test/pool-cl/helpers/MockHooks.sol | 33 +- test/pool-cl/libraries/CLPool.t.sol | 43 +- test/pool-cl/libraries/CLPoolSwapFee.t.sol | 6 +- test/pool-cl/libraries/CLPosition.t.sol | 17 +- 99 files changed, 3327 insertions(+), 877 deletions(-) delete mode 100644 .forge-snapshots/BinPoolManagerTest#testNoOpGas_Burn.snap delete mode 100644 .forge-snapshots/BinPoolManagerTest#testNoOpGas_Donate.snap delete mode 100644 .forge-snapshots/BinPoolManagerTest#testNoOpGas_Initialize.snap delete mode 100644 .forge-snapshots/BinPoolManagerTest#testNoOpGas_Mint.snap delete mode 100644 .forge-snapshots/BinPoolManagerTest#testNoOpGas_Swap.snap delete mode 100644 .forge-snapshots/CLPoolManagerTest#testNoOp_gas_Donate.snap delete mode 100644 .forge-snapshots/CLPoolManagerTest#testNoOp_gas_Initialize.snap delete mode 100644 .forge-snapshots/CLPoolManagerTest#testNoOp_gas_ModifyPosition.snap delete mode 100644 .forge-snapshots/CLPoolManagerTest#testNoOp_gas_Swap.snap create mode 100644 src/pool-bin/libraries/BinHooks.sol create mode 100644 src/pool-cl/libraries/CLHooks.sol create mode 100644 src/types/BeforeSwapDelta.sol create mode 100644 test/pool-bin/BinHookReturnsDelta.t.sol create mode 100644 test/pool-bin/BinHookReturnsFee.t.sol create mode 100644 test/pool-bin/helpers/BinDynamicReturnsFeeHook.sol delete mode 100644 test/pool-bin/helpers/BinNoOpTestHook.sol create mode 100644 test/pool-bin/helpers/BinReturnsDeltaHook.sol create mode 100644 test/pool-cl/CLHookReturnsDelta.t.sol create mode 100644 test/pool-cl/CLHookReturnsFee.sol create mode 100644 test/pool-cl/helpers/CLDynamicReturnsFeeHook.sol delete mode 100644 test/pool-cl/helpers/CLNoOpTestHook.sol create mode 100644 test/pool-cl/helpers/CLReturnsDeltaHook.sol diff --git a/.forge-snapshots/BinHookTest#testBurnSucceedsWithHook.snap b/.forge-snapshots/BinHookTest#testBurnSucceedsWithHook.snap index 5ef512ea..80dbc371 100644 --- a/.forge-snapshots/BinHookTest#testBurnSucceedsWithHook.snap +++ b/.forge-snapshots/BinHookTest#testBurnSucceedsWithHook.snap @@ -1 +1 @@ -181124 \ No newline at end of file +182049 \ No newline at end of file diff --git a/.forge-snapshots/BinHookTest#testDonateSucceedsWithHook.snap b/.forge-snapshots/BinHookTest#testDonateSucceedsWithHook.snap index f9e66567..698a7020 100644 --- a/.forge-snapshots/BinHookTest#testDonateSucceedsWithHook.snap +++ b/.forge-snapshots/BinHookTest#testDonateSucceedsWithHook.snap @@ -1 +1 @@ -185453 \ No newline at end of file +185296 \ No newline at end of file diff --git a/.forge-snapshots/BinHookTest#testInitializeSucceedsWithHook.snap b/.forge-snapshots/BinHookTest#testInitializeSucceedsWithHook.snap index bfcb7cad..eb0299a4 100644 --- a/.forge-snapshots/BinHookTest#testInitializeSucceedsWithHook.snap +++ b/.forge-snapshots/BinHookTest#testInitializeSucceedsWithHook.snap @@ -1 +1 @@ -137037 \ No newline at end of file +137576 \ No newline at end of file diff --git a/.forge-snapshots/BinHookTest#testMintSucceedsWithHook.snap b/.forge-snapshots/BinHookTest#testMintSucceedsWithHook.snap index 22fed2fe..3be97679 100644 --- a/.forge-snapshots/BinHookTest#testMintSucceedsWithHook.snap +++ b/.forge-snapshots/BinHookTest#testMintSucceedsWithHook.snap @@ -1 +1 @@ -329719 \ No newline at end of file +331250 \ No newline at end of file diff --git a/.forge-snapshots/BinHookTest#testSwapSucceedsWithHook.snap b/.forge-snapshots/BinHookTest#testSwapSucceedsWithHook.snap index fa139b82..ce686ff3 100644 --- a/.forge-snapshots/BinHookTest#testSwapSucceedsWithHook.snap +++ b/.forge-snapshots/BinHookTest#testSwapSucceedsWithHook.snap @@ -1 +1 @@ -192675 \ No newline at end of file +193836 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testBurnNativeCurrency.snap b/.forge-snapshots/BinPoolManagerTest#testBurnNativeCurrency.snap index 7e12705b..0abd0efc 100644 --- a/.forge-snapshots/BinPoolManagerTest#testBurnNativeCurrency.snap +++ b/.forge-snapshots/BinPoolManagerTest#testBurnNativeCurrency.snap @@ -1 +1 @@ -137857 \ No newline at end of file +138533 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testFuzzUpdateDynamicLPFee.snap b/.forge-snapshots/BinPoolManagerTest#testFuzzUpdateDynamicLPFee.snap index 3d7fbe59..1bfb0a9d 100644 --- a/.forge-snapshots/BinPoolManagerTest#testFuzzUpdateDynamicLPFee.snap +++ b/.forge-snapshots/BinPoolManagerTest#testFuzzUpdateDynamicLPFee.snap @@ -1 +1 @@ -32585 \ No newline at end of file +32563 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testFuzz_SetMaxBinStep.snap b/.forge-snapshots/BinPoolManagerTest#testFuzz_SetMaxBinStep.snap index 13505f4e..960d5d6f 100644 --- a/.forge-snapshots/BinPoolManagerTest#testFuzz_SetMaxBinStep.snap +++ b/.forge-snapshots/BinPoolManagerTest#testFuzz_SetMaxBinStep.snap @@ -1 +1 @@ -30350 \ No newline at end of file +30417 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testGasBurnHalfBin.snap b/.forge-snapshots/BinPoolManagerTest#testGasBurnHalfBin.snap index 9fe16797..31e735f5 100644 --- a/.forge-snapshots/BinPoolManagerTest#testGasBurnHalfBin.snap +++ b/.forge-snapshots/BinPoolManagerTest#testGasBurnHalfBin.snap @@ -1 +1 @@ -147796 \ No newline at end of file +148635 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testGasBurnNineBins.snap b/.forge-snapshots/BinPoolManagerTest#testGasBurnNineBins.snap index c4928bcd..049e9b27 100644 --- a/.forge-snapshots/BinPoolManagerTest#testGasBurnNineBins.snap +++ b/.forge-snapshots/BinPoolManagerTest#testGasBurnNineBins.snap @@ -1 +1 @@ -295502 \ No newline at end of file +296486 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testGasBurnOneBin.snap b/.forge-snapshots/BinPoolManagerTest#testGasBurnOneBin.snap index abf05eff..9d5f9687 100644 --- a/.forge-snapshots/BinPoolManagerTest#testGasBurnOneBin.snap +++ b/.forge-snapshots/BinPoolManagerTest#testGasBurnOneBin.snap @@ -1 +1 @@ -131067 \ No newline at end of file +131743 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testGasDonate.snap b/.forge-snapshots/BinPoolManagerTest#testGasDonate.snap index 7af64af3..57ffbe7e 100644 --- a/.forge-snapshots/BinPoolManagerTest#testGasDonate.snap +++ b/.forge-snapshots/BinPoolManagerTest#testGasDonate.snap @@ -1 +1 @@ -120152 \ No newline at end of file +120042 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testGasMintNneBins-1.snap b/.forge-snapshots/BinPoolManagerTest#testGasMintNneBins-1.snap index c9ffff75..d38e29c8 100644 --- a/.forge-snapshots/BinPoolManagerTest#testGasMintNneBins-1.snap +++ b/.forge-snapshots/BinPoolManagerTest#testGasMintNneBins-1.snap @@ -1 +1 @@ -974980 \ No newline at end of file +977200 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testGasMintNneBins-2.snap b/.forge-snapshots/BinPoolManagerTest#testGasMintNneBins-2.snap index 9e47d5fa..a2070f30 100644 --- a/.forge-snapshots/BinPoolManagerTest#testGasMintNneBins-2.snap +++ b/.forge-snapshots/BinPoolManagerTest#testGasMintNneBins-2.snap @@ -1 +1 @@ -337174 \ No newline at end of file +339390 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testGasMintOneBin-1.snap b/.forge-snapshots/BinPoolManagerTest#testGasMintOneBin-1.snap index 4ccc0448..6d889ce4 100644 --- a/.forge-snapshots/BinPoolManagerTest#testGasMintOneBin-1.snap +++ b/.forge-snapshots/BinPoolManagerTest#testGasMintOneBin-1.snap @@ -1 +1 @@ -341313 \ No newline at end of file +342867 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testGasMintOneBin-2.snap b/.forge-snapshots/BinPoolManagerTest#testGasMintOneBin-2.snap index 107760a9..0807b6ef 100644 --- a/.forge-snapshots/BinPoolManagerTest#testGasMintOneBin-2.snap +++ b/.forge-snapshots/BinPoolManagerTest#testGasMintOneBin-2.snap @@ -1 +1 @@ -144254 \ No newline at end of file +145810 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testGasSwapMultipleBins.snap b/.forge-snapshots/BinPoolManagerTest#testGasSwapMultipleBins.snap index d4ad3327..c80f85ba 100644 --- a/.forge-snapshots/BinPoolManagerTest#testGasSwapMultipleBins.snap +++ b/.forge-snapshots/BinPoolManagerTest#testGasSwapMultipleBins.snap @@ -1 +1 @@ -179942 \ No newline at end of file +180562 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testGasSwapOverBigBinIdGate.snap b/.forge-snapshots/BinPoolManagerTest#testGasSwapOverBigBinIdGate.snap index 44dbdbb6..138ac9d4 100644 --- a/.forge-snapshots/BinPoolManagerTest#testGasSwapOverBigBinIdGate.snap +++ b/.forge-snapshots/BinPoolManagerTest#testGasSwapOverBigBinIdGate.snap @@ -1 +1 @@ -185927 \ No newline at end of file +186545 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testGasSwapSingleBin.snap b/.forge-snapshots/BinPoolManagerTest#testGasSwapSingleBin.snap index 0e6e787f..a6455c0d 100644 --- a/.forge-snapshots/BinPoolManagerTest#testGasSwapSingleBin.snap +++ b/.forge-snapshots/BinPoolManagerTest#testGasSwapSingleBin.snap @@ -1 +1 @@ -138369 \ No newline at end of file +138925 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testMintNativeCurrency.snap b/.forge-snapshots/BinPoolManagerTest#testMintNativeCurrency.snap index e48740b1..6ead8637 100644 --- a/.forge-snapshots/BinPoolManagerTest#testMintNativeCurrency.snap +++ b/.forge-snapshots/BinPoolManagerTest#testMintNativeCurrency.snap @@ -1 +1 @@ -307829 \ No newline at end of file +309383 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Burn.snap b/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Burn.snap deleted file mode 100644 index 671d159f..00000000 --- a/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Burn.snap +++ /dev/null @@ -1 +0,0 @@ -76684 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Donate.snap b/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Donate.snap deleted file mode 100644 index 1b81150e..00000000 --- a/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Donate.snap +++ /dev/null @@ -1 +0,0 @@ -54199 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Initialize.snap b/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Initialize.snap deleted file mode 100644 index 83c8f740..00000000 --- a/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Initialize.snap +++ /dev/null @@ -1 +0,0 @@ -64662 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Mint.snap b/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Mint.snap deleted file mode 100644 index 82ed0c47..00000000 --- a/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Mint.snap +++ /dev/null @@ -1 +0,0 @@ -69699 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Swap.snap b/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Swap.snap deleted file mode 100644 index fc5969b3..00000000 --- a/.forge-snapshots/BinPoolManagerTest#testNoOpGas_Swap.snap +++ /dev/null @@ -1 +0,0 @@ -57628 \ No newline at end of file diff --git a/.forge-snapshots/BinPoolManagerTest#testSetProtocolFee.snap b/.forge-snapshots/BinPoolManagerTest#testSetProtocolFee.snap index 12ede85c..2397c98f 100644 --- a/.forge-snapshots/BinPoolManagerTest#testSetProtocolFee.snap +++ b/.forge-snapshots/BinPoolManagerTest#testSetProtocolFee.snap @@ -1 +1 @@ -34422 \ No newline at end of file +34487 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#addLiquidity_fromEmpty.snap b/.forge-snapshots/CLPoolManagerTest#addLiquidity_fromEmpty.snap index 124eb0c9..cf54c9af 100644 --- a/.forge-snapshots/CLPoolManagerTest#addLiquidity_fromEmpty.snap +++ b/.forge-snapshots/CLPoolManagerTest#addLiquidity_fromEmpty.snap @@ -1 +1 @@ -362283 \ No newline at end of file +362972 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#addLiquidity_fromNonEmpty.snap b/.forge-snapshots/CLPoolManagerTest#addLiquidity_fromNonEmpty.snap index 45b0d2de..7fe27b8f 100644 --- a/.forge-snapshots/CLPoolManagerTest#addLiquidity_fromNonEmpty.snap +++ b/.forge-snapshots/CLPoolManagerTest#addLiquidity_fromNonEmpty.snap @@ -1 +1 @@ -177451 \ No newline at end of file +178140 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#addLiquidity_nativeToken.snap b/.forge-snapshots/CLPoolManagerTest#addLiquidity_nativeToken.snap index 63b9dcef..bd641552 100644 --- a/.forge-snapshots/CLPoolManagerTest#addLiquidity_nativeToken.snap +++ b/.forge-snapshots/CLPoolManagerTest#addLiquidity_nativeToken.snap @@ -1 +1 @@ -247570 \ No newline at end of file +248255 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#donateBothTokens.snap b/.forge-snapshots/CLPoolManagerTest#donateBothTokens.snap index eecc1969..fc3ed6be 100644 --- a/.forge-snapshots/CLPoolManagerTest#donateBothTokens.snap +++ b/.forge-snapshots/CLPoolManagerTest#donateBothTokens.snap @@ -1 +1 @@ -170536 \ No newline at end of file +170425 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#gasDonateOneToken.snap b/.forge-snapshots/CLPoolManagerTest#gasDonateOneToken.snap index 22b77a6a..8d4eef07 100644 --- a/.forge-snapshots/CLPoolManagerTest#gasDonateOneToken.snap +++ b/.forge-snapshots/CLPoolManagerTest#gasDonateOneToken.snap @@ -1 +1 @@ -114459 \ No newline at end of file +114348 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#initializeWithoutHooks.snap b/.forge-snapshots/CLPoolManagerTest#initializeWithoutHooks.snap index a047ff13..0b05c93e 100644 --- a/.forge-snapshots/CLPoolManagerTest#initializeWithoutHooks.snap +++ b/.forge-snapshots/CLPoolManagerTest#initializeWithoutHooks.snap @@ -1 +1 @@ -59393 \ No newline at end of file +59778 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#removeLiquidity_toNonEmpty.snap b/.forge-snapshots/CLPoolManagerTest#removeLiquidity_toNonEmpty.snap index a0265045..8342b66c 100644 --- a/.forge-snapshots/CLPoolManagerTest#removeLiquidity_toNonEmpty.snap +++ b/.forge-snapshots/CLPoolManagerTest#removeLiquidity_toNonEmpty.snap @@ -1 +1 @@ -123811 \ No newline at end of file +124482 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#swap_againstLiquidity.snap b/.forge-snapshots/CLPoolManagerTest#swap_againstLiquidity.snap index f32b70fe..df785b22 100644 --- a/.forge-snapshots/CLPoolManagerTest#swap_againstLiquidity.snap +++ b/.forge-snapshots/CLPoolManagerTest#swap_againstLiquidity.snap @@ -1 +1 @@ -141063 \ No newline at end of file +141710 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#swap_leaveSurplusTokenInVault.snap b/.forge-snapshots/CLPoolManagerTest#swap_leaveSurplusTokenInVault.snap index feda8ffb..eeb66ea7 100644 --- a/.forge-snapshots/CLPoolManagerTest#swap_leaveSurplusTokenInVault.snap +++ b/.forge-snapshots/CLPoolManagerTest#swap_leaveSurplusTokenInVault.snap @@ -1 +1 @@ -174740 \ No newline at end of file +175389 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#swap_runOutOfLiquidity.snap b/.forge-snapshots/CLPoolManagerTest#swap_runOutOfLiquidity.snap index c77b1c8c..3b120af9 100644 --- a/.forge-snapshots/CLPoolManagerTest#swap_runOutOfLiquidity.snap +++ b/.forge-snapshots/CLPoolManagerTest#swap_runOutOfLiquidity.snap @@ -1 +1 @@ -25078958 \ No newline at end of file +25094185 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#swap_simple.snap b/.forge-snapshots/CLPoolManagerTest#swap_simple.snap index 3f5ba15e..8d89a417 100644 --- a/.forge-snapshots/CLPoolManagerTest#swap_simple.snap +++ b/.forge-snapshots/CLPoolManagerTest#swap_simple.snap @@ -1 +1 @@ -78932 \ No newline at end of file +79576 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#swap_useSurplusTokenAsInput.snap b/.forge-snapshots/CLPoolManagerTest#swap_useSurplusTokenAsInput.snap index c244d7d5..3bb330bc 100644 --- a/.forge-snapshots/CLPoolManagerTest#swap_useSurplusTokenAsInput.snap +++ b/.forge-snapshots/CLPoolManagerTest#swap_useSurplusTokenAsInput.snap @@ -1 +1 @@ -153277 \ No newline at end of file +153925 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#swap_withHooks.snap b/.forge-snapshots/CLPoolManagerTest#swap_withHooks.snap index ad48c5d8..2d773734 100644 --- a/.forge-snapshots/CLPoolManagerTest#swap_withHooks.snap +++ b/.forge-snapshots/CLPoolManagerTest#swap_withHooks.snap @@ -1 +1 @@ -95439 \ No newline at end of file +96383 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#swap_withNative.snap b/.forge-snapshots/CLPoolManagerTest#swap_withNative.snap index 947950d6..800f4840 100644 --- a/.forge-snapshots/CLPoolManagerTest#swap_withNative.snap +++ b/.forge-snapshots/CLPoolManagerTest#swap_withNative.snap @@ -1 +1 @@ -78935 \ No newline at end of file +79579 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#testFuzzUpdateDynamicLPFee.snap b/.forge-snapshots/CLPoolManagerTest#testFuzzUpdateDynamicLPFee.snap index eccf7bcd..e9b48a35 100644 --- a/.forge-snapshots/CLPoolManagerTest#testFuzzUpdateDynamicLPFee.snap +++ b/.forge-snapshots/CLPoolManagerTest#testFuzzUpdateDynamicLPFee.snap @@ -1 +1 @@ -32281 \ No newline at end of file +32291 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#testNoOp_gas_Donate.snap b/.forge-snapshots/CLPoolManagerTest#testNoOp_gas_Donate.snap deleted file mode 100644 index ff833eec..00000000 --- a/.forge-snapshots/CLPoolManagerTest#testNoOp_gas_Donate.snap +++ /dev/null @@ -1 +0,0 @@ -54035 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#testNoOp_gas_Initialize.snap b/.forge-snapshots/CLPoolManagerTest#testNoOp_gas_Initialize.snap deleted file mode 100644 index 21de32ba..00000000 --- a/.forge-snapshots/CLPoolManagerTest#testNoOp_gas_Initialize.snap +++ /dev/null @@ -1 +0,0 @@ -65115 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#testNoOp_gas_ModifyPosition.snap b/.forge-snapshots/CLPoolManagerTest#testNoOp_gas_ModifyPosition.snap deleted file mode 100644 index bcfe4060..00000000 --- a/.forge-snapshots/CLPoolManagerTest#testNoOp_gas_ModifyPosition.snap +++ /dev/null @@ -1 +0,0 @@ -60287 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#testNoOp_gas_Swap.snap b/.forge-snapshots/CLPoolManagerTest#testNoOp_gas_Swap.snap deleted file mode 100644 index 2c23e660..00000000 --- a/.forge-snapshots/CLPoolManagerTest#testNoOp_gas_Swap.snap +++ /dev/null @@ -1 +0,0 @@ -56984 \ No newline at end of file diff --git a/.forge-snapshots/ExtsloadTest#extsload.snap b/.forge-snapshots/ExtsloadTest#extsload.snap index 2531cd46..19277c69 100644 --- a/.forge-snapshots/ExtsloadTest#extsload.snap +++ b/.forge-snapshots/ExtsloadTest#extsload.snap @@ -1 +1 @@ -7468 \ No newline at end of file +7446 \ No newline at end of file diff --git a/src/libraries/Hooks.sol b/src/libraries/Hooks.sol index 279998e3..f3309ffd 100644 --- a/src/libraries/Hooks.sol +++ b/src/libraries/Hooks.sol @@ -13,10 +13,12 @@ library Hooks { using ParametersHelper for bytes32; using LPFeeLibrary for uint24; - bytes4 constant NO_OP_SELECTOR = bytes4(keccak256(abi.encodePacked("NoOp"))); - - /// @notice Hook has no-op defined, but lacking before* call - error NoOpHookMissingBeforeCall(); + /// @notice Hook permissions contain conflict + /// 1. enabled beforeSwapReturnsDelta, but lacking beforeSwap call + /// 2. enabled afterSwapReturnsDelta, but lacking afterSwap call + /// 3. enabled addLiquidityReturnsDelta/mintReturnsDelta, but lacking addLiquidity/mint call + /// 4. enabled removeLiquidityReturnsDelta/burnReturnsDelta, but lacking removeLiquidityburn call + error HookPermissionsValidationError(); /// @notice Hook config validation failed /// 1. either registration bitmap mismatch @@ -27,6 +29,9 @@ library Hooks { /// @notice Hook did not return its selector error InvalidHookResponse(); + /// @notice Hook delta exceeds swap amount + error HookDeltaExceedsSwapAmount(); + /// @notice Utility function intended to be used in pool initialization to ensure /// the hook contract's hooks registration bitmap match the configration in the pool key function validateHookConfig(PoolKey memory poolKey) internal view { @@ -56,11 +61,4 @@ library Hooks { function shouldCall(bytes32 parameters, uint8 offset, IHooks hook) internal view returns (bool) { return hasOffsetEnabled(parameters, offset) && address(hook) != msg.sender; } - - /// @dev Verify hook return value matches no-op when these 2 conditions are met - /// 1) Hook have permission for no-op - /// 2) Return value is no-op selector - function isValidNoOpCall(bytes32 parameters, uint8 noOpOffset, bytes4 selector) internal pure returns (bool) { - return hasOffsetEnabled(parameters, noOpOffset) && selector == NO_OP_SELECTOR; - } } diff --git a/src/libraries/LPFeeLibrary.sol b/src/libraries/LPFeeLibrary.sol index 770812bd..6c3c1f8e 100644 --- a/src/libraries/LPFeeLibrary.sol +++ b/src/libraries/LPFeeLibrary.sol @@ -13,9 +13,16 @@ library LPFeeLibrary { error FeeTooLarge(); /// @dev the flag and mask - uint24 public constant STATIC_FEE_MASK = 0x0FFFFF; + uint24 public constant FEE_MASK = 0x7FFFFF; + uint24 public constant OVERRIDE_MASK = 0xBFFFFF; + + // the top bit of the fee in a PoolKey is used to signal if a Pool's LP fee is dynamic uint24 public constant DYNAMIC_FEE_FLAG = 0x800000; + // the second bit of the fee returned by beforeSwap is used to signal if the stored LP fee should be overridden in this swap + // only dynamic-fee pools can return a fee via the beforeSwap hook + uint24 public constant OVERRIDE_FEE_FLAG = 0x400000; + /// @dev the fee is represented in hundredths of a bip /// @dev the max fee for cl pool is 100% and for bin, it is 10% uint24 public constant ONE_HUNDRED_PERCENT_FEE = 1_000_000; @@ -33,6 +40,23 @@ library LPFeeLibrary { function getInitialLPFee(uint24 self) internal pure returns (uint24 lpFee) { // the initial fee for a dynamic fee pool is 0 if (self.isDynamicLPFee()) return 0; - lpFee = self & STATIC_FEE_MASK; + lpFee = self & FEE_MASK; + } + + /// @notice returns true if the fee has the override flag set (top bit of the uint24) + function isOverride(uint24 self) internal pure returns (bool) { + return self & OVERRIDE_FEE_FLAG != 0; + } + + /// @notice returns a fee with the override flag removed + function removeOverrideFlag(uint24 self) internal pure returns (uint24) { + return self & OVERRIDE_MASK; + } + + /// @notice Removes the override flag and validates the fee (reverts if the fee is too large) + function removeOverrideAndValidate(uint24 self, uint24 maxFee) internal pure returns (uint24) { + uint24 fee = self.removeOverrideFlag(); + fee.validate(maxFee); + return fee; } } diff --git a/src/pool-bin/BinPoolManager.sol b/src/pool-bin/BinPoolManager.sol index 90027597..94b49cdb 100644 --- a/src/pool-bin/BinPoolManager.sol +++ b/src/pool-bin/BinPoolManager.sol @@ -18,6 +18,8 @@ import {BinPosition} from "./libraries/BinPosition.sol"; import {LPFeeLibrary} from "../libraries/LPFeeLibrary.sol"; import {PackedUint128Math} from "./libraries/math/PackedUint128Math.sol"; import {Extsload} from "../Extsload.sol"; +import {BinHooks} from "./libraries/BinHooks.sol"; +import {BeforeSwapDelta} from "../types/BeforeSwapDelta.sol"; import "./interfaces/IBinHooks.sol"; /// @notice Holds the state for all bin pools @@ -68,13 +70,13 @@ contract BinPoolManager is IBinPoolManager, ProtocolFees, Extsload { } /// @inheritdoc IBinPoolManager - function getPosition(PoolId id, address owner, uint24 binId) + function getPosition(PoolId id, address owner, uint24 binId, bytes32 salt) external view override returns (BinPosition.Info memory position) { - return pools[id].positions.get(owner, binId); + return pools[id].positions.get(owner, binId, salt); } /// @inheritdoc IBinPoolManager @@ -100,7 +102,7 @@ contract BinPoolManager is IBinPoolManager, ProtocolFees, Extsload { IBinHooks hooks = IBinHooks(address(key.hooks)); Hooks.validateHookConfig(key); - _validateHookNoOp(key); + BinHooks.validatePermissionsConflict(key); /// @notice init value for dynamic lp fee is 0, but hook can still set it in afterInitialize uint24 lpFee = key.fee.getInitialLPFee(); @@ -135,27 +137,25 @@ contract BinPoolManager is IBinPoolManager, ProtocolFees, Extsload { whenNotPaused returns (BalanceDelta delta) { + if (amountIn == 0) revert InsufficientAmountIn(); + PoolId id = key.toId(); _checkPoolInitialized(id); - IBinHooks hooks = IBinHooks(address(key.hooks)); - if (key.parameters.shouldCall(HOOKS_BEFORE_SWAP_OFFSET, hooks)) { - bytes4 selector = hooks.beforeSwap(msg.sender, key, swapForY, amountIn, hookData); - if (key.parameters.isValidNoOpCall(HOOKS_NO_OP_OFFSET, selector)) { - // Sentinel return value used to signify that a NoOp occurred. - return BalanceDeltaLibrary.MAXIMUM_DELTA; - } else if (selector != IBinHooks.beforeSwap.selector) { - revert Hooks.InvalidHookResponse(); - } - } + (uint128 amountToSwap, BeforeSwapDelta beforeSwapDelta, uint24 lpFeeOverride) = + BinHooks.beforeSwap(key, swapForY, amountIn, hookData); /// @dev fix stack too deep { BinPool.SwapState memory state; - (delta, state) = - pools[id].swap(BinPool.SwapParams({swapForY: swapForY, binStep: key.parameters.getBinStep()}), amountIn); - - vault.accountPoolBalanceDelta(key, delta, msg.sender); + (delta, state) = pools[id].swap( + BinPool.SwapParams({ + swapForY: swapForY, + binStep: key.parameters.getBinStep(), + lpFeeOverride: lpFeeOverride + }), + amountToSwap + ); unchecked { if (state.feeForProtocol > 0) { @@ -170,11 +170,14 @@ contract BinPoolManager is IBinPoolManager, ProtocolFees, Extsload { ); } - if (key.parameters.shouldCall(HOOKS_AFTER_SWAP_OFFSET, hooks)) { - if (hooks.afterSwap(msg.sender, key, swapForY, amountIn, delta, hookData) != IBinHooks.afterSwap.selector) { - revert Hooks.InvalidHookResponse(); - } + BalanceDelta hookDelta; + (delta, hookDelta) = BinHooks.afterSwap(key, swapForY, amountIn, delta, hookData, beforeSwapDelta); + + if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) { + vault.accountPoolBalanceDelta(key, hookDelta, address(key.hooks)); } + + vault.accountPoolBalanceDelta(key, delta, msg.sender); } /// @inheritdoc IBinPoolManager @@ -240,11 +243,7 @@ contract BinPoolManager is IBinPoolManager, ProtocolFees, Extsload { IBinHooks hooks = IBinHooks(address(key.hooks)); if (key.parameters.shouldCall(HOOKS_BEFORE_MINT_OFFSET, hooks)) { - bytes4 selector = hooks.beforeMint(msg.sender, key, params, hookData); - if (key.parameters.isValidNoOpCall(HOOKS_NO_OP_OFFSET, selector)) { - // Sentinel return value used to signify that a NoOp occurred. - return (BalanceDeltaLibrary.MAXIMUM_DELTA, mintArray); - } else if (selector != IBinHooks.beforeMint.selector) { + if (hooks.beforeMint(msg.sender, key, params, hookData) != IBinHooks.beforeMint.selector) { revert Hooks.InvalidHookResponse(); } } @@ -256,12 +255,11 @@ contract BinPoolManager is IBinPoolManager, ProtocolFees, Extsload { to: msg.sender, liquidityConfigs: params.liquidityConfigs, amountIn: params.amountIn, - binStep: key.parameters.getBinStep() + binStep: key.parameters.getBinStep(), + salt: params.salt }) ); - vault.accountPoolBalanceDelta(key, delta, msg.sender); - unchecked { if (feeForProtocol > 0) { protocolFeesAccrued[key.currency0] += feeForProtocol.decodeX(); @@ -270,13 +268,15 @@ contract BinPoolManager is IBinPoolManager, ProtocolFees, Extsload { } /// @notice Make sure the first event is noted, so that later events from afterHook won't get mixed up with this one - emit Mint(id, msg.sender, mintArray.ids, mintArray.amounts, compositionFee, feeForProtocol); + emit Mint(id, msg.sender, mintArray.ids, params.salt, mintArray.amounts, compositionFee, feeForProtocol); - if (key.parameters.shouldCall(HOOKS_AFTER_MINT_OFFSET, hooks)) { - if (hooks.afterMint(msg.sender, key, params, delta, hookData) != IBinHooks.afterMint.selector) { - revert Hooks.InvalidHookResponse(); - } + BalanceDelta hookDelta; + (delta, hookDelta) = BinHooks.afterMint(key, params, delta, hookData); + + if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) { + vault.accountPoolBalanceDelta(key, hookDelta, address(key.hooks)); } + vault.accountPoolBalanceDelta(key, delta, msg.sender); } /// @inheritdoc IBinPoolManager @@ -291,30 +291,32 @@ contract BinPoolManager is IBinPoolManager, ProtocolFees, Extsload { IBinHooks hooks = IBinHooks(address(key.hooks)); if (key.parameters.shouldCall(HOOKS_BEFORE_BURN_OFFSET, hooks)) { - bytes4 selector = hooks.beforeBurn(msg.sender, key, params, hookData); - if (key.parameters.isValidNoOpCall(HOOKS_NO_OP_OFFSET, selector)) { - // Sentinel return value used to signify that a NoOp occurred. - return BalanceDeltaLibrary.MAXIMUM_DELTA; - } else if (selector != IBinHooks.beforeBurn.selector) { + if (hooks.beforeBurn(msg.sender, key, params, hookData) != IBinHooks.beforeBurn.selector) { revert Hooks.InvalidHookResponse(); } } uint256[] memory binIds; bytes32[] memory amountRemoved; - (delta, binIds, amountRemoved) = - pools[id].burn(BinPool.BurnParams({from: msg.sender, ids: params.ids, amountsToBurn: params.amountsToBurn})); - - vault.accountPoolBalanceDelta(key, delta, msg.sender); + (delta, binIds, amountRemoved) = pools[id].burn( + BinPool.BurnParams({ + from: msg.sender, + ids: params.ids, + amountsToBurn: params.amountsToBurn, + salt: params.salt + }) + ); /// @notice Make sure the first event is noted, so that later events from afterHook won't get mixed up with this one - emit Burn(id, msg.sender, binIds, amountRemoved); + emit Burn(id, msg.sender, binIds, params.salt, amountRemoved); - if (key.parameters.shouldCall(HOOKS_AFTER_BURN_OFFSET, hooks)) { - if (hooks.afterBurn(msg.sender, key, params, delta, hookData) != IBinHooks.afterBurn.selector) { - revert Hooks.InvalidHookResponse(); - } + BalanceDelta hookDelta; + (delta, hookDelta) = BinHooks.afterBurn(key, params, delta, hookData); + + if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) { + vault.accountPoolBalanceDelta(key, hookDelta, address(key.hooks)); } + vault.accountPoolBalanceDelta(key, delta, msg.sender); } function donate(PoolKey memory key, uint128 amount0, uint128 amount1, bytes calldata hookData) @@ -329,11 +331,7 @@ contract BinPoolManager is IBinPoolManager, ProtocolFees, Extsload { IBinHooks hooks = IBinHooks(address(key.hooks)); if (key.parameters.shouldCall(HOOKS_BEFORE_DONATE_OFFSET, hooks)) { - bytes4 selector = hooks.beforeDonate(msg.sender, key, amount0, amount1, hookData); - if (key.parameters.isValidNoOpCall(HOOKS_NO_OP_OFFSET, selector)) { - // Sentinel return value used to signify that a NoOp occurred. - return (BalanceDeltaLibrary.MAXIMUM_DELTA, binId); - } else if (selector != IBinHooks.beforeDonate.selector) { + if (hooks.beforeDonate(msg.sender, key, amount0, amount1, hookData) != IBinHooks.beforeDonate.selector) { revert Hooks.InvalidHookResponse(); } } @@ -377,18 +375,4 @@ contract BinPoolManager is IBinPoolManager, ProtocolFees, Extsload { function _checkPoolInitialized(PoolId id) internal view { if (pools[id].isNotInitialized()) revert PoolNotInitialized(); } - - function _validateHookNoOp(PoolKey memory key) internal pure { - // if no-op is active for hook, there must be a before* hook active too - if (key.parameters.hasOffsetEnabled(HOOKS_NO_OP_OFFSET)) { - if ( - !key.parameters.hasOffsetEnabled(HOOKS_BEFORE_MINT_OFFSET) - && !key.parameters.hasOffsetEnabled(HOOKS_BEFORE_BURN_OFFSET) - && !key.parameters.hasOffsetEnabled(HOOKS_BEFORE_SWAP_OFFSET) - && !key.parameters.hasOffsetEnabled(HOOKS_BEFORE_DONATE_OFFSET) - ) { - revert Hooks.NoOpHookMissingBeforeCall(); - } - } - } } diff --git a/src/pool-bin/interfaces/IBinHooks.sol b/src/pool-bin/interfaces/IBinHooks.sol index c8d06524..8d6f4adc 100644 --- a/src/pool-bin/interfaces/IBinHooks.sol +++ b/src/pool-bin/interfaces/IBinHooks.sol @@ -5,6 +5,7 @@ import {PoolKey} from "../../types/PoolKey.sol"; import {BalanceDelta} from "../../types/BalanceDelta.sol"; import {IBinPoolManager} from "./IBinPoolManager.sol"; import {IHooks} from "../../interfaces/IHooks.sol"; +import {BeforeSwapDelta} from "../../types/BeforeSwapDelta.sol"; uint8 constant HOOKS_BEFORE_INITIALIZE_OFFSET = 0; uint8 constant HOOKS_AFTER_INITIALIZE_OFFSET = 1; @@ -16,7 +17,10 @@ uint8 constant HOOKS_BEFORE_SWAP_OFFSET = 6; uint8 constant HOOKS_AFTER_SWAP_OFFSET = 7; uint8 constant HOOKS_BEFORE_DONATE_OFFSET = 8; uint8 constant HOOKS_AFTER_DONATE_OFFSET = 9; -uint8 constant HOOKS_NO_OP_OFFSET = 10; +uint8 constant HOOKS_BEFORE_SWAP_RETURNS_DELTA_OFFSET = 10; +uint8 constant HOOKS_AFTER_SWAP_RETURNS_DELTA_OFFSET = 11; +uint8 constant HOOKS_AFTER_MINT_RETURNS_DELTA_OFFSET = 12; +uint8 constant HOOKS_AFTER_BURN_RETURNS_DELTA_OFFSET = 13; /// @notice The PoolManager contract decides whether to invoke specific hook by inspecting the first 16 /// bits of bytes32 PoolKey.parameters. For example a 1 bit in the first bit will cause the beforeInitialize @@ -63,13 +67,14 @@ interface IBinHooks is IHooks { /// @param delta The amount owed to the locker (negative) or owed to the pool (positive) /// @param hookData Arbitrary data handed into the PoolManager by the liquidty provider to be be passed on to the hook /// @return bytes4 The function selector for the hook + /// @return BalanceDelta The hook's delta in token0 and token1. function afterMint( address sender, PoolKey calldata key, IBinPoolManager.MintParams calldata params, BalanceDelta delta, bytes calldata hookData - ) external returns (bytes4); + ) external returns (bytes4, BalanceDelta); /// @notice The hook called before removing liquidity /// @param sender The initial msg.sender for the modify position call @@ -91,13 +96,14 @@ interface IBinHooks is IHooks { /// @param delta The amount owed to the locker (negative) or owed to the pool (positive) /// @param hookData Arbitrary data handed into the PoolManager by the liquidty provider to be be passed on to the hook /// @return bytes4 The function selector for the hook + /// @return BalanceDelta The hook's delta in token0 and token1. function afterBurn( address sender, PoolKey calldata key, IBinPoolManager.BurnParams calldata params, BalanceDelta delta, bytes calldata hookData - ) external returns (bytes4); + ) external returns (bytes4, BalanceDelta); /// @notice The hook called before a swap /// @param sender The initial msg.sender for the swap call @@ -106,9 +112,14 @@ interface IBinHooks is IHooks { /// @param amountIn Amount of tokenX or tokenY in /// @param hookData Arbitrary data handed into the PoolManager by the swapper to be be passed on to the hook /// @return bytes4 The function selector for the hook + /// @return BeforeSwapDelta The hook's delta in specified and unspecified currencies. + /// @return uint24 Optionally override the lp fee, only used if three conditions are met: + /// 1) the Pool has a dynamic fee, + /// 2) the value's override flag is set to 1 i.e. vaule & OVERRIDE_FEE_FLAG = 0x400000 != 0 + /// 3) the value is less than or equal to the maximum fee (100_000) - 10% function beforeSwap(address sender, PoolKey calldata key, bool swapForY, uint128 amountIn, bytes calldata hookData) external - returns (bytes4); + returns (bytes4, BeforeSwapDelta, uint24); /// @notice The hook called after a swap /// @param sender The initial msg.sender for the swap call @@ -118,6 +129,7 @@ interface IBinHooks is IHooks { /// @param delta The amount owed to the locker (negative) or owed to the pool (positive) /// @param hookData Arbitrary data handed into the PoolManager by the swapper to be be passed on to the hook /// @return bytes4 The function selector for the hook + /// @return int128 The hook's delta in unspecified currency function afterSwap( address sender, PoolKey calldata key, @@ -125,7 +137,7 @@ interface IBinHooks is IHooks { uint128 amountIn, BalanceDelta delta, bytes calldata hookData - ) external returns (bytes4); + ) external returns (bytes4, int128); /// @notice The hook called before donate /// @param sender The initial msg.sender for the donate call diff --git a/src/pool-bin/interfaces/IBinPoolManager.sol b/src/pool-bin/interfaces/IBinPoolManager.sol index 457d0fb8..0423c05c 100644 --- a/src/pool-bin/interfaces/IBinPoolManager.sol +++ b/src/pool-bin/interfaces/IBinPoolManager.sol @@ -24,6 +24,9 @@ interface IBinPoolManager is IProtocolFees, IPoolManager, IExtsload { /// @notice Error thrown when owner set max bin step too small error MaxBinStepTooSmall(uint16 maxBinStep); + /// @notice Error thrown when amountIn is 0 + error InsufficientAmountIn(); + /// @notice Returns the constant representing the max bin step /// @return maxBinStep a value of 100 would represent a 1% price jump between bin (limit can be raised by owner) function MAX_BIN_STEP() external view returns (uint16); @@ -70,6 +73,7 @@ interface IBinPoolManager is IProtocolFees, IPoolManager, IExtsload { /// @param id The abi encoded hash of the pool key struct for the pool that was modified /// @param sender The address that modified the pool /// @param ids List of binId with liquidity added + /// @param salt The salt to distinguish different mint from the same owner /// @param amounts List of amount added to each bin /// @param compositionFee fee occurred /// @param pFee Protocol fee from the swap: token0 and token1 amount @@ -77,6 +81,7 @@ interface IBinPoolManager is IProtocolFees, IPoolManager, IExtsload { PoolId indexed id, address indexed sender, uint256[] ids, + bytes32 salt, bytes32[] amounts, bytes32 compositionFee, bytes32 pFee @@ -86,8 +91,9 @@ interface IBinPoolManager is IProtocolFees, IPoolManager, IExtsload { /// @param id The abi encoded hash of the pool key struct for the pool that was modified /// @param sender The address that modified the pool /// @param ids List of binId with liquidity removed + /// @param salt The salt to specify the position to burn if multiple positions are available /// @param amounts List of amount removed from each bin - event Burn(PoolId indexed id, address indexed sender, uint256[] ids, bytes32[] amounts); + event Burn(PoolId indexed id, address indexed sender, uint256[] ids, bytes32 salt, bytes32[] amounts); /// @notice Emitted when donate happen /// @param id The abi encoded hash of the pool key struct for the pool that was modified @@ -104,6 +110,8 @@ interface IBinPoolManager is IProtocolFees, IPoolManager, IExtsload { bytes32[] liquidityConfigs; /// @dev amountIn intended bytes32 amountIn; + /// the salt to distinguish different mint from the same owner + bytes32 salt; } struct BurnParams { @@ -111,6 +119,8 @@ interface IBinPoolManager is IProtocolFees, IPoolManager, IExtsload { uint256[] ids; /// @notice amount of share to burn for each bin uint256[] amountsToBurn; + /// the salt to specify the position to burn if multiple positions are available + bytes32 salt; } /// @notice Get the current value in slot0 of the given pool @@ -126,7 +136,8 @@ interface IBinPoolManager is IProtocolFees, IPoolManager, IExtsload { /// @param id The id of PoolKey /// @param owner Address of the owner /// @param binId The id of the bin - function getPosition(PoolId id, address owner, uint24 binId) + /// @param salt The salt to distinguish different positions for the same owner + function getPosition(PoolId id, address owner, uint24 binId, bytes32 salt) external view returns (BinPosition.Info memory position); diff --git a/src/pool-bin/libraries/BinHooks.sol b/src/pool-bin/libraries/BinHooks.sol new file mode 100644 index 00000000..30a8414f --- /dev/null +++ b/src/pool-bin/libraries/BinHooks.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap +pragma solidity ^0.8.24; + +import "../interfaces/IBinHooks.sol"; +import {PoolKey} from "../../types/PoolKey.sol"; +import {IBinPoolManager} from "../interfaces/IBinPoolManager.sol"; +import {Hooks} from "../../libraries/Hooks.sol"; +import {BalanceDelta, BalanceDeltaLibrary, toBalanceDelta} from "../../types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../../types/BeforeSwapDelta.sol"; +import {LPFeeLibrary} from "../../libraries/LPFeeLibrary.sol"; + +library BinHooks { + using Hooks for bytes32; + using LPFeeLibrary for uint24; + using BeforeSwapDeltaLibrary for BeforeSwapDelta; + + function validatePermissionsConflict(PoolKey memory key) internal pure { + if ( + key.parameters.hasOffsetEnabled(HOOKS_BEFORE_SWAP_RETURNS_DELTA_OFFSET) + && !key.parameters.hasOffsetEnabled(HOOKS_BEFORE_SWAP_OFFSET) + ) { + revert Hooks.HookPermissionsValidationError(); + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_SWAP_RETURNS_DELTA_OFFSET) + && !key.parameters.hasOffsetEnabled(HOOKS_AFTER_SWAP_OFFSET) + ) { + revert Hooks.HookPermissionsValidationError(); + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_MINT_RETURNS_DELTA_OFFSET) + && !key.parameters.hasOffsetEnabled(HOOKS_AFTER_MINT_OFFSET) + ) { + revert Hooks.HookPermissionsValidationError(); + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_BURN_RETURNS_DELTA_OFFSET) + && !key.parameters.hasOffsetEnabled(HOOKS_AFTER_BURN_OFFSET) + ) { + revert Hooks.HookPermissionsValidationError(); + } + } + + function afterMint( + PoolKey memory key, + IBinPoolManager.MintParams memory params, + BalanceDelta delta, + bytes calldata hookData + ) internal returns (BalanceDelta callerDelta, BalanceDelta hookDelta) { + IBinHooks hooks = IBinHooks(address(key.hooks)); + callerDelta = delta; + + if (key.parameters.shouldCall(HOOKS_AFTER_MINT_OFFSET, hooks)) { + bytes4 selector; + (selector, hookDelta) = hooks.afterMint(msg.sender, key, params, delta, hookData); + + if (selector != IBinHooks.afterMint.selector) { + revert Hooks.InvalidHookResponse(); + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_MINT_RETURNS_DELTA_OFFSET) + && hookDelta != BalanceDeltaLibrary.ZERO_DELTA + ) { + callerDelta = callerDelta - hookDelta; + } + } + } + + function afterBurn( + PoolKey memory key, + IBinPoolManager.BurnParams memory params, + BalanceDelta delta, + bytes calldata hookData + ) internal returns (BalanceDelta callerDelta, BalanceDelta hookDelta) { + IBinHooks hooks = IBinHooks(address(key.hooks)); + callerDelta = delta; + + if (key.parameters.shouldCall(HOOKS_AFTER_BURN_OFFSET, hooks)) { + bytes4 selector; + (selector, hookDelta) = hooks.afterBurn(msg.sender, key, params, delta, hookData); + + if (selector != IBinHooks.afterBurn.selector) { + revert Hooks.InvalidHookResponse(); + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_BURN_RETURNS_DELTA_OFFSET) + && hookDelta != BalanceDeltaLibrary.ZERO_DELTA + ) { + callerDelta = callerDelta - hookDelta; + } + } + } + + function beforeSwap(PoolKey memory key, bool swapForY, uint128 amountIn, bytes calldata hookData) + internal + returns (uint128 amountToSwap, BeforeSwapDelta beforeSwapDelta, uint24 lpFeeOverride) + { + IBinHooks hooks = IBinHooks(address(key.hooks)); + amountToSwap = amountIn; + + /// @notice If the hook is not registered, return the original amount to swap + if (!key.parameters.shouldCall(HOOKS_BEFORE_SWAP_OFFSET, hooks)) { + return (amountToSwap, BeforeSwapDeltaLibrary.ZERO_DELTA, lpFeeOverride); + } + + bytes4 selector; + (selector, beforeSwapDelta, lpFeeOverride) = hooks.beforeSwap(msg.sender, key, swapForY, amountIn, hookData); + if (selector != IBinHooks.beforeSwap.selector) { + revert Hooks.InvalidHookResponse(); + } + + if (!key.fee.isDynamicLPFee()) { + lpFeeOverride = 0; + } + + // Update the swap amount according to the hook's return + if (key.parameters.hasOffsetEnabled(HOOKS_BEFORE_SWAP_RETURNS_DELTA_OFFSET)) { + // any return in unspecified is passed to the afterSwap hook for handling + int128 hookDeltaSpecified = beforeSwapDelta.getSpecifiedDelta(); + + if (hookDeltaSpecified != 0) { + /// @dev default overflow check make sure the swap amount is always valid + if (hookDeltaSpecified > 0) { + amountToSwap += uint128(hookDeltaSpecified); + } else { + amountToSwap -= uint128(-hookDeltaSpecified); + } + } + } + } + + function afterSwap( + PoolKey memory key, + bool swapForY, + uint128 amountIn, + BalanceDelta delta, + bytes calldata hookData, + BeforeSwapDelta beforeSwapDelta + ) internal returns (BalanceDelta swapperDelta, BalanceDelta hookDelta) { + IBinHooks hooks = IBinHooks(address(key.hooks)); + swapperDelta = delta; + + int128 hookDeltaSpecified = beforeSwapDelta.getSpecifiedDelta(); + int128 hookDeltaUnspecified; + if (key.parameters.shouldCall(HOOKS_AFTER_SWAP_OFFSET, hooks)) { + bytes4 selector; + (selector, hookDeltaUnspecified) = hooks.afterSwap(msg.sender, key, swapForY, amountIn, delta, hookData); + if (selector != IBinHooks.afterSwap.selector) { + revert Hooks.InvalidHookResponse(); + } + + // TODO: Potentially optimization: skip decoding the second return value when afterSwapReturnDelta not set + if (!key.parameters.hasOffsetEnabled(HOOKS_AFTER_SWAP_RETURNS_DELTA_OFFSET)) { + hookDeltaUnspecified = 0; + } + } + hookDeltaUnspecified += beforeSwapDelta.getUnspecifiedDelta(); + + if (hookDeltaUnspecified != 0 || hookDeltaSpecified != 0) { + hookDelta = swapForY + ? toBalanceDelta(hookDeltaSpecified, hookDeltaUnspecified) + : toBalanceDelta(hookDeltaUnspecified, hookDeltaSpecified); + + // the caller has to pay for (or receive) the hook's delta + swapperDelta = delta - hookDelta; + } + } +} diff --git a/src/pool-bin/libraries/BinPool.sol b/src/pool-bin/libraries/BinPool.sol index b6c36d31..4a1efb36 100644 --- a/src/pool-bin/libraries/BinPool.sol +++ b/src/pool-bin/libraries/BinPool.sol @@ -14,6 +14,7 @@ import {SafeCast} from "./math/SafeCast.sol"; import {Constants} from "./Constants.sol"; import {FeeHelper} from "./FeeHelper.sol"; import {ProtocolFeeLibrary} from "../../libraries/ProtocolFeeLibrary.sol"; +import {LPFeeLibrary} from "../../libraries/LPFeeLibrary.sol"; library BinPool { using BinHelper for bytes32; @@ -30,6 +31,7 @@ library BinPool { using FeeHelper for uint128; using BinPool for State; using ProtocolFeeLibrary for uint24; + using LPFeeLibrary for uint24; error PoolNotInitialized(); error PoolAlreadyInitialized(); @@ -38,7 +40,6 @@ library BinPool { error BinPool__InvalidBurnInput(); error BinPool__BurnZeroAmount(uint24 id); error BinPool__ZeroAmountsOut(uint24 id); - error BinPool__InsufficientAmountIn(); error BinPool__OutOfLiquidity(); error BinPool__InsufficientAmountOut(); error BinPool__NoLiquidityToReceiveFees(); @@ -109,7 +110,7 @@ library BinPool { uint24 protocolFee = swapForY ? slot0Cache.protocolFee.getOneForZeroFee() : slot0Cache.protocolFee.getZeroForOneFee(); - uint24 swapFee = protocolFee.calculateSwapFee(params.lpFee); + uint24 swapFee = protocolFee == 0 ? params.lpFee : protocolFee.calculateSwapFee(params.lpFee); while (true) { uint128 binReserves = self.reserveOfBin[id].decode(!swapForY); @@ -156,7 +157,7 @@ library BinPool { { uint24 protocolFee = swapForY ? slot0Cache.protocolFee.getOneForZeroFee() : slot0Cache.protocolFee.getZeroForOneFee(); - swapFee = protocolFee.calculateSwapFee(params.lpFee); + swapFee = protocolFee == 0 ? params.lpFee : protocolFee.calculateSwapFee(params.lpFee); } while (true) { @@ -189,6 +190,7 @@ library BinPool { struct SwapParams { bool swapForY; uint16 binStep; + uint24 lpFeeOverride; } struct SwapState { @@ -202,8 +204,6 @@ library BinPool { internal returns (BalanceDelta result, SwapState memory swapState) { - if (amountIn == 0) revert BinPool__InsufficientAmountIn(); - Slot0 memory slot0Cache = self.slot0; swapState.activeId = slot0Cache.activeId; bool swapForY = params.swapForY; @@ -213,8 +213,17 @@ library BinPool { bytes32 amountsLeft = swapForY ? amountIn.encodeFirst() : amountIn.encodeSecond(); bytes32 amountsOut; - /// @dev swap fee includes protocolFee (charged first) and lpFee - swapState.swapFee = swapState.protocolFee.calculateSwapFee(slot0Cache.lpFee); + { + uint24 lpFee = params.lpFeeOverride.isOverride() + ? params.lpFeeOverride.removeOverrideAndValidate(LPFeeLibrary.TEN_PERCENT_FEE) + : slot0Cache.lpFee; + + /// @dev swap fee includes protocolFee (charged first) and lpFee + swapState.swapFee = swapState.protocolFee == 0 ? lpFee : swapState.protocolFee.calculateSwapFee(lpFee); + } + + /// @notice early return if hook has updated amountIn to 0 + if (amountIn == 0) return (result, swapState); while (true) { bytes32 binReserves = self.reserveOfBin[swapState.activeId]; @@ -264,6 +273,7 @@ library BinPool { bytes32[] liquidityConfigs; bytes32 amountIn; uint16 binStep; + bytes32 salt; } struct MintArrays { @@ -318,6 +328,7 @@ library BinPool { address from; uint256[] ids; uint256[] amountsToBurn; + bytes32 salt; } /// @notice Burn user's share and withdraw tokens form the pool. @@ -342,7 +353,7 @@ library BinPool { bytes32 binReserves = self.reserveOfBin[id]; uint256 supply = self.shareOfBin[id]; - _subShare(self, params.from, id, amountToBurn); + _subShare(self, params.from, id, params.salt, amountToBurn); bytes32 amountsOutFromBin = binReserves.getAmountOutOfBin(amountToBurn, supply); @@ -395,11 +406,21 @@ library BinPool { { amountsLeft = params.amountIn; + uint24 id; + uint256 shares; + bytes32 amountsIn; + bytes32 amountsInToBin; + bytes32 binFeeAmt; + bytes32 binCompositionFee; for (uint256 i; i < params.liquidityConfigs.length;) { - (bytes32 maxAmountsInToBin, uint24 id) = params.liquidityConfigs[i].getAmountsAndId(params.amountIn); + // fix stack too deep + { + bytes32 maxAmountsInToBin; + (maxAmountsInToBin, id) = params.liquidityConfigs[i].getAmountsAndId(params.amountIn); - (uint256 shares, bytes32 amountsIn, bytes32 amountsInToBin, bytes32 binFeeAmt, bytes32 binCompositionFee) = - _updateBin(self, params, id, maxAmountsInToBin); + (shares, amountsIn, amountsInToBin, binFeeAmt, binCompositionFee) = + _updateBin(self, params, id, maxAmountsInToBin); + } amountsLeft = amountsLeft.sub(amountsIn); feeForProtocol = feeForProtocol.add(binFeeAmt); @@ -408,7 +429,7 @@ library BinPool { arrays.amounts[i] = amountsInToBin; arrays.liquidityMinted[i] = shares; - _addShare(self, params.to, id, shares); + _addShare(self, params.to, id, params.salt, shares); compositionFee = compositionFee.add(binCompositionFee); @@ -475,14 +496,14 @@ library BinPool { } /// @notice Subtract share from user's position and update total share supply of bin - function _subShare(State storage self, address owner, uint24 binId, uint256 shares) internal { - self.positions.get(owner, binId).subShare(shares); + function _subShare(State storage self, address owner, uint24 binId, bytes32 salt, uint256 shares) internal { + self.positions.get(owner, binId, salt).subShare(shares); self.shareOfBin[binId] -= shares; } /// @notice Add share to user's position and update total share supply of bin - function _addShare(State storage self, address owner, uint24 binId, uint256 shares) internal { - self.positions.get(owner, binId).addShare(shares); + function _addShare(State storage self, address owner, uint24 binId, bytes32 salt, uint256 shares) internal { + self.positions.get(owner, binId, salt).addShare(shares); self.shareOfBin[binId] += shares; } diff --git a/src/pool-bin/libraries/BinPosition.sol b/src/pool-bin/libraries/BinPosition.sol index 6cf568a3..c07f279f 100644 --- a/src/pool-bin/libraries/BinPosition.sol +++ b/src/pool-bin/libraries/BinPosition.sol @@ -18,8 +18,9 @@ library BinPosition { /// @param self The mapping containing all user positions /// @param owner The address of the position owner /// @param binId The binId + /// @param salt The salt to distinguish different positions for the same owner /// @return position The position info struct of the given owners' position - function get(mapping(bytes32 => Info) storage self, address owner, uint24 binId) + function get(mapping(bytes32 => Info) storage self, address owner, uint24 binId, bytes32 salt) internal view returns (BinPosition.Info storage position) @@ -29,9 +30,13 @@ library BinPosition { // ref: https://github.com/Vectorized/solady/blob/main/src/tokens/ERC20.sol#L95 // memory will be 12 bytes of zeros, the 20 bytes of address, 3 bytes for uint24 assembly { + mstore(0x23, salt) mstore(0x03, binId) mstore(0x00, owner) - key := keccak256(0x0c, 0x17) + key := keccak256(0x0c, 0x37) + // 0x00 - 0x3f is scratch space + // 0x40 ~ 0x46 should be clear to avoid polluting free pointer + mstore(0x23, 0) } position = self[key]; } diff --git a/src/pool-cl/CLPoolManager.sol b/src/pool-cl/CLPoolManager.sol index 54b0b572..a54b2217 100644 --- a/src/pool-cl/CLPoolManager.sol +++ b/src/pool-cl/CLPoolManager.sol @@ -20,6 +20,8 @@ import {BalanceDelta, BalanceDeltaLibrary} from "../types/BalanceDelta.sol"; import {Extsload} from "../Extsload.sol"; import {SafeCast} from "../libraries/SafeCast.sol"; import {CLPoolGetters} from "./libraries/CLPoolGetters.sol"; +import {CLHooks} from "./libraries/CLHooks.sol"; +import {BeforeSwapDelta} from "../types/BeforeSwapDelta.sol"; contract CLPoolManager is ICLPoolManager, ProtocolFees, Extsload { using SafeCast for int256; @@ -64,23 +66,23 @@ contract CLPoolManager is ICLPoolManager, ProtocolFees, Extsload { } /// @inheritdoc ICLPoolManager - function getLiquidity(PoolId id, address _owner, int24 tickLower, int24 tickUpper) + 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).liquidity; + return pools[id].positions.get(_owner, tickLower, tickUpper, salt).liquidity; } /// @inheritdoc ICLPoolManager - function getPosition(PoolId id, address owner, int24 tickLower, int24 tickUpper) + function getPosition(PoolId id, address owner, int24 tickLower, int24 tickUpper, bytes32 salt) external view override returns (CLPosition.Info memory position) { - return pools[id].positions.get(owner, tickLower, tickUpper); + return pools[id].positions.get(owner, tickLower, tickUpper, salt); } /// @inheritdoc ICLPoolManager @@ -97,7 +99,7 @@ contract CLPoolManager is ICLPoolManager, ProtocolFees, Extsload { ICLHooks hooks = ICLHooks(address(key.hooks)); Hooks.validateHookConfig(key); - _validateHookNoOp(key); + CLHooks.validatePermissionsConflict(key); /// @notice init value for dynamic lp fee is 0, but hook can still set it in afterInitialize uint24 lpFee = key.fee.getInitialLPFee(); @@ -144,22 +146,16 @@ contract CLPoolManager is ICLPoolManager, ProtocolFees, Extsload { _checkPoolInitialized(id); ICLHooks hooks = ICLHooks(address(key.hooks)); - if (params.liquidityDelta > 0 && key.parameters.shouldCall(HOOKS_BEFORE_ADD_LIQUIDITY_OFFSET, hooks)) { - bytes4 selector = hooks.beforeAddLiquidity(msg.sender, key, params, hookData); - if (key.parameters.isValidNoOpCall(HOOKS_NO_OP_OFFSET, selector)) { - // Sentinel return value used to signify that a NoOp occurred. - return (BalanceDeltaLibrary.MAXIMUM_DELTA, BalanceDeltaLibrary.ZERO_DELTA); - } else if (selector != ICLHooks.beforeAddLiquidity.selector) { + if (hooks.beforeAddLiquidity(msg.sender, key, params, hookData) != ICLHooks.beforeAddLiquidity.selector) { revert Hooks.InvalidHookResponse(); } } else if (params.liquidityDelta <= 0 && key.parameters.shouldCall(HOOKS_BEFORE_REMOVE_LIQUIDITY_OFFSET, hooks)) { - bytes4 selector = hooks.beforeRemoveLiquidity(msg.sender, key, params, hookData); - if (key.parameters.isValidNoOpCall(HOOKS_NO_OP_OFFSET, selector)) { - // Sentinel return value used to signify that a NoOp occurred. - return (BalanceDeltaLibrary.MAXIMUM_DELTA, BalanceDeltaLibrary.ZERO_DELTA); - } else if (selector != ICLHooks.beforeRemoveLiquidity.selector) { + if ( + hooks.beforeRemoveLiquidity(msg.sender, key, params, hookData) + != ICLHooks.beforeRemoveLiquidity.selector + ) { revert Hooks.InvalidHookResponse(); } } @@ -170,30 +166,21 @@ contract CLPoolManager is ICLPoolManager, ProtocolFees, Extsload { tickLower: params.tickLower, tickUpper: params.tickUpper, liquidityDelta: params.liquidityDelta.toInt128(), - tickSpacing: key.parameters.getTickSpacing() + tickSpacing: key.parameters.getTickSpacing(), + salt: params.salt }) ); - vault.accountPoolBalanceDelta(key, delta + feeDelta, msg.sender); - /// @notice Make sure the first event is noted, so that later events from afterHook won't get mixed up with this one - emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper, params.liquidityDelta); + emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper, params.salt, params.liquidityDelta); - if (params.liquidityDelta > 0 && key.parameters.shouldCall(HOOKS_AFTER_ADD_LIQUIDITY_OFFSET, hooks)) { - if ( - hooks.afterAddLiquidity(msg.sender, key, params, delta, hookData) != ICLHooks.afterAddLiquidity.selector - ) { - revert Hooks.InvalidHookResponse(); - } - } else if (params.liquidityDelta <= 0 && key.parameters.shouldCall(HOOKS_AFTER_REMOVE_LIQUIDITY_OFFSET, hooks)) - { - if ( - hooks.afterRemoveLiquidity(msg.sender, key, params, delta, hookData) - != ICLHooks.afterRemoveLiquidity.selector - ) { - revert Hooks.InvalidHookResponse(); - } + BalanceDelta hookDelta; + (delta, hookDelta) = CLHooks.afterModifyLiquidity(key, params, delta + feeDelta, hookData); + + if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) { + vault.accountPoolBalanceDelta(key, hookDelta, address(key.hooks)); } + vault.accountPoolBalanceDelta(key, delta, msg.sender); } /// @inheritdoc ICLPoolManager @@ -204,35 +191,24 @@ contract CLPoolManager is ICLPoolManager, ProtocolFees, Extsload { whenNotPaused returns (BalanceDelta delta) { + if (params.amountSpecified == 0) revert SwapAmountCannotBeZero(); + PoolId id = key.toId(); _checkPoolInitialized(id); - ICLHooks hooks = ICLHooks(address(key.hooks)); - - if (key.parameters.shouldCall(HOOKS_BEFORE_SWAP_OFFSET, hooks)) { - bytes4 selector = hooks.beforeSwap(msg.sender, key, params, hookData); - if (key.parameters.isValidNoOpCall(HOOKS_NO_OP_OFFSET, selector)) { - // Sentinel return value used to signify that a NoOp occurred. - return BalanceDeltaLibrary.MAXIMUM_DELTA; - } else if (selector != ICLHooks.beforeSwap.selector) { - revert Hooks.InvalidHookResponse(); - } - } - + (int256 amountToSwap, BeforeSwapDelta beforeSwapDelta, uint24 lpFeeOverride) = + CLHooks.beforeSwap(key, params, hookData); CLPool.SwapState memory state; (delta, state) = pools[id].swap( CLPool.SwapParams({ tickSpacing: key.parameters.getTickSpacing(), zeroForOne: params.zeroForOne, - amountSpecified: params.amountSpecified, - sqrtPriceLimitX96: params.sqrtPriceLimitX96 + amountSpecified: amountToSwap, + sqrtPriceLimitX96: params.sqrtPriceLimitX96, + lpFeeOverride: lpFeeOverride }) ); - /// @dev delta already includes protocol fee - /// all tokens go into the vault - vault.accountPoolBalanceDelta(key, delta, msg.sender); - unchecked { if (state.feeForProtocol > 0) { protocolFeesAccrued[params.zeroForOne ? key.currency0 : key.currency1] += state.feeForProtocol; @@ -252,11 +228,16 @@ contract CLPoolManager is ICLPoolManager, ProtocolFees, Extsload { state.protocolFee ); - if (key.parameters.shouldCall(HOOKS_AFTER_SWAP_OFFSET, hooks)) { - if (hooks.afterSwap(msg.sender, key, params, delta, hookData) != ICLHooks.afterSwap.selector) { - revert Hooks.InvalidHookResponse(); - } + BalanceDelta hookDelta; + (delta, hookDelta) = CLHooks.afterSwap(key, params, delta, hookData, beforeSwapDelta); + + if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) { + vault.accountPoolBalanceDelta(key, hookDelta, address(key.hooks)); } + + /// @dev delta already includes protocol fee + /// all tokens go into the vault + vault.accountPoolBalanceDelta(key, delta, msg.sender); } /// @inheritdoc ICLPoolManager @@ -272,11 +253,7 @@ contract CLPoolManager is ICLPoolManager, ProtocolFees, Extsload { ICLHooks hooks = ICLHooks(address(key.hooks)); if (key.parameters.shouldCall(HOOKS_BEFORE_DONATE_OFFSET, hooks)) { - bytes4 selector = hooks.beforeDonate(msg.sender, key, amount0, amount1, hookData); - if (key.parameters.isValidNoOpCall(HOOKS_NO_OP_OFFSET, selector)) { - // Sentinel return value used to signify that a NoOp occurred. - return BalanceDeltaLibrary.MAXIMUM_DELTA; - } else if (selector != ICLHooks.beforeDonate.selector) { + if (hooks.beforeDonate(msg.sender, key, amount0, amount1, hookData) != ICLHooks.beforeDonate.selector) { revert Hooks.InvalidHookResponse(); } } @@ -329,20 +306,6 @@ contract CLPoolManager is ICLPoolManager, ProtocolFees, Extsload { if (pools[id].isNotInitialized()) revert PoolNotInitialized(); } - function _validateHookNoOp(PoolKey memory key) internal pure { - // if no-op is active for hook, there must be a before* hook active too - if (key.parameters.hasOffsetEnabled(HOOKS_NO_OP_OFFSET)) { - if ( - !key.parameters.hasOffsetEnabled(HOOKS_BEFORE_ADD_LIQUIDITY_OFFSET) - && !key.parameters.hasOffsetEnabled(HOOKS_BEFORE_REMOVE_LIQUIDITY_OFFSET) - && !key.parameters.hasOffsetEnabled(HOOKS_BEFORE_SWAP_OFFSET) - && !key.parameters.hasOffsetEnabled(HOOKS_BEFORE_DONATE_OFFSET) - ) { - revert Hooks.NoOpHookMissingBeforeCall(); - } - } - } - /// @notice not accept ether // receive() external payable {} // fallback() external payable {} diff --git a/src/pool-cl/interfaces/ICLHooks.sol b/src/pool-cl/interfaces/ICLHooks.sol index 1b14f0c9..17c4345d 100644 --- a/src/pool-cl/interfaces/ICLHooks.sol +++ b/src/pool-cl/interfaces/ICLHooks.sol @@ -5,6 +5,7 @@ import {PoolKey} from "../../types/PoolKey.sol"; import {BalanceDelta} from "../../types/BalanceDelta.sol"; import {ICLPoolManager} from "./ICLPoolManager.sol"; import {IHooks} from "../../interfaces/IHooks.sol"; +import {BeforeSwapDelta} from "../../types/BeforeSwapDelta.sol"; /// @dev Update PoolManager#_validateHookNoOp if theres a new offset with no-op uint8 constant HOOKS_BEFORE_INITIALIZE_OFFSET = 0; @@ -17,7 +18,10 @@ uint8 constant HOOKS_BEFORE_SWAP_OFFSET = 6; uint8 constant HOOKS_AFTER_SWAP_OFFSET = 7; uint8 constant HOOKS_BEFORE_DONATE_OFFSET = 8; uint8 constant HOOKS_AFTER_DONATE_OFFSET = 9; -uint8 constant HOOKS_NO_OP_OFFSET = 10; +uint8 constant HOOKS_BEFORE_SWAP_RETURNS_DELTA_OFFSET = 10; +uint8 constant HOOKS_AFTER_SWAP_RETURNS_DELTA_OFFSET = 11; +uint8 constant HOOKS_AFTER_ADD_LIQUIDIY_RETURNS_DELTA_OFFSET = 12; +uint8 constant HOOKS_AFTER_REMOVE_LIQUIDIY_RETURNS_DELTA_OFFSET = 13; /// @notice The PoolManager contract decides whether to invoke specific hooks by inspecting the leading bits /// of the hooks contract address. For example, a 1 bit in the first bit of the address will @@ -69,13 +73,14 @@ interface ICLHooks is IHooks { /// @param params The parameters for adding liquidity /// @param hookData Arbitrary data handed into the PoolManager by the liquidty provider to be be passed on to the hook /// @return bytes4 The function selector for the hook + /// @return BalanceDelta The hook's delta in token0 and token1. function afterAddLiquidity( address sender, PoolKey calldata key, ICLPoolManager.ModifyLiquidityParams calldata params, BalanceDelta delta, bytes calldata hookData - ) external returns (bytes4); + ) external returns (bytes4, BalanceDelta); /// @notice The hook called before liquidity is removed /// @param sender The initial msg.sender for the remove liquidity call @@ -96,13 +101,14 @@ interface ICLHooks is IHooks { /// @param params The parameters for removing liquidity /// @param hookData Arbitrary data handed into the PoolManager by the liquidty provider to be be passed on to the hook /// @return bytes4 The function selector for the hook + /// @return BalanceDelta The hook's delta in token0 and token1. function afterRemoveLiquidity( address sender, PoolKey calldata key, ICLPoolManager.ModifyLiquidityParams calldata params, BalanceDelta delta, bytes calldata hookData - ) external returns (bytes4); + ) external returns (bytes4, BalanceDelta); /// @notice The hook called before a swap /// @param sender The initial msg.sender for the swap call @@ -110,12 +116,17 @@ interface ICLHooks is IHooks { /// @param params The parameters for the swap /// @param hookData Arbitrary data handed into the PoolManager by the swapper to be be passed on to the hook /// @return bytes4 The function selector for the hook + /// @return BeforeSwapDelta The hook's delta in specified and unspecified currencies. + /// @return uint24 Optionally override the lp fee, only used if three conditions are met: + /// 1) the Pool has a dynamic fee, + /// 2) the value's override flag is set to 1 i.e. vaule & OVERRIDE_FEE_FLAG = 0x400000 != 0 + /// 3) the value is less than or equal to the maximum fee (1 million) function beforeSwap( address sender, PoolKey calldata key, ICLPoolManager.SwapParams calldata params, bytes calldata hookData - ) external returns (bytes4); + ) external returns (bytes4, BeforeSwapDelta, uint24); /// @notice The hook called after a swap /// @param sender The initial msg.sender for the swap call @@ -124,13 +135,14 @@ interface ICLHooks is IHooks { /// @param delta The amount owed to the locker (positive) or owed to the pool (negative) /// @param hookData Arbitrary data handed into the PoolManager by the swapper to be be passed on to the hook /// @return bytes4 The function selector for the hook + /// @return int128 The hook's delta in unspecified currency function afterSwap( address sender, PoolKey calldata key, ICLPoolManager.SwapParams calldata params, BalanceDelta delta, bytes calldata hookData - ) external returns (bytes4); + ) external returns (bytes4, int128); /// @notice The hook called before donate /// @param sender The initial msg.sender for the donate call diff --git a/src/pool-cl/interfaces/ICLPoolManager.sol b/src/pool-cl/interfaces/ICLPoolManager.sol index ff92ac4f..549a92d2 100644 --- a/src/pool-cl/interfaces/ICLPoolManager.sol +++ b/src/pool-cl/interfaces/ICLPoolManager.sol @@ -21,6 +21,8 @@ interface ICLPoolManager is IProtocolFees, IPoolManager, IExtsload { error TickSpacingTooSmall(); /// @notice Error thrown when add liquidity is called when paused() error PoolPaused(); + /// @notice Thrown when trying to swap amount of 0 + error SwapAmountCannotBeZero(); /// @notice Emitted when a new pool is initialized /// @param id The abi encoded hash of the pool key struct for the new pool @@ -43,9 +45,10 @@ interface ICLPoolManager is IProtocolFees, IPoolManager, IExtsload { /// @param sender The address that modified the pool /// @param tickLower The lower tick of the position /// @param tickUpper The upper tick of the position + /// @param salt The value used to create a unique liquidity position /// @param liquidityDelta The amount of liquidity that was added or removed event ModifyLiquidity( - PoolId indexed id, address indexed sender, int24 tickLower, int24 tickUpper, int256 liquidityDelta + PoolId indexed id, address indexed sender, int24 tickLower, int24 tickUpper, bytes32 salt, int256 liquidityDelta ); /// @notice Emitted for swaps between currency0 and currency1 @@ -94,13 +97,13 @@ interface ICLPoolManager is IProtocolFees, IPoolManager, IExtsload { 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) + function getLiquidity(PoolId id, address owner, int24 tickLower, int24 tickUpper, bytes32 salt) external view returns (uint128 liquidity); /// @notice Get the position struct for a specified pool and position - function getPosition(PoolId id, address owner, int24 tickLower, int24 tickUpper) + function getPosition(PoolId id, address owner, int24 tickLower, int24 tickUpper, bytes32 salt) external view returns (CLPosition.Info memory position); @@ -116,11 +119,13 @@ interface ICLPoolManager is IProtocolFees, IPoolManager, IExtsload { int24 tickUpper; // how to modify the liquidity int256 liquidityDelta; + // a value to set if you want unique liquidity positions at the same range + bytes32 salt; } /// @notice Modify the position for the given pool - /// @return delta The balance delta of the liquidity change - /// @return feeDelta The balance delta of the fees generated in the liquidity range + /// @return delta The total balance delta of the caller of modifyLiquidity. + /// @return feeDelta The balance delta of the fees generated in the liquidity range. function modifyLiquidity(PoolKey memory key, ModifyLiquidityParams memory params, bytes calldata hookData) external returns (BalanceDelta delta, BalanceDelta feeDelta); @@ -132,6 +137,13 @@ interface ICLPoolManager is IProtocolFees, IPoolManager, IExtsload { } /// @notice Swap against the given pool + /// @param key The pool to swap in + /// @param params The parameters for swapping + /// @param hookData Any data to pass to the callback + /// @return delta The balance delta of the address swapping + /// @dev Swapping on low liquidity pools may cause unexpected swap amounts when liquidity available is less than amountSpecified. + /// Additionally note that if interacting with hooks that have the BEFORE_SWAP_RETURNS_DELTA_FLAG or AFTER_SWAP_RETURNS_DELTA_FLAG + /// the hook may alter the swap input/output. Integrators should perform checks on the returned swapDelta. function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData) external returns (BalanceDelta); diff --git a/src/pool-cl/libraries/CLHooks.sol b/src/pool-cl/libraries/CLHooks.sol new file mode 100644 index 00000000..618213ca --- /dev/null +++ b/src/pool-cl/libraries/CLHooks.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap +pragma solidity ^0.8.24; + +import "../interfaces/ICLHooks.sol"; +import {PoolKey} from "../../types/PoolKey.sol"; +import {ICLPoolManager} from "../interfaces/ICLPoolManager.sol"; +import {Hooks} from "../../libraries/Hooks.sol"; +import {BalanceDelta, BalanceDeltaLibrary, toBalanceDelta} from "../../types/BalanceDelta.sol"; +import {LPFeeLibrary} from "../../libraries/LPFeeLibrary.sol"; +import {BeforeSwapDeltaLibrary, BeforeSwapDelta} from "../../types/BeforeSwapDelta.sol"; + +library CLHooks { + using Hooks for bytes32; + using LPFeeLibrary for uint24; + using BeforeSwapDeltaLibrary for BeforeSwapDelta; + + function validatePermissionsConflict(PoolKey memory key) internal pure { + if ( + key.parameters.hasOffsetEnabled(HOOKS_BEFORE_SWAP_RETURNS_DELTA_OFFSET) + && !key.parameters.hasOffsetEnabled(HOOKS_BEFORE_SWAP_OFFSET) + ) { + revert Hooks.HookPermissionsValidationError(); + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_SWAP_RETURNS_DELTA_OFFSET) + && !key.parameters.hasOffsetEnabled(HOOKS_AFTER_SWAP_OFFSET) + ) { + revert Hooks.HookPermissionsValidationError(); + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_ADD_LIQUIDIY_RETURNS_DELTA_OFFSET) + && !key.parameters.hasOffsetEnabled(HOOKS_AFTER_ADD_LIQUIDITY_OFFSET) + ) { + revert Hooks.HookPermissionsValidationError(); + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_REMOVE_LIQUIDIY_RETURNS_DELTA_OFFSET) + && !key.parameters.hasOffsetEnabled(HOOKS_AFTER_REMOVE_LIQUIDITY_OFFSET) + ) { + revert Hooks.HookPermissionsValidationError(); + } + } + + function afterModifyLiquidity( + PoolKey memory key, + ICLPoolManager.ModifyLiquidityParams memory params, + BalanceDelta delta, + bytes calldata hookData + ) internal returns (BalanceDelta callerDelta, BalanceDelta hookDelta) { + ICLHooks hooks = ICLHooks(address(key.hooks)); + callerDelta = delta; + + if (params.liquidityDelta > 0 && key.parameters.shouldCall(HOOKS_AFTER_ADD_LIQUIDITY_OFFSET, hooks)) { + bytes4 selector; + (selector, hookDelta) = hooks.afterAddLiquidity(msg.sender, key, params, delta, hookData); + + if (selector != ICLHooks.afterAddLiquidity.selector) { + revert Hooks.InvalidHookResponse(); + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_ADD_LIQUIDIY_RETURNS_DELTA_OFFSET) + && hookDelta != BalanceDeltaLibrary.ZERO_DELTA + ) { + callerDelta = callerDelta - hookDelta; + } + } else if (params.liquidityDelta < 0 && key.parameters.shouldCall(HOOKS_AFTER_REMOVE_LIQUIDITY_OFFSET, hooks)) { + bytes4 selector; + (selector, hookDelta) = hooks.afterRemoveLiquidity(msg.sender, key, params, delta, hookData); + + if (selector != ICLHooks.afterRemoveLiquidity.selector) { + revert Hooks.InvalidHookResponse(); + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_REMOVE_LIQUIDIY_RETURNS_DELTA_OFFSET) + && hookDelta != BalanceDeltaLibrary.ZERO_DELTA + ) { + callerDelta = callerDelta - hookDelta; + } + } + } + + function beforeSwap(PoolKey memory key, ICLPoolManager.SwapParams memory params, bytes calldata hookData) + internal + returns (int256 amountToSwap, BeforeSwapDelta beforeSwapDelta, uint24 lpFeeOverride) + { + ICLHooks hooks = ICLHooks(address(key.hooks)); + amountToSwap = params.amountSpecified; + + /// @notice If the hook is not registered, return the original amount to swap + if (!key.parameters.shouldCall(HOOKS_BEFORE_SWAP_OFFSET, hooks)) { + return (amountToSwap, beforeSwapDelta, lpFeeOverride); + } + + bytes4 selector; + // TODO: Potentially optimization: skip decoding the second return value when afterSwapReturnDelta not set + (selector, beforeSwapDelta, lpFeeOverride) = hooks.beforeSwap(msg.sender, key, params, hookData); + if (selector != ICLHooks.beforeSwap.selector) { + revert Hooks.InvalidHookResponse(); + } + + if (!key.fee.isDynamicLPFee()) { + lpFeeOverride = 0; + } + + // Update the swap amount according to the hook's return, and check that the swap type doesnt change (exact input/output) + if (key.parameters.hasOffsetEnabled(HOOKS_BEFORE_SWAP_RETURNS_DELTA_OFFSET)) { + // any return in unspecified is passed to the afterSwap hook for handling + int128 hookDeltaSpecified = beforeSwapDelta.getSpecifiedDelta(); + + if (hookDeltaSpecified != 0) { + bool exactInput = amountToSwap > 0; + amountToSwap += hookDeltaSpecified; + if (exactInput ? amountToSwap < 0 : amountToSwap > 0) revert Hooks.HookDeltaExceedsSwapAmount(); + } + } + } + + function afterSwap( + PoolKey memory key, + ICLPoolManager.SwapParams memory params, + BalanceDelta delta, + bytes calldata hookData, + BeforeSwapDelta beforeSwapDelta + ) internal returns (BalanceDelta swapperDelta, BalanceDelta hookDelta) { + ICLHooks hooks = ICLHooks(address(key.hooks)); + swapperDelta = delta; + + int128 hookDeltaSpecified = beforeSwapDelta.getSpecifiedDelta(); + int128 hookDeltaUnspecified; + if (key.parameters.shouldCall(HOOKS_AFTER_SWAP_OFFSET, hooks)) { + bytes4 selector; + (selector, hookDeltaUnspecified) = hooks.afterSwap(msg.sender, key, params, delta, hookData); + if (selector != ICLHooks.afterSwap.selector) { + revert Hooks.InvalidHookResponse(); + } + + if (!key.parameters.hasOffsetEnabled(HOOKS_AFTER_SWAP_RETURNS_DELTA_OFFSET)) { + hookDeltaUnspecified = 0; + } + } + hookDeltaUnspecified += beforeSwapDelta.getUnspecifiedDelta(); + + if (hookDeltaUnspecified != 0 || hookDeltaSpecified != 0) { + hookDelta = (params.amountSpecified > 0 == params.zeroForOne) + ? toBalanceDelta(hookDeltaSpecified, hookDeltaUnspecified) + : toBalanceDelta(hookDeltaUnspecified, hookDeltaSpecified); + + // the caller has to pay for (or receive) the hook's delta + swapperDelta = delta - hookDelta; + } + } +} diff --git a/src/pool-cl/libraries/CLPool.sol b/src/pool-cl/libraries/CLPool.sol index 5f10defd..e6c4cfe5 100644 --- a/src/pool-cl/libraries/CLPool.sol +++ b/src/pool-cl/libraries/CLPool.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.24; import {CLPosition} from "./CLPosition.sol"; import {TickMath} from "./TickMath.sol"; -import {BalanceDelta, toBalanceDelta} from "../../types/BalanceDelta.sol"; +import {BalanceDelta, BalanceDeltaLibrary, toBalanceDelta} from "../../types/BalanceDelta.sol"; import {Tick} from "./Tick.sol"; import {TickBitmap} from "./TickBitmap.sol"; import {SqrtPriceMath} from "./SqrtPriceMath.sol"; @@ -26,6 +26,7 @@ library CLPool { using LiquidityMath for uint128; using CLPool for State; using ProtocolFeeLibrary for uint24; + using LPFeeLibrary for uint24; /// @notice Thrown when trying to initalize an already initialized pool error PoolAlreadyInitialized(); @@ -33,9 +34,6 @@ library CLPool { /// @notice Thrown when trying to interact with a non-initialized pool error PoolNotInitialized(); - /// @notice Thrown when trying to swap amount of 0 - error SwapAmountCannotBeZero(); - /// @notice Thrown when trying to swap with max lp fee and specifying an output amount error InvalidFeeForExactOut(); @@ -94,6 +92,8 @@ library CLPool { int128 liquidityDelta; // the spacing between ticks int24 tickSpacing; + // used to distinguish positions of the same owner, at the same tick range + bytes32 salt; } /// @dev Effect changes to the liquidity of a position in a pool @@ -145,7 +145,7 @@ library CLPool { } // Fees earned from LPing are removed from the pool balance. - feeDelta = toBalanceDelta(feesOwed0.toInt128(), feesOwed1.toInt128()); + feeDelta = toBalanceDelta(-feesOwed0.toInt128(), -feesOwed1.toInt128()); } // the top level state of the swap, the results of which are recorded in storage at the end @@ -192,18 +192,19 @@ library CLPool { bool zeroForOne; int256 amountSpecified; uint160 sqrtPriceLimitX96; + uint24 lpFeeOverride; } function swap(State storage self, SwapParams memory params) internal returns (BalanceDelta balanceDelta, SwapState memory state) { - if (params.amountSpecified == 0) revert SwapAmountCannotBeZero(); - + // cache variables for gas optimization Slot0 memory slot0Start = self.slot0; - // Declare zeroForOne and sqrtPriceLimitX96 upfront for gas optmization bool zeroForOne = params.zeroForOne; uint160 sqrtPriceLimitX96 = params.sqrtPriceLimitX96; + + // check price limit if ( zeroForOne ? (sqrtPriceLimitX96 >= slot0Start.sqrtPriceX96 || sqrtPriceLimitX96 <= TickMath.MIN_SQRT_RATIO) @@ -212,20 +213,26 @@ library CLPool { revert InvalidSqrtPriceLimit(slot0Start.sqrtPriceX96, sqrtPriceLimitX96); } + // cache variables for gas optimization // liquidity at the beginning of the swap uint128 liquidityStart = self.liquidity; bool exactInput = params.amountSpecified > 0; + // init swap state { uint16 protocolFee = zeroForOne ? slot0Start.protocolFee.getZeroForOneFee() : slot0Start.protocolFee.getOneForZeroFee(); + uint24 lpFee = params.lpFeeOverride.isOverride() + ? params.lpFeeOverride.removeOverrideAndValidate(LPFeeLibrary.ONE_HUNDRED_PERCENT_FEE) + : slot0Start.lpFee; + state = SwapState({ amountSpecifiedRemaining: params.amountSpecified, amountCalculated: 0, sqrtPriceX96: slot0Start.sqrtPriceX96, tick: slot0Start.tick, - swapFee: protocolFee == 0 ? slot0Start.lpFee : uint24(protocolFee).calculateSwapFee(slot0Start.lpFee), + swapFee: protocolFee == 0 ? lpFee : uint24(protocolFee).calculateSwapFee(lpFee), protocolFee: protocolFee, feeGrowthGlobalX128: zeroForOne ? self.feeGrowthGlobal0X128 : self.feeGrowthGlobal1X128, feeForProtocol: 0, @@ -239,6 +246,9 @@ library CLPool { revert InvalidFeeForExactOut(); } + /// @notice early return if hook has updated amountSpecified to 0 + if (params.amountSpecified == 0) return (BalanceDeltaLibrary.ZERO_DELTA, state); + StepComputations memory step; // continue swapping as long as we haven't used the entire input/output and haven't reached the price limit while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) { @@ -374,51 +384,52 @@ library CLPool { internal returns (uint256, uint256) { - uint256 _feeGrowthGlobal0X128 = self.feeGrowthGlobal0X128; // SLOAD for gas optimization - uint256 _feeGrowthGlobal1X128 = self.feeGrowthGlobal1X128; // SLOAD for gas optimization - //@dev avoid stack too deep UpdatePositionCache memory cache; + { + uint256 _feeGrowthGlobal0X128 = self.feeGrowthGlobal0X128; // SLOAD for gas optimization + uint256 _feeGrowthGlobal1X128 = self.feeGrowthGlobal1X128; // SLOAD for gas optimization + + ///@dev update ticks if nencessary + if (params.liquidityDelta != 0) { + cache.maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick(params.tickSpacing); + cache.flippedLower = self.ticks.update( + params.tickLower, + tick, + params.liquidityDelta, + _feeGrowthGlobal0X128, + _feeGrowthGlobal1X128, + false, + cache.maxLiquidityPerTick + ); + cache.flippedUpper = self.ticks.update( + params.tickUpper, + tick, + params.liquidityDelta, + _feeGrowthGlobal0X128, + _feeGrowthGlobal1X128, + true, + cache.maxLiquidityPerTick + ); - ///@dev update ticks if nencessary - if (params.liquidityDelta != 0) { - cache.maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick(params.tickSpacing); - cache.flippedLower = self.ticks.update( - params.tickLower, - tick, - params.liquidityDelta, - _feeGrowthGlobal0X128, - _feeGrowthGlobal1X128, - false, - cache.maxLiquidityPerTick - ); - cache.flippedUpper = self.ticks.update( - params.tickUpper, - tick, - params.liquidityDelta, - _feeGrowthGlobal0X128, - _feeGrowthGlobal1X128, - true, - cache.maxLiquidityPerTick - ); - - if (cache.flippedLower) { - self.tickBitmap.flipTick(params.tickLower, params.tickSpacing); - } - if (cache.flippedUpper) { - self.tickBitmap.flipTick(params.tickUpper, params.tickSpacing); + if (cache.flippedLower) { + self.tickBitmap.flipTick(params.tickLower, params.tickSpacing); + } + if (cache.flippedUpper) { + self.tickBitmap.flipTick(params.tickUpper, params.tickSpacing); + } } - } - (cache.feeGrowthInside0X128, cache.feeGrowthInside1X128) = self.ticks.getFeeGrowthInside( - params.tickLower, params.tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128 - ); + (cache.feeGrowthInside0X128, cache.feeGrowthInside1X128) = self.ticks.getFeeGrowthInside( + params.tickLower, params.tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128 + ); + } ///@dev update user position and collect fees /// must be done after ticks are updated in case of a 0 -> 1 flip - (cache.feesOwed0, cache.feesOwed1) = self.positions.get(params.owner, params.tickLower, params.tickUpper).update( - params.liquidityDelta, cache.feeGrowthInside0X128, cache.feeGrowthInside1X128 - ); + (cache.feesOwed0, cache.feesOwed1) = self.positions.get( + params.owner, params.tickLower, params.tickUpper, params.salt + ).update(params.liquidityDelta, cache.feeGrowthInside0X128, cache.feeGrowthInside1X128); ///@dev clear any tick data that is no longer needed /// must be done after fee collection in case of a 1 -> 0 flip diff --git a/src/pool-cl/libraries/CLPosition.sol b/src/pool-cl/libraries/CLPosition.sol index b1538021..b511841a 100644 --- a/src/pool-cl/libraries/CLPosition.sol +++ b/src/pool-cl/libraries/CLPosition.sol @@ -27,8 +27,9 @@ library CLPosition { /// @param owner The address of the position owner /// @param tickLower The lower tick boundary of the position /// @param tickUpper The upper tick boundary of the position + /// @param salt A unique value to differentiate between multiple positions in the same range /// @return position The position info struct of the given owners' position - function get(mapping(bytes32 => Info) storage self, address owner, int24 tickLower, int24 tickUpper) + function get(mapping(bytes32 => Info) storage self, address owner, int24 tickLower, int24 tickUpper, bytes32 salt) internal view returns (Info storage position) @@ -37,10 +38,14 @@ library CLPosition { // make use of memory scratch space // ref: https://github.com/Vectorized/solady/blob/main/src/tokens/ERC20.sol#L95 assembly { + mstore(0x26, salt) mstore(0x06, tickUpper) mstore(0x03, tickLower) mstore(0x00, owner) - key := keccak256(0x0c, 0x1a) + key := keccak256(0x0c, 0x3a) + // 0x00 - 0x3f is scratch space + // 0x40 ~ 0x46 should be clear to avoid polluting free pointer + mstore(0x26, 0) } position = self[key]; } diff --git a/src/test/pool-bin/MockBinHooks.sol b/src/test/pool-bin/MockBinHooks.sol index f7aab641..25d9ad8b 100644 --- a/src/test/pool-bin/MockBinHooks.sol +++ b/src/test/pool-bin/MockBinHooks.sol @@ -5,7 +5,8 @@ import {Hooks} from "../../libraries/Hooks.sol"; import {IBinHooks} from "../../pool-bin/interfaces/IBinHooks.sol"; import {IBinPoolManager} from "../../pool-bin/interfaces/IBinPoolManager.sol"; import {PoolKey} from "../../types/PoolKey.sol"; -import {BalanceDelta} from "../../types/BalanceDelta.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "../../types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../../types/BeforeSwapDelta.sol"; import {PoolId, PoolIdLibrary} from "../../types/PoolId.sol"; contract MockBinHooks is IBinHooks { @@ -67,10 +68,10 @@ contract MockBinHooks is IBinHooks { IBinPoolManager.MintParams calldata, BalanceDelta, bytes calldata hookData - ) external override returns (bytes4) { + ) external override returns (bytes4, BalanceDelta) { afterMintData = hookData; bytes4 selector = MockBinHooks.afterMint.selector; - return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; + return (returnValues[selector] == bytes4(0) ? selector : returnValues[selector], BalanceDeltaLibrary.ZERO_DELTA); } function beforeBurn(address, PoolKey calldata, IBinPoolManager.BurnParams calldata, bytes calldata hookData) @@ -89,30 +90,34 @@ contract MockBinHooks is IBinHooks { IBinPoolManager.BurnParams calldata, BalanceDelta, bytes calldata hookData - ) external override returns (bytes4) { + ) external override returns (bytes4, BalanceDelta) { afterBurnData = hookData; bytes4 selector = MockBinHooks.afterBurn.selector; - return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; + return (returnValues[selector] == bytes4(0) ? selector : returnValues[selector], BalanceDeltaLibrary.ZERO_DELTA); } function beforeSwap(address, PoolKey calldata, bool, uint128, bytes calldata hookData) external override - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { beforeSwapData = hookData; bytes4 selector = MockBinHooks.beforeSwap.selector; - return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; + return ( + returnValues[selector] == bytes4(0) ? selector : returnValues[selector], + BeforeSwapDeltaLibrary.ZERO_DELTA, + 0 + ); } function afterSwap(address, PoolKey calldata, bool, uint128, BalanceDelta, bytes calldata hookData) external override - returns (bytes4) + returns (bytes4, int128) { afterSwapData = hookData; bytes4 selector = MockBinHooks.afterSwap.selector; - return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; + return (returnValues[selector] == bytes4(0) ? selector : returnValues[selector], 0); } function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata hookData) diff --git a/src/types/BalanceDelta.sol b/src/types/BalanceDelta.sol index 32c5918c..6e837712 100644 --- a/src/types/BalanceDelta.sol +++ b/src/types/BalanceDelta.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; type BalanceDelta is int256; -using {add as +, sub as -, eq as ==} for BalanceDelta global; +using {add as +, sub as -, eq as ==, neq as !=} for BalanceDelta global; using BalanceDeltaLibrary for BalanceDelta global; function toBalanceDelta(int128 _amount0, int128 _amount1) pure returns (BalanceDelta balanceDelta) { @@ -26,10 +26,11 @@ function eq(BalanceDelta a, BalanceDelta b) pure returns (bool) { return a.amount0() == b.amount0() && a.amount1() == b.amount1(); } -library BalanceDeltaLibrary { - // Sentinel return value used to signify that a NoOp occurred. - BalanceDelta public constant MAXIMUM_DELTA = BalanceDelta.wrap(int256(type(uint256).max)); +function neq(BalanceDelta a, BalanceDelta b) pure returns (bool) { + return a.amount0() != b.amount0() || a.amount1() != b.amount1(); +} +library BalanceDeltaLibrary { // Sentinel return value used for feeDelta to signify that a NoOp occurred. BalanceDelta public constant ZERO_DELTA = BalanceDelta.wrap(0); diff --git a/src/types/BeforeSwapDelta.sol b/src/types/BeforeSwapDelta.sol new file mode 100644 index 00000000..d167b5ae --- /dev/null +++ b/src/types/BeforeSwapDelta.sol @@ -0,0 +1,40 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {BalanceDelta} from "./BalanceDelta.sol"; + +// Return type of the beforeSwap hook. +// Upper 128 bits is the delta in specified tokens. Lower 128 bits is delta in unspecified tokens (to match the afterSwap hook) +type BeforeSwapDelta is int256; + +// Creates a BeforeSwapDelta from specified and unspecified +function toBeforeSwapDelta(int128 deltaSpecified, int128 deltaUnspecified) + pure + returns (BeforeSwapDelta beforeSwapDelta) +{ + /// @solidity memory-safe-assembly + assembly { + beforeSwapDelta := or(shl(128, deltaSpecified), and(sub(shl(128, 1), 1), deltaUnspecified)) + } +} + +library BeforeSwapDeltaLibrary { + BeforeSwapDelta public constant ZERO_DELTA = BeforeSwapDelta.wrap(0); + + /// extracts int128 from the upper 128 bits of the BeforeSwapDelta + /// returned by beforeSwap + function getSpecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128 deltaSpecified) { + assembly { + deltaSpecified := sar(128, delta) + } + } + + /// extracts int128 from the lower 128 bits of the BeforeSwapDelta + /// returned by beforeSwap and afterSwap + function getUnspecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128 deltaUnspecified) { + /// @solidity memory-safe-assembly + assembly { + deltaUnspecified := signextend(15, delta) + } + } +} diff --git a/test/libraries/Hooks/Hooks.t.sol b/test/libraries/Hooks/Hooks.t.sol index 25bece44..2133d1a0 100644 --- a/test/libraries/Hooks/Hooks.t.sol +++ b/test/libraries/Hooks/Hooks.t.sol @@ -90,21 +90,4 @@ contract HooksTest is Test { assertEq(Hooks.hasOffsetEnabled(bytes32(uint256(0xaaaa)), 14), false); assertEq(Hooks.hasOffsetEnabled(bytes32(uint256(0xaaaa)), 15), true); } - - function testIsValidNoOpCall(bytes32 parameters, uint8 noOpOffset, bytes4 selector) public { - // make sure enough true cases are covered - noOpOffset = uint8(bound(noOpOffset, 0, 15)); - if (uint32(selector) > type(uint32).max / 2) { - selector = Hooks.NO_OP_SELECTOR; - } - - bool expectRet; - assembly { - expectRet := and(shr(noOpOffset, parameters), 1) - } - expectRet = expectRet && selector == Hooks.NO_OP_SELECTOR; - - bool actualRet = Hooks.isValidNoOpCall(parameters, noOpOffset, selector); - assertEq(expectRet, actualRet); - } } diff --git a/test/libraries/LPFeeLibrary.t.sol b/test/libraries/LPFeeLibrary.t.sol index dd1acff1..04ec1f01 100644 --- a/test/libraries/LPFeeLibrary.t.sol +++ b/test/libraries/LPFeeLibrary.t.sol @@ -25,7 +25,7 @@ contract LPFeeLibraryTest is Test { assertEq(LPFeeLibrary.isDynamicLPFee(0xFFFFFF), true); // 0111 1111 1111 1111 1111 1111 - assertEq(LPFeeLibrary.isDynamicLPFee(0x7FFFF), false); + assertEq(LPFeeLibrary.isDynamicLPFee(0x7FFFFF), false); } function testGetInitialLPFee() public { @@ -34,12 +34,14 @@ contract LPFeeLibraryTest is Test { assertEq(LPFeeLibrary.getInitialLPFee(0x000002), 0x000002); assertEq(LPFeeLibrary.getInitialLPFee(0x0F0003), 0x0F0003); assertEq(LPFeeLibrary.getInitialLPFee(0x001004), 0x001004); - assertEq(LPFeeLibrary.getInitialLPFee(0x111020), 0x011020); - assertEq(LPFeeLibrary.getInitialLPFee(0x101020), 0x001020); + assertEq(LPFeeLibrary.getInitialLPFee(0x111020), 0x111020); + assertEq(LPFeeLibrary.getInitialLPFee(0x511020), 0x511020); // dynamic assertEq(LPFeeLibrary.getInitialLPFee(0xF00F05), 0); assertEq(LPFeeLibrary.getInitialLPFee(0x800310), 0); + assertEq(LPFeeLibrary.getInitialLPFee(0x800000), 0); + assertEq(LPFeeLibrary.getInitialLPFee(0x901020), 0); } function testFuzzValidate(uint24 self, uint24 maxFee) public { @@ -48,4 +50,36 @@ contract LPFeeLibraryTest is Test { } LPFeeLibrary.validate(self, maxFee); } + + function testIsOverride() public { + // 1000 0000 0000 0000 0000 0000 + assertEq(LPFeeLibrary.isOverride(0x800000), false); + + // 0100 0000 0000 0000 0000 0000 + assertEq(LPFeeLibrary.isOverride(0x400000), true); + + // 0010 0000 0000 0000 0000 0000 + assertEq(LPFeeLibrary.isOverride(0x200000), false); + + // 0001 0000 0000 0000 0000 0000 + assertEq(LPFeeLibrary.isOverride(0x100000), false); + + // 1111 1111 1111 1111 1111 1111 + assertEq(LPFeeLibrary.isOverride(0xFFFFFF), true); + + // 0111 1111 1111 1111 1111 1111 + assertEq(LPFeeLibrary.isOverride(0x7FFFFF), true); + + // 1011 1111 1111 1111 1111 1111 + assertEq(LPFeeLibrary.isOverride(0xBFFFFF), false); + } + + function testFuzzRemoveOverrideAndValidate(uint24 self, uint24 maxFee) public { + if ((self & 0xBFFFFF) > maxFee) { + vm.expectRevert(LPFeeLibrary.FeeTooLarge.selector); + } + + uint24 fee = self.removeOverrideAndValidate(maxFee); + assertEq(fee, self & 0xBFFFFF); + } } diff --git a/test/pool-bin/BinHook.t.sol b/test/pool-bin/BinHook.t.sol index f434e18b..1dcecd05 100644 --- a/test/pool-bin/BinHook.t.sol +++ b/test/pool-bin/BinHook.t.sol @@ -123,7 +123,7 @@ contract BinHookTest is BinTestHelper, GasSnapshot { poolManager.initialize(key, binId, ""); addLiquidityToBin(key, poolManager, bob, binId, 1e18, 1e18, 1e18, 1e18, new bytes(123)); - uint256 bobBal = poolManager.getPosition(key.toId(), bob, binId).share; + uint256 bobBal = poolManager.getPosition(key.toId(), bob, binId, 0).share; snapStart("BinHookTest#testBurnSucceedsWithHook"); removeLiquidityFromBin(key, poolManager, bob, binId, bobBal, new bytes(456)); @@ -144,7 +144,7 @@ contract BinHookTest is BinTestHelper, GasSnapshot { poolManager.initialize(key, binId, ""); addLiquidityToBin(key, poolManager, bob, binId, 1e18, 1e18, 1e18, 1e18, ""); - uint256 bobBal = poolManager.getPosition(key.toId(), bob, binId).share; + uint256 bobBal = poolManager.getPosition(key.toId(), bob, binId, 0).share; vm.expectRevert(Hooks.InvalidHookResponse.selector); removeLiquidityFromBin(key, poolManager, bob, binId, bobBal, ""); } @@ -160,7 +160,7 @@ contract BinHookTest is BinTestHelper, GasSnapshot { poolManager.initialize(key, binId, ""); addLiquidityToBin(key, poolManager, bob, binId, 1e18, 1e18, 1e18, 1e18, ""); - uint256 bobBal = poolManager.getPosition(key.toId(), bob, binId).share; + uint256 bobBal = poolManager.getPosition(key.toId(), bob, binId, 0).share; vm.expectRevert(Hooks.InvalidHookResponse.selector); removeLiquidityFromBin(key, poolManager, bob, binId, bobBal, ""); } diff --git a/test/pool-bin/BinHookReturnsDelta.t.sol b/test/pool-bin/BinHookReturnsDelta.t.sol new file mode 100644 index 00000000..063bc1f1 --- /dev/null +++ b/test/pool-bin/BinHookReturnsDelta.t.sol @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {IVault} from "../../src/interfaces/IVault.sol"; +import {IPoolManager} from "../../src/interfaces/IPoolManager.sol"; +import {IBinPoolManager} from "../../src/pool-bin/interfaces/IBinPoolManager.sol"; +import {Vault} from "../../src/Vault.sol"; +import {Currency} from "../../src/types/Currency.sol"; +import {PoolKey} from "../../src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "../../src/types/PoolId.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "../../src/types/BalanceDelta.sol"; +import {BinPoolManager} from "../../src/pool-bin/BinPoolManager.sol"; +import {BinPool} from "../../src/pool-bin/libraries/BinPool.sol"; +import {PackedUint128Math} from "../../src/pool-bin/libraries/math/PackedUint128Math.sol"; +import {SafeCast} from "../../src/pool-bin/libraries/math/SafeCast.sol"; +import {BinPoolParametersHelper} from "../../src/pool-bin/libraries/BinPoolParametersHelper.sol"; +import {Constants} from "../../src/pool-bin/libraries/Constants.sol"; +import {IBinHooks} from "../../src/pool-bin/interfaces/IBinHooks.sol"; +import {BinFeeManagerHook} from "./helpers/BinFeeManagerHook.sol"; +import {IHooks} from "../../src/interfaces/IHooks.sol"; +import {IBinHooks} from "../../src/pool-bin/interfaces/IBinHooks.sol"; +import {BinSwapHelper} from "./helpers/BinSwapHelper.sol"; +import {BinLiquidityHelper} from "./helpers/BinLiquidityHelper.sol"; +import {BinDonateHelper} from "./helpers/BinDonateHelper.sol"; +import {BinTestHelper} from "./helpers/BinTestHelper.sol"; +import {Hooks} from "../../src/libraries/Hooks.sol"; +import {BinReturnsDeltaHook} from "./helpers/BinReturnsDeltaHook.sol"; + +contract BinHookReturnsDelta is Test, GasSnapshot, BinTestHelper { + using PoolIdLibrary for PoolKey; + using SafeCast for uint256; + using PackedUint128Math for bytes32; + using PackedUint128Math for uint128; + using BinPoolParametersHelper for bytes32; + + Vault public vault; + BinPoolManager public poolManager; + BinReturnsDeltaHook public binReturnsDeltaHook; + + BinSwapHelper public binSwapHelper; + BinLiquidityHelper public binLiquidityHelper; + BinDonateHelper public binDonateHelper; + + uint24 activeId = 2 ** 23; // where token0 and token1 price is the same + + PoolKey key; + bytes32 poolParam; + MockERC20 token0; + MockERC20 token1; + Currency currency0; + Currency currency1; + + function setUp() public { + vault = new Vault(); + poolManager = new BinPoolManager(IVault(address(vault)), 500000); + + vault.registerPoolManager(address(poolManager)); + + token0 = new MockERC20("TestA", "A", 18); + token1 = new MockERC20("TestB", "B", 18); + (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); + currency0 = Currency.wrap(address(token0)); + currency1 = Currency.wrap(address(token1)); + + token0.mint(address(this), 1000 ether); + token1.mint(address(this), 1000 ether); + + IBinPoolManager iBinPoolManager = IBinPoolManager(address(poolManager)); + IVault iVault = IVault(address(vault)); + + binSwapHelper = new BinSwapHelper(iBinPoolManager, iVault); + binLiquidityHelper = new BinLiquidityHelper(iBinPoolManager, iVault); + binDonateHelper = new BinDonateHelper(iBinPoolManager, iVault); + token0.approve(address(binSwapHelper), 1000 ether); + token1.approve(address(binSwapHelper), 1000 ether); + token0.approve(address(binLiquidityHelper), 1000 ether); + token1.approve(address(binLiquidityHelper), 1000 ether); + token0.approve(address(binDonateHelper), 1000 ether); + token1.approve(address(binDonateHelper), 1000 ether); + + binReturnsDeltaHook = new BinReturnsDeltaHook(iVault, iBinPoolManager); + token0.approve(address(binReturnsDeltaHook), 1000 ether); + token1.approve(address(binReturnsDeltaHook), 1000 ether); + + key = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: binReturnsDeltaHook, + poolManager: IPoolManager(address(poolManager)), + fee: uint24(3000), // 3000 = 0.3% + parameters: bytes32(uint256(binReturnsDeltaHook.getHooksRegistrationBitmap())).setBinStep(10) + }); + + poolManager.initialize(key, activeId, new bytes(0)); + } + + function testMint_MintMore() external { + IBinPoolManager.MintParams memory mintParams = _getSingleBinMintParams(activeId, 1 ether, 1 ether); + BalanceDelta delta = binLiquidityHelper.mint(key, mintParams, abi.encode(0)); + (uint128 reserveXBefore, uint128 reserveYBefore) = poolManager.getBin(key.toId(), activeId); + + BalanceDelta delta2 = binLiquidityHelper.mint(key, mintParams, abi.encode(mintParams.amountIn)); + (uint128 reserveXAfter, uint128 reserveYAfter) = poolManager.getBin(key.toId(), activeId); + + assertEq(reserveXAfter - reserveXBefore, 2 * reserveXBefore); + assertEq(reserveYAfter - reserveYBefore, 2 * reserveYBefore); + + assertEq(delta.amount0() * 2, delta2.amount0()); + assertEq(delta.amount1() * 2, delta2.amount1()); + } + + function testBurn_FeeCharge() external { + IBinPoolManager.MintParams memory mintParams = _getSingleBinMintParams(activeId, 1 ether, 1 ether); + binLiquidityHelper.mint(key, mintParams, abi.encode(0)); + + (uint128 reserveXBefore, uint128 reserveYBefore) = poolManager.getBin(key.toId(), activeId); + + assertEq(reserveXBefore, 1 ether); + assertEq(reserveYBefore, 1 ether); + assertEq(token0.balanceOf(address(binReturnsDeltaHook)), 0); + assertEq(token1.balanceOf(address(binReturnsDeltaHook)), 0); + + IBinPoolManager.BurnParams memory burnParams = + _getSingleBinBurnLiquidityParams(key, poolManager, activeId, address(binLiquidityHelper), 100); + + binLiquidityHelper.burn(key, burnParams, ""); + + (uint128 reserveXAfter, uint128 reserveYAfter) = poolManager.getBin(key.toId(), activeId); + + assertEq(reserveXAfter, 0); + assertEq(reserveYAfter, 0); + assertEq(token0.balanceOf(address(binReturnsDeltaHook)), 0.1 ether); + assertEq(token1.balanceOf(address(binReturnsDeltaHook)), 0.1 ether); + } + + function testSwap_noSwap() external { + IBinPoolManager.MintParams memory mintParams = _getSingleBinMintParams(activeId, 10 ether, 10 ether); + binLiquidityHelper.mint(key, mintParams, abi.encode(0)); + + uint256 amt0Before = token0.balanceOf(address(vault)); + uint256 amt1Before = token1.balanceOf(address(vault)); + + BalanceDelta delta = + binSwapHelper.swap(key, true, 1 ether, BinSwapHelper.TestSettings(true, true), abi.encode(-1 ether, 0, 0)); + + uint256 amt0After = token0.balanceOf(address(vault)); + uint256 amt1After = token1.balanceOf(address(vault)); + + assertEq(amt0After - amt0Before, 0); + assertEq(amt1After - amt1Before, 0); + + // user pays 1 ether of currency0 to hook and no swap happens + + // trader's payment & return + assertEq(delta.amount0(), 1 ether); + assertEq(delta.amount1(), 0); + + // hook's payment & return + assertEq(token0.balanceOf(address(binReturnsDeltaHook)), 1 ether); + } + + function testSwap_noSwap_returnUnspecifiedInBeforeSwap() external { + IBinPoolManager.MintParams memory mintParams = _getSingleBinMintParams(activeId, 10 ether, 10 ether); + binLiquidityHelper.mint(key, mintParams, abi.encode(0)); + + token1.transfer(address(binReturnsDeltaHook), 1 ether); + + uint256 amt0Before = token0.balanceOf(address(vault)); + uint256 amt1Before = token1.balanceOf(address(vault)); + + BalanceDelta delta = binSwapHelper.swap( + key, true, 1 ether, BinSwapHelper.TestSettings(true, true), abi.encode(-1 ether, 1 ether, 0) + ); + + uint256 amt0After = token0.balanceOf(address(vault)); + uint256 amt1After = token1.balanceOf(address(vault)); + + assertEq(amt0After - amt0Before, 0); + assertEq(amt1After - amt1Before, 0); + + // user pays 1 ether of currency0 to hook and no swap happens + + // trader's payment & return + assertEq(delta.amount0(), 1 ether); + assertEq(delta.amount1(), -1 ether); + + // hook's payment & return + assertEq(token0.balanceOf(address(binReturnsDeltaHook)), 1 ether); + assertEq(token1.balanceOf(address(binReturnsDeltaHook)), 0 ether); + } + + function testSwap_noSwap_returnUnspecifiedInBeforeSwapAndAfterSwap() external { + IBinPoolManager.MintParams memory mintParams = _getSingleBinMintParams(activeId, 10 ether, 10 ether); + binLiquidityHelper.mint(key, mintParams, abi.encode(0)); + + token1.transfer(address(binReturnsDeltaHook), 1 ether); + + uint256 amt0Before = token0.balanceOf(address(vault)); + uint256 amt1Before = token1.balanceOf(address(vault)); + + BalanceDelta delta = binSwapHelper.swap( + key, true, 1 ether, BinSwapHelper.TestSettings(true, true), abi.encode(-1 ether, 0.5 ether, 0.5 ether) + ); + + uint256 amt0After = token0.balanceOf(address(vault)); + uint256 amt1After = token1.balanceOf(address(vault)); + + assertEq(amt0After - amt0Before, 0); + assertEq(amt1After - amt1Before, 0); + + // user pays 1 ether of currency0 to hook and no swap happens + + // trader's payment & return + assertEq(delta.amount0(), 1 ether); + assertEq(delta.amount1(), -1 ether); + + // hook's payment & return + assertEq(token0.balanceOf(address(binReturnsDeltaHook)), 1 ether); + assertEq(token1.balanceOf(address(binReturnsDeltaHook)), 0 ether); + } + + function testSwap_swapMore() external { + IBinPoolManager.MintParams memory mintParams = _getSingleBinMintParams(activeId, 10 ether, 10 ether); + binLiquidityHelper.mint(key, mintParams, abi.encode(0)); + + uint256 amt0Before = token0.balanceOf(address(vault)); + uint256 amt1Before = token1.balanceOf(address(vault)); + + token0.transfer(address(binReturnsDeltaHook), 1 ether); + + BalanceDelta delta = + binSwapHelper.swap(key, true, 1 ether, BinSwapHelper.TestSettings(true, true), abi.encode(1 ether, 0, 0)); + + uint256 amt0After = token0.balanceOf(address(vault)); + uint256 amt1After = token1.balanceOf(address(vault)); + + assertEq(amt0After - amt0Before, 2 ether); + assertEq(amt1Before - amt1After, 2 ether * 997 / 1000); + + // user pays 1 ether of currency0 to hook and no swap happens + + // trader's payment & return + assertEq(delta.amount0(), 1 ether); + assertEq(delta.amount1(), -2 ether * 997 / 1000); + + // hook's payment & return + assertEq(token0.balanceOf(address(binReturnsDeltaHook)), 0 ether); + } + + function testSwap_swapLess() external { + IBinPoolManager.MintParams memory mintParams = _getSingleBinMintParams(activeId, 10 ether, 10 ether); + binLiquidityHelper.mint(key, mintParams, abi.encode(0)); + + uint256 amt0Before = token0.balanceOf(address(vault)); + uint256 amt1Before = token1.balanceOf(address(vault)); + + BalanceDelta delta = + binSwapHelper.swap(key, true, 1 ether, BinSwapHelper.TestSettings(true, true), abi.encode(-0.5 ether, 0, 0)); + + uint256 amt0After = token0.balanceOf(address(vault)); + uint256 amt1After = token1.balanceOf(address(vault)); + + assertEq(amt0After - amt0Before, 0.5 ether); + assertEq(amt1Before - amt1After, 0.5 ether * 997 / 1000); + + // user pays 1 ether of currency0 to hook and no swap happens + + // trader's payment & return + assertEq(delta.amount0(), 1 ether); + assertEq(delta.amount1(), -0.5 ether * 997 / 1000); + + // hook's payment & return + assertEq(token0.balanceOf(address(binReturnsDeltaHook)), 0.5 ether); + } + + receive() external payable {} +} diff --git a/test/pool-bin/BinHookReturnsFee.t.sol b/test/pool-bin/BinHookReturnsFee.t.sol new file mode 100644 index 00000000..510a6a56 --- /dev/null +++ b/test/pool-bin/BinHookReturnsFee.t.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Vault} from "../../src/Vault.sol"; +import {IVault} from "../../src/interfaces/IVault.sol"; +import {PoolId, PoolIdLibrary} from "../../src/types/PoolId.sol"; +import {Hooks} from "../../src/libraries/Hooks.sol"; +import {LPFeeLibrary} from "../../src/libraries/LPFeeLibrary.sol"; +import {IProtocolFees} from "../../src/interfaces/IProtocolFees.sol"; +import {IBinHooks} from "../../src/pool-bin/interfaces/IBinHooks.sol"; +import {PoolKey} from "../../src/types/PoolKey.sol"; +import {BinPoolManager} from "../../src/pool-bin/BinPoolManager.sol"; +import {IBinPoolManager} from "../../src/pool-bin/interfaces/IBinPoolManager.sol"; +import {BinDynamicReturnsFeeHook} from "./helpers/BinDynamicReturnsFeeHook.sol"; +import {Currency, CurrencyLibrary} from "../../src/types/Currency.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {FullMath} from "../../src/pool-cl/libraries/FullMath.sol"; +import {BalanceDelta} from "../../src/types/BalanceDelta.sol"; +import {BinTestHelper} from "./helpers/BinTestHelper.sol"; +import {BinLiquidityHelper} from "./helpers/BinLiquidityHelper.sol"; +import {BinSwapHelper} from "./helpers/BinSwapHelper.sol"; +import {BinPoolParametersHelper} from "../../src/pool-bin/libraries/BinPoolParametersHelper.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract BinHookReturnsFeeTest is Test, BinTestHelper { + using PoolIdLibrary for PoolKey; + using LPFeeLibrary for uint24; + using BinPoolParametersHelper for bytes32; + + Vault public vault; + BinPoolManager public poolManager; + BinDynamicReturnsFeeHook dynamicReturnsFeesHook; + + BinSwapHelper public binSwapHelper; + BinLiquidityHelper public binLiquidityHelper; + + uint24 activeId = 2 ** 23; // where token0 and token1 price is the same + + PoolKey key; + bytes32 poolParam; + MockERC20 token0; + MockERC20 token1; + Currency currency0; + Currency currency1; + + event Swap( + PoolId indexed poolId, + address sender, + int128 amount0, + int128 amount1, + uint160 sqrtPriceX96, + uint128 liquidity, + int24 tick, + uint24 fee + ); + + function setUp() public { + vault = new Vault(); + poolManager = new BinPoolManager(IVault(address(vault)), 500000); + vault.registerPoolManager(address(poolManager)); + + // initializeTokens + token0 = new MockERC20("TestA", "A", 18); + token1 = new MockERC20("TestB", "B", 18); + (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); + currency0 = Currency.wrap(address(token0)); + currency1 = Currency.wrap(address(token1)); + + token0.mint(address(this), 1000 ether); + token1.mint(address(this), 1000 ether); + + dynamicReturnsFeesHook = new BinDynamicReturnsFeeHook(); + dynamicReturnsFeesHook.setManager(poolManager); + + binSwapHelper = new BinSwapHelper(poolManager, vault); + binLiquidityHelper = new BinLiquidityHelper(poolManager, vault); + token0.approve(address(binSwapHelper), 1000 ether); + token1.approve(address(binSwapHelper), 1000 ether); + token0.approve(address(binLiquidityHelper), 1000 ether); + token1.approve(address(binLiquidityHelper), 1000 ether); + + token0.approve(address(dynamicReturnsFeesHook), 1000 ether); + token1.approve(address(dynamicReturnsFeesHook), 1000 ether); + + key = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: dynamicReturnsFeesHook, + poolManager: poolManager, + fee: LPFeeLibrary.DYNAMIC_FEE_FLAG, // 3000 = 0.3% + parameters: bytes32(uint256(dynamicReturnsFeesHook.getHooksRegistrationBitmap())).setBinStep(10) + }); + + poolManager.initialize(key, activeId, new bytes(0)); + + IBinPoolManager.MintParams memory mintParams = _getSingleBinMintParams(activeId, 1 ether, 1 ether); + binLiquidityHelper.mint(key, mintParams, abi.encode(0)); + } + + function test_fuzz_dynamicReturnSwapFee(uint24 fee) public { + // hook will handle adding the override flag + dynamicReturnsFeesHook.setFee(fee); + + uint24 actualFee = fee.removeOverrideFlag(); + + uint128 amountSpecified = 10000; + BalanceDelta result; + if (actualFee > LPFeeLibrary.TEN_PERCENT_FEE) { + vm.expectRevert(LPFeeLibrary.FeeTooLarge.selector); + result = + binSwapHelper.swap(key, true, amountSpecified, BinSwapHelper.TestSettings(true, true), new bytes(0)); + return; + } else { + result = + binSwapHelper.swap(key, true, amountSpecified, BinSwapHelper.TestSettings(true, true), new bytes(0)); + } + assertEq(result.amount0(), int128(amountSpecified)); + + assertApproxEqAbs( + uint256(-int256(result.amount1())), FullMath.mulDiv(uint256(amountSpecified), (1e6 - actualFee), 1e6), 1 wei + ); + } + + function test_dynamicReturnSwapFee_initializeZeroSwapFee() public { + key.parameters = + BinPoolParametersHelper.setBinStep(bytes32(uint256(dynamicReturnsFeesHook.getHooksRegistrationBitmap())), 1); + poolManager.initialize(key, activeId, new bytes(0)); + assertEq(_fetchPoolSwapFee(key), 0); + } + + function test_dynamicReturnSwapFee_notUsedIfPoolIsStaticFee() public { + key.fee = 3000; // static fee + dynamicReturnsFeesHook.setFee(1000); // 0.10% fee is NOT used because the pool has a static fee + + poolManager.initialize(key, activeId, new bytes(0)); + IBinPoolManager.MintParams memory mintParams = _getSingleBinMintParams(activeId, 1 ether, 1 ether); + binLiquidityHelper.mint(key, mintParams, abi.encode(0)); + assertEq(_fetchPoolSwapFee(key), 3000); + + // despite returning a valid swap fee (1000), the static fee is used + uint128 amountSpecified = 10000; + BalanceDelta result; + result = binSwapHelper.swap(key, true, amountSpecified, BinSwapHelper.TestSettings(true, true), new bytes(0)); + + // after swapping ~1:1, the amount out (amount1) should be approximately 0.30% less than the amount specified + assertEq(result.amount0(), int128(amountSpecified)); + assertApproxEqAbs( + uint256(-int256(result.amount1())), FullMath.mulDiv(uint256(amountSpecified), (1e6 - 3000), 1e6), 1 wei + ); + } + + function test_dynamicReturnSwapFee_notStored() public { + // fees returned by beforeSwap are not written to storage + + // create a new pool with an initial fee of 123 + key.parameters = + BinPoolParametersHelper.setBinStep(bytes32(uint256(dynamicReturnsFeesHook.getHooksRegistrationBitmap())), 1); + poolManager.initialize(key, activeId, new bytes(0)); + IBinPoolManager.MintParams memory mintParams = _getSingleBinMintParams(activeId, 1 ether, 1 ether); + binLiquidityHelper.mint(key, mintParams, abi.encode(0)); + + uint24 initialFee = 123; + dynamicReturnsFeesHook.forcePoolFeeUpdate(key, initialFee); + assertEq(_fetchPoolSwapFee(key), initialFee); + + // swap with a different fee + uint24 newFee = 3000; + dynamicReturnsFeesHook.setFee(newFee); + + uint128 amountSpecified = 10000; + BalanceDelta result = + binSwapHelper.swap(key, true, amountSpecified, BinSwapHelper.TestSettings(true, true), new bytes(0)); + + assertApproxEqAbs( + uint256(-int256(result.amount1())), FullMath.mulDiv(uint256(amountSpecified), (1e6 - newFee), 1e6), 1 wei + ); + + // the fee from beforeSwap is not stored + assertEq(_fetchPoolSwapFee(key), initialFee); + } + + function test_dynamicReturnSwapFee_revertIfFeeTooLarge() public { + assertEq(_fetchPoolSwapFee(key), 0); + + // hook adds the override flag + dynamicReturnsFeesHook.setFee(1000001); + + // a large fee is not used + uint128 amountSpecified = 10000; + vm.expectRevert(LPFeeLibrary.FeeTooLarge.selector); + binSwapHelper.swap(key, true, amountSpecified, BinSwapHelper.TestSettings(true, true), new bytes(0)); + } + + function _fetchPoolSwapFee(PoolKey memory _key) internal view returns (uint256 swapFee) { + PoolId id = _key.toId(); + (,, swapFee) = poolManager.getSlot0(id); + } +} diff --git a/test/pool-bin/BinPoolManager.t.sol b/test/pool-bin/BinPoolManager.t.sol index 9e9aa38e..76bee1d8 100644 --- a/test/pool-bin/BinPoolManager.t.sol +++ b/test/pool-bin/BinPoolManager.t.sol @@ -25,7 +25,7 @@ import {PackedUint128Math} from "../../src/pool-bin/libraries/math/PackedUint128 import {SafeCast} from "../../src/pool-bin/libraries/math/SafeCast.sol"; import {BinPoolParametersHelper} from "../../src/pool-bin/libraries/BinPoolParametersHelper.sol"; import {Constants} from "../../src/pool-bin/libraries/Constants.sol"; -import {IBinHooks} from "../../src/pool-bin/interfaces/IBinHooks.sol"; +import "../../src/pool-bin/interfaces/IBinHooks.sol"; import {BinFeeManagerHook} from "./helpers/BinFeeManagerHook.sol"; import {IHooks} from "../../src/interfaces/IHooks.sol"; import {IBinHooks} from "../../src/pool-bin/interfaces/IBinHooks.sol"; @@ -33,8 +33,8 @@ import {BinSwapHelper} from "./helpers/BinSwapHelper.sol"; import {BinLiquidityHelper} from "./helpers/BinLiquidityHelper.sol"; import {BinDonateHelper} from "./helpers/BinDonateHelper.sol"; import {BinTestHelper} from "./helpers/BinTestHelper.sol"; -import {BinNoOpTestHook} from "./helpers/BinNoOpTestHook.sol"; import {Hooks} from "../../src/libraries/Hooks.sol"; +import {BinPosition} from "../../src/pool-bin/libraries/BinPosition.sol"; contract BinPoolManagerTest is Test, GasSnapshot, BinTestHelper { using PoolIdLibrary for PoolKey; @@ -62,11 +62,12 @@ contract BinPoolManagerTest is Test, GasSnapshot, BinTestHelper { PoolId indexed id, address indexed sender, uint256[] ids, + bytes32 salt, bytes32[] amounts, bytes32 compositionFee, bytes32 pFees ); - event Burn(PoolId indexed id, address indexed sender, uint256[] ids, bytes32[] amounts); + event Burn(PoolId indexed id, address indexed sender, uint256[] ids, bytes32 salt, bytes32[] amounts); event Swap( PoolId indexed id, address indexed sender, @@ -155,6 +156,44 @@ contract BinPoolManagerTest is Test, GasSnapshot, BinTestHelper { poolManager.initialize(key, activeId, new bytes(0)); } + function testInitializeHookValidation() public { + uint16 bitMap = 0x0008; // after mint call + MockBinHooks mockHooks = new MockBinHooks(); + mockHooks.setHooksRegistrationBitmap(bitMap); + + // hook config + { + key = PoolKey({ + currency0: currency0, + currency1: currency1, + // hooks: hook, + hooks: IHooks(address(mockHooks)), + poolManager: IPoolManager(address(poolManager)), + fee: uint24(3000), // 3000 = 0.3% + parameters: bytes32(uint256(bitMap - 1)).setBinStep(10) + }); + vm.expectRevert(abi.encodeWithSelector(Hooks.HookConfigValidationError.selector)); + poolManager.initialize(key, activeId, new bytes(0)); + } + + // hook permission + { + bitMap = uint16(1 << HOOKS_AFTER_BURN_RETURNS_DELTA_OFFSET); + mockHooks.setHooksRegistrationBitmap(bitMap); + key = PoolKey({ + currency0: currency0, + currency1: currency1, + // hooks: hook, + hooks: IHooks(address(mockHooks)), + poolManager: IPoolManager(address(poolManager)), + fee: uint24(3000), // 3000 = 0.3% + parameters: bytes32(uint256(bitMap)).setBinStep(10) + }); + vm.expectRevert(abi.encodeWithSelector(Hooks.HookPermissionsValidationError.selector)); + poolManager.initialize(key, activeId, new bytes(0)); + } + } + function testInitializeSamePool() public { poolManager.initialize(key, 10, new bytes(0)); @@ -227,77 +266,6 @@ contract BinPoolManagerTest is Test, GasSnapshot, BinTestHelper { poolManager.initialize(key, activeId, ""); } - function testInitialize_NoOpMissingBeforeCall() public { - // 0000 0100 0000 0000 // only noOp - uint16 bitMap = 0x0400; - - BinNoOpTestHook noOpHook = new BinNoOpTestHook(); - noOpHook.setHooksRegistrationBitmap(bitMap); - - key = PoolKey({ - currency0: Currency.wrap(makeAddr("token0")), - currency1: Currency.wrap(makeAddr("token1")), - hooks: IHooks(address(noOpHook)), - poolManager: IPoolManager(address(poolManager)), - fee: uint24(3000), - parameters: bytes32(uint256(bitMap)).setBinStep(1) - }); - - // no op permission set, but no before call - vm.expectRevert(Hooks.NoOpHookMissingBeforeCall.selector); - poolManager.initialize(key, activeId, ""); - } - - function testNoOp_Gas() public { - // 0000 0101 0101 0100 // noOp, beforeMint, beforeBurn, beforeSwap, beforeDonate - uint16 bitMap = 0x0554; - - // pre-req create pool - BinNoOpTestHook noOpHook = new BinNoOpTestHook(); - noOpHook.setHooksRegistrationBitmap(bitMap); - key = PoolKey({ - currency0: Currency.wrap(makeAddr("token0")), - currency1: Currency.wrap(makeAddr("token1")), - hooks: IHooks(address(noOpHook)), - poolManager: IPoolManager(address(poolManager)), - fee: uint24(3000), - parameters: bytes32(uint256(bitMap)).setBinStep(1) - }); - - snapStart("BinPoolManagerTest#testNoOpGas_Initialize"); - poolManager.initialize(key, activeId, ""); - snapEnd(); - - BalanceDelta delta; - - // Action 1: mint, params doesn't matter for noOp - IBinPoolManager.MintParams memory mintParams; - snapStart("BinPoolManagerTest#testNoOpGas_Mint"); - delta = binLiquidityHelper.mint{value: 1 ether}(key, mintParams, ""); - snapEnd(); - assertTrue(delta == BalanceDeltaLibrary.MAXIMUM_DELTA); - - // Action 2: Burn, params doesn't matter for noOp - IBinPoolManager.BurnParams memory burnParams; - snapStart("BinPoolManagerTest#testNoOpGas_Burn"); - delta = binLiquidityHelper.burn(key, burnParams, ""); - snapEnd(); - assertTrue(delta == BalanceDeltaLibrary.MAXIMUM_DELTA); - - // Action 3: Swap - BinSwapHelper.TestSettings memory testSettings; - snapStart("BinPoolManagerTest#testNoOpGas_Swap"); - delta = binSwapHelper.swap(key, false, 1e17, testSettings, ""); - snapEnd(); - assertTrue(delta == BalanceDeltaLibrary.MAXIMUM_DELTA); - - // Action 4: Donate - snapStart("BinPoolManagerTest#testNoOpGas_Donate"); - delta = binDonateHelper.donate(key, 10 ether, 10 ether, ""); - snapEnd(); - assertTrue(delta == BalanceDeltaLibrary.MAXIMUM_DELTA); - } - function testGasMintOneBin() public { poolManager.initialize(key, activeId, new bytes(0)); @@ -312,7 +280,7 @@ contract BinPoolManagerTest is Test, GasSnapshot, BinTestHelper { vm.expectEmit(); bytes32 compositionFee = uint128(0).encode(uint128(0)); bytes32 pFee = uint128(0).encode(uint128(0)); - emit Mint(key.toId(), address(binLiquidityHelper), ids, amounts, compositionFee, pFee); + emit Mint(key.toId(), address(binLiquidityHelper), ids, 0, amounts, compositionFee, pFee); snapStart("BinPoolManagerTest#testGasMintOneBin-1"); binLiquidityHelper.mint(key, mintParams, ""); @@ -361,7 +329,7 @@ contract BinPoolManagerTest is Test, GasSnapshot, BinTestHelper { bytes32 compositionFee = uint128(0).encode(uint128(0)); bytes32 pFee = uint128(0).encode(uint128(0)); vm.expectEmit(); - emit Mint(key.toId(), address(binLiquidityHelper), ids, amounts, compositionFee, pFee); + emit Mint(key.toId(), address(binLiquidityHelper), ids, 0, amounts, compositionFee, pFee); // 1 ether as add 1 ether in native currency snapStart("BinPoolManagerTest#testMintNativeCurrency"); @@ -369,6 +337,202 @@ contract BinPoolManagerTest is Test, GasSnapshot, BinTestHelper { snapEnd(); } + function testMintAndBurnWithSalt() public { + bytes32 salt = bytes32(uint256(0x1234)); + poolManager.initialize(key, activeId, new bytes(0)); + + token0.mint(address(this), 10 ether); + token1.mint(address(this), 10 ether); + (IBinPoolManager.MintParams memory mintParams, uint24[] memory binIds) = + _getMultipleBinMintParams(activeId, 2 ether, 2 ether, 5, 5, salt); + binLiquidityHelper.mint(key, mintParams, ""); + + // liquidity added with salt 0x1234 not salt 0 + for (uint256 i = 0; i < binIds.length; i++) { + (uint128 binReserveX, uint128 binReserveY) = poolManager.getBin(key.toId(), binIds[i]); + + // make sure the liquidity is added to the correct bin + if (binIds[i] < activeId) { + assertEq(binReserveX, 0 ether); + assertEq(binReserveY, 0.4 ether); + } else if (binIds[i] > activeId) { + assertEq(binReserveX, 0.4 ether); + assertEq(binReserveY, 0 ether); + } else { + assertEq(binReserveX, 0.4 ether); + assertEq(binReserveY, 0.4 ether); + } + + BinPosition.Info memory position = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt); + BinPosition.Info memory position0 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], 0); + assertTrue(position.share != 0); + // position with salt = 0 + assertTrue(position0.share == 0); + } + + // burn liquidity with salt 0x1234 + IBinPoolManager.BurnParams memory burnParams = + _getMultipleBinBurnLiquidityParams(key, poolManager, binIds, address(binLiquidityHelper), 100, salt); + binLiquidityHelper.burn(key, burnParams, ""); + + for (uint256 i = 0; i < binIds.length; i++) { + (uint128 binReserveX, uint128 binReserveY) = poolManager.getBin(key.toId(), binIds[i]); + + // make sure the liquidity is added to the correct bin + assertEq(binReserveX, 0 ether); + assertEq(binReserveY, 0 ether); + + BinPosition.Info memory position = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt); + BinPosition.Info memory position0 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], 0); + assertTrue(position.share == 0); + assertTrue(position0.share == 0); + } + } + + function testMintMixWithAndWithoutSalt() public { + bytes32 salt0 = bytes32(0); + bytes32 salt1 = bytes32(uint256(0x1234)); + bytes32 salt2 = bytes32(uint256(0x5678)); + poolManager.initialize(key, activeId, new bytes(0)); + + token0.mint(address(this), 30 ether); + token1.mint(address(this), 30 ether); + + (IBinPoolManager.MintParams memory mintParams, uint24[] memory binIds) = + _getMultipleBinMintParams(activeId, 2 ether, 2 ether, 5, 5, salt1); + binLiquidityHelper.mint(key, mintParams, ""); + + // liquidity added with salt 0x1234 not salt 0 + for (uint256 i = 0; i < binIds.length; i++) { + (uint128 binReserveX, uint128 binReserveY) = poolManager.getBin(key.toId(), binIds[i]); + + // make sure the liquidity is added to the correct bin + if (binIds[i] < activeId) { + assertEq(binReserveX, 0 ether); + assertEq(binReserveY, 0.4 ether); + } else if (binIds[i] > activeId) { + assertEq(binReserveX, 0.4 ether); + assertEq(binReserveY, 0 ether); + } else { + assertEq(binReserveX, 0.4 ether); + assertEq(binReserveY, 0.4 ether); + } + + BinPosition.Info memory position0 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt0); + BinPosition.Info memory position1 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt1); + BinPosition.Info memory position2 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt2); + + // only position with salt 0x1234 should have share + assertTrue(position0.share == 0); + assertTrue(position1.share != 0); + assertTrue(position2.share == 0); + } + + { + (mintParams, binIds) = _getMultipleBinMintParams(activeId, 2 ether, 2 ether, 5, 5, salt2); + binLiquidityHelper.mint(key, mintParams, ""); + + for (uint256 i = 0; i < binIds.length; i++) { + (uint128 binReserveX, uint128 binReserveY) = poolManager.getBin(key.toId(), binIds[i]); + + // make sure the liquidity is added to the correct bin + if (binIds[i] < activeId) { + assertEq(binReserveX, 0 ether); + assertEq(binReserveY, 0.4 ether * 2); + } else if (binIds[i] > activeId) { + assertEq(binReserveX, 0.4 ether * 2); + assertEq(binReserveY, 0 ether); + } else { + assertEq(binReserveX, 0.4 ether * 2); + assertEq(binReserveY, 0.4 ether * 2); + } + + BinPosition.Info memory position0 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt0); + BinPosition.Info memory position1 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt1); + BinPosition.Info memory position2 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt2); + + // only position with salt 0 should be empty + assertTrue(position0.share == 0); + assertTrue(position1.share != 0); + assertTrue(position1.share == position2.share); + } + } + + { + (mintParams, binIds) = _getMultipleBinMintParams(activeId, 2 ether, 2 ether, 5, 5, salt0); + binLiquidityHelper.mint(key, mintParams, ""); + + for (uint256 i = 0; i < binIds.length; i++) { + (uint128 binReserveX, uint128 binReserveY) = poolManager.getBin(key.toId(), binIds[i]); + + // make sure the liquidity is added to the correct bin + if (binIds[i] < activeId) { + assertEq(binReserveX, 0 ether); + assertEq(binReserveY, 0.4 ether * 3); + } else if (binIds[i] > activeId) { + assertEq(binReserveX, 0.4 ether * 3); + assertEq(binReserveY, 0 ether); + } else { + assertEq(binReserveX, 0.4 ether * 3); + assertEq(binReserveY, 0.4 ether * 3); + } + + BinPosition.Info memory position0 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt0); + BinPosition.Info memory position1 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt1); + BinPosition.Info memory position2 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt2); + + assertTrue(position0.share != 0); + assertTrue(position1.share == position0.share); + assertTrue(position1.share == position2.share); + } + } + + // burning liquidity with salt 0x1234 should not impact position0 & position2 + IBinPoolManager.BurnParams memory burnParams = + _getMultipleBinBurnLiquidityParams(key, poolManager, binIds, address(binLiquidityHelper), 100, salt1); + binLiquidityHelper.burn(key, burnParams, ""); + + for (uint256 i = 0; i < binIds.length; i++) { + (uint128 binReserveX, uint128 binReserveY) = poolManager.getBin(key.toId(), binIds[i]); + + // make sure the liquidity is added to the correct bin + if (binIds[i] < activeId) { + assertEq(binReserveX, 0 ether); + assertEq(binReserveY, 0.4 ether * 2); + } else if (binIds[i] > activeId) { + assertEq(binReserveX, 0.4 ether * 2); + assertEq(binReserveY, 0 ether); + } else { + assertEq(binReserveX, 0.4 ether * 2); + assertEq(binReserveY, 0.4 ether * 2); + } + + BinPosition.Info memory position0 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt0); + BinPosition.Info memory position1 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt1); + BinPosition.Info memory position2 = + poolManager.getPosition(key.toId(), address(binLiquidityHelper), binIds[i], salt2); + + assertTrue(position0.share != 0); + assertTrue(position1.share == 0); + assertTrue(position0.share == position2.share); + } + } + function testGasBurnOneBin() public { // initialize poolManager.initialize(key, activeId, new bytes(0)); @@ -388,7 +552,7 @@ contract BinPoolManagerTest is Test, GasSnapshot, BinTestHelper { ids[0] = activeId; amounts[0] = uint128(1e18).encode(uint128(1e18)); vm.expectEmit(); - emit Burn(key.toId(), address(binLiquidityHelper), ids, amounts); + emit Burn(key.toId(), address(binLiquidityHelper), ids, 0, amounts); snapStart("BinPoolManagerTest#testGasBurnOneBin"); binLiquidityHelper.burn(key, burnParams, ""); @@ -457,7 +621,7 @@ contract BinPoolManagerTest is Test, GasSnapshot, BinTestHelper { ids[0] = activeId; amounts[0] = uint128(1e18).encode(uint128(1e18)); vm.expectEmit(); - emit Burn(key.toId(), address(binLiquidityHelper), ids, amounts); + emit Burn(key.toId(), address(binLiquidityHelper), ids, 0, amounts); snapStart("BinPoolManagerTest#testBurnNativeCurrency"); binLiquidityHelper.burn(key, burnParams, ""); diff --git a/test/pool-bin/helpers/BaseBinTestHook.sol b/test/pool-bin/helpers/BaseBinTestHook.sol index 3b1085c9..02597b1d 100644 --- a/test/pool-bin/helpers/BaseBinTestHook.sol +++ b/test/pool-bin/helpers/BaseBinTestHook.sol @@ -1,21 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import { - HOOKS_BEFORE_INITIALIZE_OFFSET, - HOOKS_AFTER_INITIALIZE_OFFSET, - HOOKS_BEFORE_MINT_OFFSET, - HOOKS_AFTER_MINT_OFFSET, - HOOKS_BEFORE_BURN_OFFSET, - HOOKS_AFTER_BURN_OFFSET, - HOOKS_BEFORE_SWAP_OFFSET, - HOOKS_AFTER_SWAP_OFFSET, - HOOKS_BEFORE_DONATE_OFFSET, - HOOKS_AFTER_DONATE_OFFSET, - HOOKS_NO_OP_OFFSET -} from "../../../src/pool-bin/interfaces/IBinHooks.sol"; +import "../../../src/pool-bin/interfaces/IBinHooks.sol"; import {PoolKey} from "../../../src/types/PoolKey.sol"; import {BalanceDelta} from "../../../src/types/BalanceDelta.sol"; +import {BeforeSwapDelta} from "../../../src/types/BeforeSwapDelta.sol"; import {IBinHooks} from "../../../src/pool-bin/interfaces/IBinHooks.sol"; import {IBinPoolManager} from "../../../src/pool-bin/interfaces/IBinPoolManager.sol"; @@ -33,7 +22,10 @@ contract BaseBinTestHook is IBinHooks { bool afterSwap; bool beforeDonate; bool afterDonate; - bool noOp; + bool beforeSwapReturnsDelta; + bool afterSwapReturnsDelta; + bool afterMintReturnsDelta; + bool afterBurnReturnsDelta; } function getHooksRegistrationBitmap() external view virtual returns (uint16) { @@ -59,7 +51,7 @@ contract BaseBinTestHook is IBinHooks { function afterMint(address, PoolKey calldata, IBinPoolManager.MintParams calldata, BalanceDelta, bytes calldata) external virtual - returns (bytes4) + returns (bytes4, BalanceDelta) { revert HookNotImplemented(); } @@ -75,19 +67,23 @@ contract BaseBinTestHook is IBinHooks { function afterBurn(address, PoolKey calldata, IBinPoolManager.BurnParams calldata, BalanceDelta, bytes calldata) external virtual - returns (bytes4) + returns (bytes4, BalanceDelta) { revert HookNotImplemented(); } - function beforeSwap(address, PoolKey calldata, bool, uint128, bytes calldata) external virtual returns (bytes4) { + function beforeSwap(address, PoolKey calldata, bool, uint128, bytes calldata) + external + virtual + returns (bytes4, BeforeSwapDelta, uint24) + { revert HookNotImplemented(); } function afterSwap(address, PoolKey calldata, bool, uint128, BalanceDelta, bytes calldata) external virtual - returns (bytes4) + returns (bytes4, int128) { revert HookNotImplemented(); } @@ -120,7 +116,10 @@ contract BaseBinTestHook is IBinHooks { | (permissions.afterSwap ? 1 << HOOKS_AFTER_SWAP_OFFSET : 0) | (permissions.beforeDonate ? 1 << HOOKS_BEFORE_DONATE_OFFSET : 0) | (permissions.afterDonate ? 1 << HOOKS_AFTER_DONATE_OFFSET : 0) - | (permissions.noOp ? 1 << HOOKS_NO_OP_OFFSET : 0) + | (permissions.beforeSwapReturnsDelta ? 1 << HOOKS_BEFORE_SWAP_RETURNS_DELTA_OFFSET : 0) + | (permissions.afterSwapReturnsDelta ? 1 << HOOKS_AFTER_SWAP_RETURNS_DELTA_OFFSET : 0) + | (permissions.afterMintReturnsDelta ? 1 << HOOKS_AFTER_MINT_RETURNS_DELTA_OFFSET : 0) + | (permissions.afterBurnReturnsDelta ? 1 << HOOKS_AFTER_BURN_RETURNS_DELTA_OFFSET : 0) ); } } diff --git a/test/pool-bin/helpers/BinDynamicReturnsFeeHook.sol b/test/pool-bin/helpers/BinDynamicReturnsFeeHook.sol new file mode 100644 index 00000000..ddb56389 --- /dev/null +++ b/test/pool-bin/helpers/BinDynamicReturnsFeeHook.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {BaseBinTestHook} from "./BaseBinTestHook.sol"; +import {PoolKey} from "../../../src/types/PoolKey.sol"; +import {IBinPoolManager} from "../../../src/pool-bin/interfaces/IBinPoolManager.sol"; +import {IBinHooks} from "../../../src/pool-bin/interfaces/IBinHooks.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../../../src/types/BeforeSwapDelta.sol"; +import {LPFeeLibrary} from "../../../src/libraries/LPFeeLibrary.sol"; + +contract BinDynamicReturnsFeeHook is BaseBinTestHook { + using LPFeeLibrary for uint24; + + uint24 internal fee; + IBinPoolManager manager; + + function getHooksRegistrationBitmap() external pure override returns (uint16) { + return _hooksRegistrationBitmapFrom( + Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeMint: false, + afterMint: false, + beforeBurn: false, + afterBurn: false, + beforeSwap: true, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnsDelta: false, + afterSwapReturnsDelta: false, + afterMintReturnsDelta: false, + afterBurnReturnsDelta: false + }) + ); + } + + function setManager(IBinPoolManager _manager) external { + manager = _manager; + } + + function setFee(uint24 _fee) external { + fee = _fee; + } + + function beforeSwap(address, PoolKey calldata, bool, uint128, bytes calldata) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { + // attach the fee flag to `fee` to enable overriding the pool's stored fee + return (IBinHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, fee | LPFeeLibrary.OVERRIDE_FEE_FLAG); + } + + function forcePoolFeeUpdate(PoolKey calldata _key, uint24 _fee) external { + manager.updateDynamicLPFee(_key, _fee); + } +} diff --git a/test/pool-bin/helpers/BinFeeManagerHook.sol b/test/pool-bin/helpers/BinFeeManagerHook.sol index c2bc87c5..338b1656 100644 --- a/test/pool-bin/helpers/BinFeeManagerHook.sol +++ b/test/pool-bin/helpers/BinFeeManagerHook.sol @@ -9,6 +9,7 @@ import {IBinDynamicFeeManager} from "../../../src/pool-bin/interfaces/IBinDynami import {PoolId, PoolIdLibrary} from "../../../src/types/PoolId.sol"; import {PoolKey} from "../../../src/types/PoolKey.sol"; import {BaseBinTestHook} from "./BaseBinTestHook.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../../../src/types/BeforeSwapDelta.sol"; contract BinFeeManagerHook is BaseBinTestHook, IBinDynamicFeeManager { using PoolIdLibrary for PoolKey; @@ -61,7 +62,7 @@ contract BinFeeManagerHook is BaseBinTestHook, IBinDynamicFeeManager { function beforeSwap(address, PoolKey calldata key, bool, uint128, bytes calldata hookData) external override - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { if (hookData.length > 0) { (bool _update, uint24 _fee) = abi.decode(hookData, (bool, uint24)); @@ -71,6 +72,6 @@ contract BinFeeManagerHook is BaseBinTestHook, IBinDynamicFeeManager { } } - return IBinHooks.beforeSwap.selector; + return (IBinHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } } diff --git a/test/pool-bin/helpers/BinLiquidityHelper.sol b/test/pool-bin/helpers/BinLiquidityHelper.sol index ba9bfaf1..38dd30ad 100644 --- a/test/pool-bin/helpers/BinLiquidityHelper.sol +++ b/test/pool-bin/helpers/BinLiquidityHelper.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.24; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IVault} from "../../../src/interfaces/IVault.sol"; -import {HOOKS_NO_OP_OFFSET} from "../../../src/pool-bin/interfaces/IBinHooks.sol"; import {IBinPoolManager} from "../../../src/pool-bin/interfaces/IBinPoolManager.sol"; import {BalanceDelta, BalanceDeltaLibrary} from "../../../src/types/BalanceDelta.sol"; import {CurrencyLibrary, Currency} from "../../../src/types/Currency.sol"; @@ -86,14 +85,6 @@ contract BinLiquidityHelper { (delta,) = binManager.mint(data.key, data.params, data.hookData); } - if (delta == BalanceDeltaLibrary.MAXIMUM_DELTA) { - // check if the hook has permission to no-op, if true, return early - if (!key.parameters.shouldCall(HOOKS_NO_OP_OFFSET, key.hooks)) { - revert HookMissingNoOpPermission(); - } - return abi.encode(delta); - } - if (delta.amount0() > 0) { if (key.currency0.isNative()) { vault.settle{value: uint128(delta.amount0())}(key.currency0); diff --git a/test/pool-bin/helpers/BinNoOpTestHook.sol b/test/pool-bin/helpers/BinNoOpTestHook.sol deleted file mode 100644 index c9b45e87..00000000 --- a/test/pool-bin/helpers/BinNoOpTestHook.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {PoolKey} from "../../../src/types/PoolKey.sol"; -import {BalanceDelta} from "../../../src/types/BalanceDelta.sol"; -import {IBinHooks} from "../../../src/pool-bin/interfaces/IBinHooks.sol"; -import {IBinPoolManager} from "../../../src/pool-bin/interfaces/IBinPoolManager.sol"; -import {Hooks} from "../../../src/libraries/Hooks.sol"; -import {BaseBinTestHook} from "./BaseBinTestHook.sol"; - -contract BinNoOpTestHook is BaseBinTestHook { - uint16 bitmap; - - function setHooksRegistrationBitmap(uint16 _bitmap) external { - bitmap = _bitmap; - } - - function getHooksRegistrationBitmap() external view override returns (uint16) { - return bitmap; - } - - function beforeMint(address, PoolKey calldata, IBinPoolManager.MintParams calldata, bytes calldata) - external - pure - override - returns (bytes4) - { - return Hooks.NO_OP_SELECTOR; - } - - function beforeBurn(address, PoolKey calldata, IBinPoolManager.BurnParams calldata, bytes calldata) - external - pure - override - returns (bytes4) - { - return Hooks.NO_OP_SELECTOR; - } - - function beforeSwap(address, PoolKey calldata, bool, uint128, bytes calldata) - external - pure - override - returns (bytes4) - { - return Hooks.NO_OP_SELECTOR; - } - - function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) - external - pure - override - returns (bytes4) - { - return Hooks.NO_OP_SELECTOR; - } -} diff --git a/test/pool-bin/helpers/BinReturnsDeltaHook.sol b/test/pool-bin/helpers/BinReturnsDeltaHook.sol new file mode 100644 index 00000000..776fcc60 --- /dev/null +++ b/test/pool-bin/helpers/BinReturnsDeltaHook.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IVault} from "../../../src/interfaces/IVault.sol"; +import {Hooks} from "../../../src/libraries/Hooks.sol"; +import {IBinPoolManager} from "../../../src/pool-bin/interfaces/IBinPoolManager.sol"; +import {PoolKey} from "../../../src/types/PoolKey.sol"; +import {Currency, CurrencyLibrary} from "../../../src/types/Currency.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {toBalanceDelta, BalanceDelta, BalanceDeltaLibrary} from "../../../src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, toBeforeSwapDelta} from "../../../src/types/BeforeSwapDelta.sol"; +import {BaseBinTestHook} from "./BaseBinTestHook.sol"; + +contract BinReturnsDeltaHook is BaseBinTestHook { + error InvalidAction(); + + using CurrencyLibrary for Currency; + using Hooks for bytes32; + + IVault public immutable vault; + IBinPoolManager public immutable poolManager; + + constructor(IVault _vault, IBinPoolManager _poolManager) { + vault = _vault; + poolManager = _poolManager; + } + + function getHooksRegistrationBitmap() external pure override returns (uint16) { + return _hooksRegistrationBitmapFrom( + Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeMint: false, + afterMint: true, + beforeBurn: false, + afterBurn: true, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnsDelta: true, + afterSwapReturnsDelta: true, + afterMintReturnsDelta: true, + afterBurnReturnsDelta: true + }) + ); + } + + function afterMint( + address, + PoolKey calldata key, + IBinPoolManager.MintParams memory params, + BalanceDelta, + bytes calldata data + ) external override returns (bytes4, BalanceDelta) { + (bytes32 amountIn) = abi.decode(data, (bytes32)); + if (amountIn == 0) { + return (this.afterMint.selector, BalanceDeltaLibrary.ZERO_DELTA); + } + params.amountIn = amountIn; + + (BalanceDelta hookDelta,) = poolManager.mint(key, params, new bytes(0)); + return (this.afterMint.selector, BalanceDeltaLibrary.ZERO_DELTA - hookDelta); + } + + function afterBurn( + address, + PoolKey calldata key, + IBinPoolManager.BurnParams calldata, + BalanceDelta delta, + bytes calldata + ) external override returns (bytes4, BalanceDelta) { + // charge 10% fee + int128 hookDelta0; + int128 hookDelta1; + if (delta.amount0() < 0) { + hookDelta0 = delta.amount0() / 10; + vault.take(key.currency0, address(this), uint128(-hookDelta0)); + } + if (delta.amount1() < 0) { + hookDelta1 = delta.amount1() / 10; + vault.take(key.currency1, address(this), uint128(-hookDelta1)); + } + + return (this.afterBurn.selector, toBalanceDelta(hookDelta0, hookDelta1)); + } + + function beforeSwap(address, PoolKey calldata key, bool swapForY, uint128, bytes calldata data) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { + (int128 hookDeltaSpecified, int128 hookDeltaUnspecified,) = abi.decode(data, (int128, int128, int128)); + + if (swapForY) { + // the specified token is token0 + if (hookDeltaSpecified > 0) { + vault.sync(key.currency0); + key.currency0.transfer(address(vault), uint128(hookDeltaSpecified)); + vault.settle(key.currency0); + } else { + vault.take(key.currency0, address(this), uint128(-hookDeltaSpecified)); + } + + if (hookDeltaUnspecified > 0) { + vault.sync(key.currency1); + key.currency1.transfer(address(vault), uint128(hookDeltaUnspecified)); + vault.settle(key.currency1); + } else { + vault.take(key.currency1, address(this), uint128(-hookDeltaUnspecified)); + } + } else { + // the specified token is token1 + if (hookDeltaSpecified > 0) { + vault.sync(key.currency1); + key.currency1.transfer(address(vault), uint128(hookDeltaSpecified)); + vault.settle(key.currency1); + } else { + vault.take(key.currency1, address(this), uint128(-hookDeltaSpecified)); + } + + if (hookDeltaUnspecified > 0) { + vault.sync(key.currency0); + key.currency0.transfer(address(vault), uint128(hookDeltaUnspecified)); + vault.settle(key.currency0); + } else { + vault.take(key.currency0, address(this), uint128(-hookDeltaUnspecified)); + } + } + + return (this.beforeSwap.selector, toBeforeSwapDelta(hookDeltaSpecified, hookDeltaUnspecified), 0); + } + + function afterSwap(address, PoolKey calldata key, bool swapForY, uint128, BalanceDelta, bytes calldata data) + external + override + returns (bytes4, int128) + { + (,, int128 hookDeltaUnspecified) = abi.decode(data, (int128, int128, int128)); + + if (hookDeltaUnspecified == 0) { + return (this.afterSwap.selector, 0); + } + + if (swapForY) { + // the unspecified token is token1 + if (hookDeltaUnspecified > 0) { + vault.sync(key.currency1); + key.currency1.transfer(address(vault), uint128(hookDeltaUnspecified)); + vault.settle(key.currency1); + } else { + vault.take(key.currency1, address(this), uint128(-hookDeltaUnspecified)); + } + } else { + // the unspecified token is token0 + if (hookDeltaUnspecified > 0) { + vault.sync(key.currency0); + key.currency0.transfer(address(vault), uint128(hookDeltaUnspecified)); + vault.settle(key.currency0); + } else { + vault.take(key.currency0, address(this), uint128(-hookDeltaUnspecified)); + } + } + + return (this.afterSwap.selector, hookDeltaUnspecified); + } +} diff --git a/test/pool-bin/helpers/BinSkipCallbackHook.sol b/test/pool-bin/helpers/BinSkipCallbackHook.sol index adf53fc4..0aa6c71e 100644 --- a/test/pool-bin/helpers/BinSkipCallbackHook.sol +++ b/test/pool-bin/helpers/BinSkipCallbackHook.sol @@ -6,7 +6,8 @@ import {Currency, CurrencyLibrary} from "../../../src/types/Currency.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {PoolKey} from "../../../src/types/PoolKey.sol"; -import {BalanceDelta} from "../../../src/types/BalanceDelta.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "../../../src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../../../src/types/BeforeSwapDelta.sol"; import {IBinHooks} from "../../../src/pool-bin/interfaces/IBinHooks.sol"; import {IBinPoolManager} from "../../../src/pool-bin/interfaces/IBinPoolManager.sol"; import {Hooks} from "../../../src/libraries/Hooks.sol"; @@ -51,7 +52,10 @@ contract BinSkipCallbackHook is BaseBinTestHook { afterSwap: true, beforeDonate: true, afterDonate: true, - noOp: false + beforeSwapReturnsDelta: true, + afterSwapReturnsDelta: true, + afterMintReturnsDelta: true, + afterBurnReturnsDelta: true }) ); } @@ -241,10 +245,10 @@ contract BinSkipCallbackHook is BaseBinTestHook { function afterMint(address, PoolKey calldata, IBinPoolManager.MintParams calldata, BalanceDelta, bytes calldata) external override - returns (bytes4) + returns (bytes4, BalanceDelta) { hookCounterCallbackCount++; - return BinSkipCallbackHook.afterMint.selector; + return (BinSkipCallbackHook.afterMint.selector, BalanceDeltaLibrary.ZERO_DELTA); } function beforeBurn(address, PoolKey calldata, IBinPoolManager.BurnParams calldata, bytes calldata) @@ -259,24 +263,28 @@ contract BinSkipCallbackHook is BaseBinTestHook { function afterBurn(address, PoolKey calldata, IBinPoolManager.BurnParams calldata, BalanceDelta, bytes calldata) external override - returns (bytes4) + returns (bytes4, BalanceDelta) { hookCounterCallbackCount++; - return BinSkipCallbackHook.afterBurn.selector; + return (BinSkipCallbackHook.afterBurn.selector, BalanceDeltaLibrary.ZERO_DELTA); } - function beforeSwap(address, PoolKey calldata, bool, uint128, bytes calldata) external override returns (bytes4) { + function beforeSwap(address, PoolKey calldata, bool, uint128, bytes calldata) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { hookCounterCallbackCount++; - return BinSkipCallbackHook.beforeSwap.selector; + return (BinSkipCallbackHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } function afterSwap(address, PoolKey calldata, bool, uint128, BalanceDelta, bytes calldata) external override - returns (bytes4) + returns (bytes4, int128) { hookCounterCallbackCount++; - return BinSkipCallbackHook.afterSwap.selector; + return (BinSkipCallbackHook.afterSwap.selector, 0); } function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) diff --git a/test/pool-bin/helpers/BinSwapHelper.sol b/test/pool-bin/helpers/BinSwapHelper.sol index 07adad73..f5f5c175 100644 --- a/test/pool-bin/helpers/BinSwapHelper.sol +++ b/test/pool-bin/helpers/BinSwapHelper.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.24; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IVault} from "../../../src/interfaces/IVault.sol"; -import {HOOKS_NO_OP_OFFSET} from "../../../src/pool-bin/interfaces/IBinHooks.sol"; import {IBinPoolManager} from "../../../src/pool-bin/interfaces/IBinPoolManager.sol"; import {BalanceDelta, BalanceDeltaLibrary} from "../../../src/types/BalanceDelta.sol"; import {CurrencyLibrary, Currency} from "../../../src/types/Currency.sol"; @@ -61,14 +60,6 @@ contract BinSwapHelper { BalanceDelta delta = binManager.swap(data.key, data.swapForY, data.amountIn, data.hookData); - if (delta == BalanceDeltaLibrary.MAXIMUM_DELTA) { - // check if the hook has permission to no-op, if true, return early - if (!data.key.parameters.shouldCall(HOOKS_NO_OP_OFFSET, data.key.hooks)) { - revert HookMissingNoOpPermission(); - } - return abi.encode(delta); - } - if (data.swapForY) { if (delta.amount0() > 0) { if (data.testSettings.settleUsingTransfer) { diff --git a/test/pool-bin/helpers/BinTestHelper.sol b/test/pool-bin/helpers/BinTestHelper.sol index 53689423..b0e26f56 100644 --- a/test/pool-bin/helpers/BinTestHelper.sol +++ b/test/pool-bin/helpers/BinTestHelper.sol @@ -41,7 +41,8 @@ abstract contract BinTestHelper is Test { IBinPoolManager.MintParams memory params = IBinPoolManager.MintParams({ liquidityConfigs: liquidityConfigurations, - amountIn: PackedUint128Math.encode(amountX.safe128(), amountY.safe128()) + amountIn: PackedUint128Math.encode(amountX.safe128(), amountY.safe128()), + salt: 0 }); vm.prank(from); @@ -87,14 +88,16 @@ abstract contract BinTestHelper is Test { params = IBinPoolManager.MintParams({ liquidityConfigs: liquidityConfigurations, - amountIn: PackedUint128Math.encode(0, amount.safe128()) + amountIn: PackedUint128Math.encode(0, amount.safe128()), + salt: 0 }); } else { liquidityConfigurations[0] = LiquidityConfigurations.encodeParams(1e18, 0, binId); params = IBinPoolManager.MintParams({ liquidityConfigs: liquidityConfigurations, - amountIn: PackedUint128Math.encode(amount.safe128(), 0) + amountIn: PackedUint128Math.encode(amount.safe128(), 0), + salt: 0 }); } } @@ -127,7 +130,38 @@ abstract contract BinTestHelper is Test { params = IBinPoolManager.MintParams({ liquidityConfigs: liquidityConfigurations, - amountIn: PackedUint128Math.encode(amountX.safe128(), amountY.safe128()) + amountIn: PackedUint128Math.encode(amountX.safe128(), amountY.safe128()), + salt: 0 + }); + } + + function _getMultipleBinMintParams( + uint24 binId, + uint256 amountX, + uint256 amountY, + uint8 nbBinX, + uint8 nbBinY, + bytes32 salt + ) internal pure returns (IBinPoolManager.MintParams memory params, uint24[] memory binIds) { + uint256 total = getTotalBins(nbBinX, nbBinY); // nbBinX + nbBinY - 1 + + bytes32[] memory liquidityConfigurations = new bytes32[](total); + binIds = new uint24[](total); + + for (uint256 i; i < total; ++i) { + uint24 id = getId(binId, i, nbBinY); // all the binId from left to right :: id = activeId + i - nBinY + 1 + binIds[i] = id; + + uint64 distribX = id >= binId && nbBinX > 0 ? (Constants.PRECISION / nbBinX).safe64() : 0; + uint64 distribY = id <= binId && nbBinY > 0 ? (Constants.PRECISION / nbBinY).safe64() : 0; + + liquidityConfigurations[i] = LiquidityConfigurations.encodeParams(distribX, distribY, id); + } + + params = IBinPoolManager.MintParams({ + liquidityConfigs: liquidityConfigurations, + amountIn: PackedUint128Math.encode(amountX.safe128(), amountY.safe128()), + salt: salt }); } @@ -145,7 +179,8 @@ abstract contract BinTestHelper is Test { ids[0] = binId; amtToBurn[0] = amountsToBurn; - IBinPoolManager.BurnParams memory params = IBinPoolManager.BurnParams({ids: ids, amountsToBurn: amtToBurn}); + IBinPoolManager.BurnParams memory params = + IBinPoolManager.BurnParams({ids: ids, amountsToBurn: amtToBurn, salt: 0}); vm.prank(from); delta = poolManager.burn(key, params, hookData); @@ -158,7 +193,8 @@ abstract contract BinTestHelper is Test { uint256[] memory ids, uint256[] memory amountsToBurn ) public { - IBinPoolManager.BurnParams memory params = IBinPoolManager.BurnParams({ids: ids, amountsToBurn: amountsToBurn}); + IBinPoolManager.BurnParams memory params = + IBinPoolManager.BurnParams({ids: ids, amountsToBurn: amountsToBurn, salt: 0}); vm.prank(from); poolManager.burn(key, params, "0x00"); @@ -177,9 +213,9 @@ abstract contract BinTestHelper is Test { uint256[] memory balances = new uint256[](1); ids[0] = binId; - balances[0] = (pm.getPosition(_key.toId(), from, binId).share * sharePercentage) / 100; + balances[0] = (pm.getPosition(_key.toId(), from, binId, 0).share * sharePercentage) / 100; - params = IBinPoolManager.BurnParams({ids: ids, amountsToBurn: balances}); + params = IBinPoolManager.BurnParams({ids: ids, amountsToBurn: balances, salt: 0}); } /// @dev get burn params assuming user is burning all liquidity at the binId @@ -196,10 +232,29 @@ abstract contract BinTestHelper is Test { for (uint256 i; i < binIds.length; i++) { ids[i] = binIds[i]; - balances[i] = (pm.getPosition(_key.toId(), from, binIds[i]).share * sharePercentage) / 100; + balances[i] = (pm.getPosition(_key.toId(), from, binIds[i], 0).share * sharePercentage) / 100; + } + + params = IBinPoolManager.BurnParams({ids: ids, amountsToBurn: balances, salt: 0}); + } + + function _getMultipleBinBurnLiquidityParams( + PoolKey memory _key, + BinPoolManager pm, + uint24[] memory binIds, + address from, + uint256 sharePercentage, + bytes32 salt + ) internal view returns (IBinPoolManager.BurnParams memory params) { + uint256[] memory ids = new uint256[](binIds.length); + uint256[] memory balances = new uint256[](binIds.length); + + for (uint256 i; i < binIds.length; i++) { + ids[i] = binIds[i]; + balances[i] = (pm.getPosition(_key.toId(), from, binIds[i], salt).share * sharePercentage) / 100; } - params = IBinPoolManager.BurnParams({ids: ids, amountsToBurn: balances}); + params = IBinPoolManager.BurnParams({ids: ids, amountsToBurn: balances, salt: salt}); } function getTotalBins(uint8 nbBinX, uint8 nbBinY) public pure returns (uint256) { diff --git a/test/pool-bin/libraries/BinPoolDonate.t.sol b/test/pool-bin/libraries/BinPoolDonate.t.sol index ad727245..a189716c 100644 --- a/test/pool-bin/libraries/BinPoolDonate.t.sol +++ b/test/pool-bin/libraries/BinPoolDonate.t.sol @@ -67,9 +67,9 @@ contract BinPoolDonateTest is BinTestHelper { // Initialize. Alice/Bob both add 1e18 token0, token1 to the active bin poolManager.initialize(key, activeId, new bytes(0)); addLiquidityToBin(key, poolManager, alice, activeId, 1e18, 1e18, 1e18, 1e18, ""); - uint256 aliceShare = poolManager.getPosition(poolId, alice, activeId).share; + uint256 aliceShare = poolManager.getPosition(poolId, alice, activeId, 0).share; addLiquidityToBin(key, poolManager, bob, activeId, 1e18, 1e18, 1e18, 1e18, ""); - uint256 bobShare = poolManager.getPosition(poolId, bob, activeId).share; + uint256 bobShare = poolManager.getPosition(poolId, bob, activeId, 0).share; // Verify reserve before donate uint128 reserveX; @@ -110,7 +110,7 @@ contract BinPoolDonateTest is BinTestHelper { // Initialize and add 1e18 token0, token1 to the active bin. price of bin: 2**128, 3.4e38 poolManager.initialize(key, activeId, new bytes(0)); addLiquidityToBin(key, poolManager, bob, activeId, 1e18, 1e18, 1e18, 1e18, ""); - poolManager.getPosition(poolId, bob, activeId).share; + poolManager.getPosition(poolId, bob, activeId, 0).share; poolManager.donate(key, amt0, amt1, ""); diff --git a/test/pool-bin/libraries/BinPoolFee.t.sol b/test/pool-bin/libraries/BinPoolFee.t.sol index b23e361b..15669a9c 100644 --- a/test/pool-bin/libraries/BinPoolFee.t.sol +++ b/test/pool-bin/libraries/BinPoolFee.t.sol @@ -40,11 +40,12 @@ contract BinPoolFeeTest is BinTestHelper { PoolId indexed id, address indexed sender, uint256[] ids, + bytes32 salt, bytes32[] amounts, bytes32 compositionFee, bytes32 pFee ); - event Burn(PoolId indexed id, address indexed sender, uint256[] ids, bytes32[] amounts); + event Burn(PoolId indexed id, address indexed sender, uint256[] ids, bytes32 salt, bytes32[] amounts); event Swap( PoolId indexed id, address indexed sender, @@ -121,7 +122,7 @@ contract BinPoolFeeTest is BinTestHelper { ids[0] = binId; amounts[0] = expectedAmtInBin; vm.expectEmit(); - emit Mint(key.toId(), bob, ids, amounts, expectedFee, protocolFee); + emit Mint(key.toId(), bob, ids, 0, amounts, expectedFee, protocolFee); addLiquidityToBin(key, poolManager, bob, binId, amountX, amountY, 4e17, 5e17, ""); } @@ -187,7 +188,7 @@ contract BinPoolFeeTest is BinTestHelper { ids[0] = binId; amounts[0] = expectedAmtInBin; vm.expectEmit(); - emit Mint(key.toId(), bob, ids, amounts, expectedFee, protocolFee); + emit Mint(key.toId(), bob, ids, 0, amounts, expectedFee, protocolFee); addLiquidityToBin(key, poolManager, bob, binId, amountX, amountY, 4e17, 5e17, ""); } @@ -216,7 +217,7 @@ contract BinPoolFeeTest is BinTestHelper { ids[0] = binId; amounts[0] = expectedAmtInBin; vm.expectEmit(); - emit Mint(key.toId(), bob, ids, amounts, expectedFee, protocolFee); + emit Mint(key.toId(), bob, ids, 0, amounts, expectedFee, protocolFee); addLiquidityToBin(key, poolManager, bob, binId, amountX, amountY, 4e17, 5e17, ""); @@ -240,7 +241,7 @@ contract BinPoolFeeTest is BinTestHelper { // then remove liquidity uint256[] memory balances = new uint256[](1); uint256[] memory ids = new uint256[](1); - balances[0] = poolManager.getPosition(poolId, bob, activeId).share; + balances[0] = poolManager.getPosition(poolId, bob, activeId, 0).share; ids[0] = activeId; removeLiquidity(key, poolManager, bob, ids, balances); diff --git a/test/pool-bin/libraries/BinPoolLiquidity.t.sol b/test/pool-bin/libraries/BinPoolLiquidity.t.sol index 6edf5d96..9dc378dc 100644 --- a/test/pool-bin/libraries/BinPoolLiquidity.t.sol +++ b/test/pool-bin/libraries/BinPoolLiquidity.t.sol @@ -98,7 +98,7 @@ contract BinPoolLiquidityTest is BinTestHelper { assertEq(binReserveY, 0, "test_SimpleMint::8"); } - assertGt(poolManager.getPosition(poolId, bob, id).share, 0, "test_SimpleMint::9"); + assertGt(poolManager.getPosition(poolId, bob, id, 0).share, 0, "test_SimpleMint::9"); } } { @@ -123,7 +123,7 @@ contract BinPoolLiquidityTest is BinTestHelper { } // verify liquidity minted - assertEq(poolManager.getPosition(poolId, bob, id).share, array.liquidityMinted[i]); + assertEq(poolManager.getPosition(poolId, bob, id, 0).share, array.liquidityMinted[i]); } } } @@ -143,7 +143,7 @@ contract BinPoolLiquidityTest is BinTestHelper { for (uint256 i; i < total; ++i) { uint24 id = getId(activeId, i, nbBinY); - balances[i] = poolManager.getPosition(poolId, bob, id).share; + balances[i] = poolManager.getPosition(poolId, bob, id, 0).share; } addLiquidity(key, poolManager, bob, activeId, amountX, amountY, nbBinX, nbBinY); @@ -169,7 +169,7 @@ contract BinPoolLiquidityTest is BinTestHelper { assertEq(binReserveY, 0, "test_SimpleMint::6"); } - assertEq(poolManager.getPosition(poolId, bob, id).share, 2 * balances[i], "test_DoubleMint:7"); + assertEq(poolManager.getPosition(poolId, bob, id, 0).share, 2 * balances[i], "test_DoubleMint:7"); } } @@ -188,7 +188,7 @@ contract BinPoolLiquidityTest is BinTestHelper { for (uint256 i; i < total; ++i) { uint24 id = getId(activeId, i, nbBinY); - balances[i] = poolManager.getPosition(poolId, bob, id).share; + balances[i] = poolManager.getPosition(poolId, bob, id, 0).share; } addLiquidity(key, poolManager, bob, activeId, amountX, amountY, nbBinX, 0); @@ -199,14 +199,14 @@ contract BinPoolLiquidityTest is BinTestHelper { if (id == activeId) { assertApproxEqRel( - poolManager.getPosition(poolId, bob, id).share, + poolManager.getPosition(poolId, bob, id, 0).share, 2 * balances[i], 1e15, "test_MintWithDifferentBins::1" ); // composition fee } else { assertEq( - poolManager.getPosition(poolId, bob, id).share, 2 * balances[i], "test_MintWithDifferentBins::2" + poolManager.getPosition(poolId, bob, id, 0).share, 2 * balances[i], "test_MintWithDifferentBins::2" ); } } @@ -215,8 +215,11 @@ contract BinPoolLiquidityTest is BinTestHelper { function test_revert_MintEmptyConfig() public { poolManager.initialize(key, activeId, new bytes(0)); - IBinPoolManager.MintParams memory params = - IBinPoolManager.MintParams({liquidityConfigs: new bytes32[](0), amountIn: PackedUint128Math.encode(0, 0)}); + IBinPoolManager.MintParams memory params = IBinPoolManager.MintParams({ + liquidityConfigs: new bytes32[](0), + amountIn: PackedUint128Math.encode(0, 0), + salt: 0 + }); vm.expectRevert(BinPool.BinPool__EmptyLiquidityConfigs.selector); poolManager.mint(key, params, "0x00"); @@ -229,7 +232,7 @@ contract BinPoolLiquidityTest is BinTestHelper { data[0] = LiquidityConfigurations.encodeParams(1e18, 1e18, activeId); IBinPoolManager.MintParams memory params = - IBinPoolManager.MintParams({liquidityConfigs: data, amountIn: PackedUint128Math.encode(0, 0)}); + IBinPoolManager.MintParams({liquidityConfigs: data, amountIn: PackedUint128Math.encode(0, 0), salt: 0}); vm.expectRevert(abi.encodeWithSelector(BinPool.BinPool__ZeroShares.selector, activeId)); poolManager.mint(key, params, "0x00"); @@ -243,14 +246,21 @@ contract BinPoolLiquidityTest is BinTestHelper { data[1] = LiquidityConfigurations.encodeParams(0, 0.5e18 + 1, activeId); vm.expectRevert(PackedUint128Math.PackedUint128Math__SubUnderflow.selector); - IBinPoolManager.MintParams memory params = - IBinPoolManager.MintParams({liquidityConfigs: data, amountIn: PackedUint128Math.encode(1e18, 1e18)}); + IBinPoolManager.MintParams memory params = IBinPoolManager.MintParams({ + liquidityConfigs: data, + amountIn: PackedUint128Math.encode(1e18, 1e18), + salt: 0 + }); poolManager.mint(key, params, "0x00"); data[1] = LiquidityConfigurations.encodeParams(0.5e18, 0, activeId); data[0] = LiquidityConfigurations.encodeParams(0.5e18 + 1, 0, activeId + 1); vm.expectRevert(PackedUint128Math.PackedUint128Math__SubUnderflow.selector); - params = IBinPoolManager.MintParams({liquidityConfigs: data, amountIn: PackedUint128Math.encode(1e18, 1e18)}); + params = IBinPoolManager.MintParams({ + liquidityConfigs: data, + amountIn: PackedUint128Math.encode(1e18, 1e18), + salt: 0 + }); poolManager.mint(key, params, "0x00"); } @@ -271,7 +281,7 @@ contract BinPoolLiquidityTest is BinTestHelper { for (uint256 i; i < total; ++i) { uint24 id = getId(activeId, i, nbBinY); ids[i] = id; - balances[i] = poolManager.getPosition(poolId, bob, id).share; + balances[i] = poolManager.getPosition(poolId, bob, id, 0).share; } uint256 reserveX = vault.reservesOfPoolManager(key.poolManager, key.currency0); @@ -313,7 +323,7 @@ contract BinPoolLiquidityTest is BinTestHelper { uint24 id = getId(activeId, i, nbBinY); ids[i] = id; - uint256 balance = poolManager.getPosition(poolId, bob, id).share; + uint256 balance = poolManager.getPosition(poolId, bob, id, 0).share; halfbalances[i] = balance / 2; balances[i] = balance - balance / 2; @@ -378,7 +388,7 @@ contract BinPoolLiquidityTest is BinTestHelper { uint256[] memory balances = new uint256[](1); ids[0] = activeId; - balances[0] = poolManager.getPosition(poolId, bob, activeId).share + 1; + balances[0] = poolManager.getPosition(poolId, bob, activeId, 0).share + 1; vm.expectRevert(stdError.arithmeticError); removeLiquidity(key, poolManager, bob, ids, balances); diff --git a/test/pool-bin/libraries/BinPoolSwap.t.sol b/test/pool-bin/libraries/BinPoolSwap.t.sol index c4e354a9..4e876134 100644 --- a/test/pool-bin/libraries/BinPoolSwap.t.sol +++ b/test/pool-bin/libraries/BinPoolSwap.t.sol @@ -236,10 +236,10 @@ contract BinPoolSwapTest is BinTestHelper { uint128 amountIn = 0; - vm.expectRevert(BinPool.BinPool__InsufficientAmountIn.selector); + vm.expectRevert(IBinPoolManager.InsufficientAmountIn.selector); poolManager.swap(key, true, amountIn, "0x"); - vm.expectRevert(BinPool.BinPool__InsufficientAmountIn.selector); + vm.expectRevert(IBinPoolManager.InsufficientAmountIn.selector); poolManager.swap(key, false, amountIn, "0x"); } diff --git a/test/pool-bin/libraries/BinPosition.t.sol b/test/pool-bin/libraries/BinPosition.t.sol index 72b995e4..fd9bd18a 100644 --- a/test/pool-bin/libraries/BinPosition.t.sol +++ b/test/pool-bin/libraries/BinPosition.t.sol @@ -16,43 +16,43 @@ contract BinPositionTest is Test { State private _self; function testFuzz_AddShare(address owner, uint24 binId, uint256 share) public { - _self.positions.get(owner, binId).addShare(share); + _self.positions.get(owner, binId, 0).addShare(share); - assertEq(_self.positions.get(owner, binId).share, share, "testFuzz_AddShare::1"); + assertEq(_self.positions.get(owner, binId, 0).share, share, "testFuzz_AddShare::1"); } function testFuzz_AddShareMultiple(address owner, uint24 binId, uint256 share1, uint256 share2) public { share1 = bound(share1, 0, type(uint128).max); share2 = bound(share2, 0, type(uint128).max); - _self.positions.get(owner, binId).addShare(share1); - _self.positions.get(owner, binId).addShare(share2); + _self.positions.get(owner, binId, 0).addShare(share1); + _self.positions.get(owner, binId, 0).addShare(share2); - assertEq(_self.positions.get(owner, binId).share, share1 + share2, "testFuzz_AddShareMultiple::1"); + assertEq(_self.positions.get(owner, binId, 0).share, share1 + share2, "testFuzz_AddShareMultiple::1"); } function testFuzz_SubShare(address owner, uint24 binId, uint256 share) public { - _self.positions.get(owner, binId).addShare(share); - _self.positions.get(owner, binId).subShare(share); + _self.positions.get(owner, binId, 0).addShare(share); + _self.positions.get(owner, binId, 0).subShare(share); - assertEq(_self.positions.get(owner, binId).share, 0, "testFuzz_SubShare::1"); + assertEq(_self.positions.get(owner, binId, 0).share, 0, "testFuzz_SubShare::1"); } function testFuzz_SubShareMultiple(address owner, uint24 binId, uint256 share1, uint256 share2) public { share1 = bound(share1, 1, type(uint128).max); share2 = bound(share2, 0, share1 - 1); - _self.positions.get(owner, binId).addShare(share1); - _self.positions.get(owner, binId).subShare(share2); + _self.positions.get(owner, binId, 0).addShare(share1); + _self.positions.get(owner, binId, 0).subShare(share2); - assertEq(_self.positions.get(owner, binId).share, share1 - share2, "testFuzz_SubShareMultiple::1"); + assertEq(_self.positions.get(owner, binId, 0).share, share1 - share2, "testFuzz_SubShareMultiple::1"); } - function testFuzz_GetPosition(address owner, uint24 binId, uint256 share) public { + function testFuzz_GetPosition(address owner, uint24 binId, bytes32 salt, uint256 share) public { // manual keccak and add share - bytes32 key = keccak256(abi.encodePacked(owner, binId)); + bytes32 key = keccak256(abi.encodePacked(owner, binId, salt)); _self.positions[key].addShare(share); // verify assembly version of keccak retrival works - assertEq(_self.positions.get(owner, binId).share, share); + assertEq(_self.positions.get(owner, binId, salt).share, share); } } diff --git a/test/pool-cl/CLFees.t.sol b/test/pool-cl/CLFees.t.sol index 5866fb2c..321b2e59 100644 --- a/test/pool-cl/CLFees.t.sol +++ b/test/pool-cl/CLFees.t.sol @@ -110,7 +110,7 @@ contract CLFeesTest is Test, Deployers, TokenFixture, GasSnapshot { int256 liquidityDelta = 10000; ICLPoolManager.ModifyLiquidityParams memory params = - ICLPoolManager.ModifyLiquidityParams(-60, 60, liquidityDelta); + ICLPoolManager.ModifyLiquidityParams(-60, 60, liquidityDelta, 0); router.modifyPosition(key, params, ZERO_BYTES); // Fees dont accrue for positive liquidity delta. @@ -118,11 +118,11 @@ contract CLFeesTest is Test, Deployers, TokenFixture, GasSnapshot { assertEq(manager.protocolFeesAccrued(currency1), 0); ICLPoolManager.ModifyLiquidityParams memory params2 = - ICLPoolManager.ModifyLiquidityParams(-60, 60, -liquidityDelta); + ICLPoolManager.ModifyLiquidityParams(-60, 60, -liquidityDelta, 0); router.modifyPosition(key, params2, ZERO_BYTES); // add larger liquidity - params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 10e18); + params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 10e18, 0); router.modifyPosition(key, params, ZERO_BYTES); MockERC20(Currency.unwrap(currency1)).approve(address(router), type(uint256).max); @@ -147,7 +147,7 @@ contract CLFeesTest is Test, Deployers, TokenFixture, GasSnapshot { (CLPool.Slot0 memory slot0,,,) = manager.pools(key.toId()); assertEq(slot0.protocolFee, protocolFee); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-120, 120, 10e18); + ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-120, 120, 10e18, 0); router.modifyPosition(key, params, ZERO_BYTES); // 1 for 0 swap MockERC20(Currency.unwrap(currency1)).approve(address(router), type(uint256).max); diff --git a/test/pool-cl/CLHookReturnsDelta.t.sol b/test/pool-cl/CLHookReturnsDelta.t.sol new file mode 100644 index 00000000..f657071f --- /dev/null +++ b/test/pool-cl/CLHookReturnsDelta.t.sol @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {IVault} from "../../src/interfaces/IVault.sol"; +import {Vault} from "../../src/Vault.sol"; +import {IPoolManager} from "../../src/interfaces/IPoolManager.sol"; +import {ICLPoolManager} from "../../src/pool-cl/interfaces/ICLPoolManager.sol"; +import {CLPoolManager} from "../../src/pool-cl/CLPoolManager.sol"; +import {CLPool} from "../../src/pool-cl/libraries/CLPool.sol"; +import {Currency, CurrencyLibrary} from "../../src/types/Currency.sol"; +import {PoolKey} from "../../src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "../../src/types/PoolId.sol"; +import {IHooks} from "../../src/interfaces/IHooks.sol"; +import {Hooks} from "../../src/libraries/Hooks.sol"; +import {CLPoolManagerRouter} from "./helpers/CLPoolManagerRouter.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Deployers} from "./helpers/Deployers.sol"; +import {TokenFixture} from "../helpers/TokenFixture.sol"; +import {LPFeeLibrary} from "../../src/libraries/LPFeeLibrary.sol"; +import {CLPoolParametersHelper} from "../../src/pool-cl/libraries/CLPoolParametersHelper.sol"; +import {CLReturnsDeltaHook} from "./helpers/CLReturnsDeltaHook.sol"; +import {BalanceDelta} from "../../src/types/BalanceDelta.sol"; +import {TickMath} from "../../src/pool-cl/libraries/TickMath.sol"; + +contract CLHookReturnsDeltaTest is Test, Deployers, TokenFixture, GasSnapshot { + using PoolIdLibrary for PoolKey; + using CurrencyLibrary for Currency; + using CLPoolParametersHelper for bytes32; + using LPFeeLibrary for uint24; + + PoolKey key; + IVault public vault; + CLPoolManager public poolManager; + CLPoolManagerRouter public router; + CLReturnsDeltaHook public clReturnsDeltaHook; + + function setUp() public { + initializeTokens(); + (vault, poolManager) = createFreshManager(); + + router = new CLPoolManagerRouter(vault, poolManager); + clReturnsDeltaHook = new CLReturnsDeltaHook(vault, poolManager); + + IERC20(Currency.unwrap(currency0)).approve(address(router), 1000 ether); + IERC20(Currency.unwrap(currency1)).approve(address(router), 1000 ether); + IERC20(Currency.unwrap(currency0)).approve(address(clReturnsDeltaHook), 1000 ether); + IERC20(Currency.unwrap(currency1)).approve(address(clReturnsDeltaHook), 1000 ether); + + key = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: clReturnsDeltaHook, + poolManager: poolManager, + fee: uint24(3000), + parameters: bytes32(uint256(clReturnsDeltaHook.getHooksRegistrationBitmap())).setTickSpacing(10) + }); + + poolManager.initialize(key, SQRT_RATIO_1_1, new bytes(0)); + } + + function testModifyPosition_AddMore() external { + (BalanceDelta delta,) = router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10 ether, salt: 0}), + abi.encode(0 ether) + ); + + uint128 liquidity = poolManager.getLiquidity(key.toId()); + + (BalanceDelta delta2,) = router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10 ether, salt: 0}), + abi.encode(10 ether) + ); + uint128 liquidity2 = poolManager.getLiquidity(key.toId()); + + // hook double the liquidity + assertEq(delta.amount0() * 2, delta2.amount0()); + assertEq(delta.amount1() * 2, delta2.amount1()); + + assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)), uint128(delta.amount0()) * 3); + assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)), uint128(delta.amount1()) * 3); + + assertEq(liquidity * 2, liquidity2 - liquidity); + } + + function testModifyPosition_AddLess() external { + // add some liquidity first in case the pool is empty + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10 ether, salt: 0}), + abi.encode(10 ether) + ); + + uint128 liquidityBefore = poolManager.getLiquidity(key.toId()); + uint256 amt0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + + (BalanceDelta delta,) = router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10 ether, salt: 0}), + abi.encode(-10 ether) + ); + uint128 liquidityAfter = poolManager.getLiquidity(key.toId()); + uint256 amt0After = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + + assertEq(liquidityBefore, liquidityAfter); + assertEq(amt0Before, amt0After - 1); + assertEq(amt1Before, amt1After - 1); + + assertEq(delta.amount0(), 1); + assertEq(delta.amount1(), 1); + } + + function testModifyPosition_RemoveMore() external { + // add some liquidity first in case the pool is empty + (BalanceDelta delta,) = router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10 ether, salt: 0}), + abi.encode(10 ether) + ); + + uint128 liquidityBefore = poolManager.getLiquidity(key.toId()); + uint256 amt0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + + (BalanceDelta delta2,) = router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: -5 ether, salt: 0}), + abi.encode(-5 ether) + ); + uint128 liquidityAfter = poolManager.getLiquidity(key.toId()); + uint256 amt0After = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + + assertEq(liquidityBefore, liquidityAfter * 2); + assertEq(amt0Before, (amt0After - 1) * 2); + assertEq(amt1Before, (amt1After - 1) * 2); + + assertEq(delta.amount0(), -(delta2.amount0() - 1) * 2); + assertEq(delta.amount1(), -(delta2.amount1() - 1) * 2); + } + + function testModifyPosition_RemoveLess() external { + // add some liquidity first in case the pool is empty + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10 ether, salt: 0}), + abi.encode(10 ether) + ); + + uint128 liquidityBefore = poolManager.getLiquidity(key.toId()); + uint256 amt0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + + (BalanceDelta delta,) = router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: -5 ether, salt: 0}), + abi.encode(5 ether) + ); + uint128 liquidityAfter = poolManager.getLiquidity(key.toId()); + uint256 amt0After = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + + assertEq(liquidityBefore, liquidityAfter); + assertEq(amt0Before, amt0After - 1); + assertEq(amt1Before, amt1After - 1); + + assertEq(delta.amount0(), 1); + assertEq(delta.amount1(), 1); + } + + function testSwap_noSwap_specifyInput() external { + // add some liquidity first in case the pool is empty + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10000 ether, salt: 0}), + abi.encode(10000 ether) + ); + + uint256 amt0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + uint128 liquidityBefore = poolManager.getLiquidity(key.toId()); + + (BalanceDelta delta) = router.swap( + key, + ICLPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: 1 ether, + sqrtPriceLimitX96: TickMath.MIN_SQRT_RATIO + 1 + }), + CLPoolManagerRouter.SwapTestSettings({withdrawTokens: true, settleUsingTransfer: true}), + abi.encode(-1 ether, 0, 0) + ); + + uint256 amt0After = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + uint128 liquidityAfter = poolManager.getLiquidity(key.toId()); + + // user pays 1 ether of currency0 to hook and no swap happens + + // trader's payment & return + assertEq(delta.amount0(), 1 ether); + assertEq(delta.amount1(), 0); + + // hook's payment & return + assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(clReturnsDeltaHook)), 1 ether); + + assertEq(amt0Before, amt0After); + assertEq(amt1Before, amt1After); + assertEq(liquidityBefore, liquidityAfter); + } + + function testSwap_noSwap_specifyOutput() external { + // add some liquidity first in case the pool is empty + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10000 ether, salt: 0}), + abi.encode(10000 ether) + ); + + uint256 amt0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + uint128 liquidityBefore = poolManager.getLiquidity(key.toId()); + + // make sure hook has enough balance to pay + currency1.transfer(address(clReturnsDeltaHook), 1 ether); + + (BalanceDelta delta) = router.swap( + key, + ICLPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: -1 ether, + sqrtPriceLimitX96: TickMath.MIN_SQRT_RATIO + 1 + }), + CLPoolManagerRouter.SwapTestSettings({withdrawTokens: true, settleUsingTransfer: true}), + abi.encode(1 ether, 0, 0) + ); + + uint256 amt0After = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + uint128 liquidityAfter = poolManager.getLiquidity(key.toId()); + + // hook pays 1 ether of currency1 to user and no swap happens + + // trader's payment & return + assertEq(delta.amount0(), 0); + assertEq(delta.amount1(), -1 ether); + + // hook's payment & return + assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(clReturnsDeltaHook)), 0 ether); + + assertEq(amt0Before, amt0After); + assertEq(amt1Before, amt1After); + assertEq(liquidityBefore, liquidityAfter); + } + + function testSwap_noSwap_returnUnspecifiedInBeforeSwap() external { + // add some liquidity first in case the pool is empty + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10000 ether, salt: 0}), + abi.encode(10000 ether) + ); + + currency1.transfer(address(clReturnsDeltaHook), 1 ether); + + uint256 amt0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + uint128 liquidityBefore = poolManager.getLiquidity(key.toId()); + + (BalanceDelta delta) = router.swap( + key, + ICLPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: 1 ether, + sqrtPriceLimitX96: TickMath.MIN_SQRT_RATIO + 1 + }), + CLPoolManagerRouter.SwapTestSettings({withdrawTokens: true, settleUsingTransfer: true}), + abi.encode(-1 ether, 1 ether, 0) + ); + + uint256 amt0After = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + uint128 liquidityAfter = poolManager.getLiquidity(key.toId()); + + // user pays 1 ether of currency0 to hook and no swap happens + + // trader's payment & return + assertEq(delta.amount0(), 1 ether); + assertEq(delta.amount1(), -1 ether); + + // hook's payment & return + assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(clReturnsDeltaHook)), 1 ether); + assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(clReturnsDeltaHook)), 0 ether); + + assertEq(amt0Before, amt0After); + assertEq(amt1Before, amt1After); + assertEq(liquidityBefore, liquidityAfter); + } + + function testSwap_noSwap_returnUnspecifiedInBeforeSwapAndAfterSwap() external { + // add some liquidity first in case the pool is empty + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10000 ether, salt: 0}), + abi.encode(10000 ether) + ); + + currency1.transfer(address(clReturnsDeltaHook), 1 ether); + + uint256 amt0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + uint128 liquidityBefore = poolManager.getLiquidity(key.toId()); + + (BalanceDelta delta) = router.swap( + key, + ICLPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: 1 ether, + sqrtPriceLimitX96: TickMath.MIN_SQRT_RATIO + 1 + }), + CLPoolManagerRouter.SwapTestSettings({withdrawTokens: true, settleUsingTransfer: true}), + abi.encode(-1 ether, 0.5 ether, 0.5 ether) + ); + + uint256 amt0After = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + uint128 liquidityAfter = poolManager.getLiquidity(key.toId()); + + // user pays 1 ether of currency0 to hook and no swap happens + + // trader's payment & return + assertEq(delta.amount0(), 1 ether); + assertEq(delta.amount1(), -1 ether); + + // hook's payment & return + assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(clReturnsDeltaHook)), 1 ether); + assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(clReturnsDeltaHook)), 0 ether); + + assertEq(amt0Before, amt0After); + assertEq(amt1Before, amt1After); + assertEq(liquidityBefore, liquidityAfter); + } + + function testSwap_SwapMore() external { + // add some liquidity first in case the pool is empty + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10000 ether, salt: 0}), + abi.encode(10000 ether) + ); + + uint256 amt0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + uint128 liquidityBefore = poolManager.getLiquidity(key.toId()); + + // make sure hook has enough balance to pay + currency0.transfer(address(clReturnsDeltaHook), 1 ether); + + (BalanceDelta delta) = router.swap( + key, + ICLPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: 1 ether, + sqrtPriceLimitX96: TickMath.MIN_SQRT_RATIO + 1 + }), + CLPoolManagerRouter.SwapTestSettings({withdrawTokens: true, settleUsingTransfer: true}), + // double the swap amt + abi.encode(1 ether, 0, 0) + ); + + uint256 amt0After = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + uint128 liquidityAfter = poolManager.getLiquidity(key.toId()); + + // trader's payment & return + assertEq(delta.amount0(), 1 ether); + assertApproxEqRel(delta.amount1(), -2 ether, 1e16); + + // hook's payment & return + assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(clReturnsDeltaHook)), 0 ether); + + assertEq(amt0After - amt0Before, 2 ether); + assertApproxEqRel(amt1Before - amt1After, 2 ether, 1e16); + assertEq(liquidityBefore, liquidityAfter); + } + + function testSwap_SwapLess() external { + // add some liquidity first in case the pool is empty + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10000 ether, salt: 0}), + abi.encode(10000 ether) + ); + + uint256 amt0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + uint128 liquidityBefore = poolManager.getLiquidity(key.toId()); + + (BalanceDelta delta) = router.swap( + key, + ICLPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: 1 ether, + sqrtPriceLimitX96: TickMath.MIN_SQRT_RATIO + 1 + }), + CLPoolManagerRouter.SwapTestSettings({withdrawTokens: true, settleUsingTransfer: true}), + abi.encode(-0.5 ether, 0, 0) + ); + + uint256 amt0After = IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)); + uint256 amt1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)); + uint128 liquidityAfter = poolManager.getLiquidity(key.toId()); + + // trader's payment & return + assertEq(delta.amount0(), 1 ether); + assertApproxEqRel(delta.amount1(), -0.5 ether, 1e16); + + // hook's payment & return + assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(clReturnsDeltaHook)), 0.5 ether); + + assertEq(amt0After - amt0Before, 0.5 ether); + assertApproxEqRel(amt1Before - amt1After, 0.5 ether, 1e16); + assertEq(liquidityBefore, liquidityAfter); + } +} diff --git a/test/pool-cl/CLHookReturnsFee.sol b/test/pool-cl/CLHookReturnsFee.sol new file mode 100644 index 00000000..e6a9a30f --- /dev/null +++ b/test/pool-cl/CLHookReturnsFee.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {IVault} from "../../src/interfaces/IVault.sol"; +import {PoolId, PoolIdLibrary} from "../../src/types/PoolId.sol"; +import {Hooks} from "../../src/libraries/Hooks.sol"; +import {LPFeeLibrary} from "../../src/libraries/LPFeeLibrary.sol"; +import {IProtocolFees} from "../../src/interfaces/IProtocolFees.sol"; +import {ICLHooks} from "../../src/pool-cl/interfaces/ICLHooks.sol"; +import {PoolKey} from "../../src/types/PoolKey.sol"; +import {CLPoolManager} from "../../src/pool-cl/CLPoolManager.sol"; +import {ICLPoolManager} from "../../src/pool-cl/interfaces/ICLPoolManager.sol"; +import {Deployers} from "./helpers/Deployers.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {CLDynamicReturnsFeeHook} from "./helpers/CLDynamicReturnsFeeHook.sol"; +import {Currency, CurrencyLibrary} from "../../src/types/Currency.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {FullMath} from "../../src/pool-cl/libraries/FullMath.sol"; +import {BalanceDelta} from "../../src/types/BalanceDelta.sol"; +import {TokenFixture} from "../helpers/TokenFixture.sol"; +import {CLPoolManagerRouter} from "./helpers/CLPoolManagerRouter.sol"; +import {CLPoolParametersHelper} from "../../src/pool-cl/libraries/CLPoolParametersHelper.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract CLHookReturnsFeeTest is Test, Deployers, TokenFixture, GasSnapshot { + using PoolIdLibrary for PoolKey; + using LPFeeLibrary for uint24; + + IVault vault; + ICLPoolManager poolManager; + CLDynamicReturnsFeeHook dynamicReturnsFeesHook; + CLPoolManagerRouter router; + + PoolKey key; + + event Swap( + PoolId indexed poolId, + address sender, + int128 amount0, + int128 amount1, + uint160 sqrtPriceX96, + uint128 liquidity, + int24 tick, + uint24 fee + ); + + function setUp() public { + dynamicReturnsFeesHook = new CLDynamicReturnsFeeHook(); + + (vault, poolManager) = createFreshManager(); + dynamicReturnsFeesHook.setManager(poolManager); + router = new CLPoolManagerRouter(vault, poolManager); + + initializeTokens(); + key = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: dynamicReturnsFeesHook, + poolManager: poolManager, + fee: LPFeeLibrary.DYNAMIC_FEE_FLAG, + parameters: CLPoolParametersHelper.setTickSpacing( + bytes32(uint256(dynamicReturnsFeesHook.getHooksRegistrationBitmap())), 1 + ) + }); + + poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + + IERC20(Currency.unwrap(currency0)).approve(address(router), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(router), type(uint256).max); + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10000 ether, salt: 0}), + ZERO_BYTES + ); + } + + function test_fuzz_dynamicReturnSwapFee(uint24 fee) public { + // hook will handle adding the override flag + dynamicReturnsFeesHook.setFee(fee); + + uint24 actualFee = fee.removeOverrideFlag(); + + int256 amountSpecified = 10000; + BalanceDelta result; + if (actualFee > LPFeeLibrary.ONE_HUNDRED_PERCENT_FEE) { + vm.expectRevert(LPFeeLibrary.FeeTooLarge.selector); + result = router.swap( + key, + ICLPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: SQRT_RATIO_1_2 + }), + CLPoolManagerRouter.SwapTestSettings({withdrawTokens: true, settleUsingTransfer: true}), + ZERO_BYTES + ); + return; + } else { + result = router.swap( + key, + ICLPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: SQRT_RATIO_1_2 + }), + CLPoolManagerRouter.SwapTestSettings({withdrawTokens: true, settleUsingTransfer: true}), + ZERO_BYTES + ); + } + assertEq(result.amount0(), amountSpecified); + + assertApproxEqAbs( + uint256(-int256(result.amount1())), FullMath.mulDiv(uint256(amountSpecified), (1e6 - actualFee), 1e6), 1 wei + ); + } + + function test_dynamicReturnSwapFee_initializeZeroSwapFee() public { + key.parameters = CLPoolParametersHelper.setTickSpacing( + bytes32(uint256(dynamicReturnsFeesHook.getHooksRegistrationBitmap())), 10 + ); + poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + assertEq(_fetchPoolSwapFee(key), 0); + } + + function test_dynamicReturnSwapFee_notUsedIfPoolIsStaticFee() public { + key.fee = 3000; // static fee + dynamicReturnsFeesHook.setFee(1000); // 0.10% fee is NOT used because the pool has a static fee + + poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + + IERC20(Currency.unwrap(currency0)).approve(address(router), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(router), type(uint256).max); + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10000 ether, salt: 0}), + ZERO_BYTES + ); + + assertEq(_fetchPoolSwapFee(key), 3000); + + // despite returning a valid swap fee (1000), the static fee is used + int256 amountSpecified = 10000; + BalanceDelta result = router.swap( + key, + ICLPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: SQRT_RATIO_1_2 + }), + CLPoolManagerRouter.SwapTestSettings({withdrawTokens: true, settleUsingTransfer: true}), + ZERO_BYTES + ); + + // after swapping ~1:1, the amount out (amount1) should be approximately 0.30% less than the amount specified + assertEq(result.amount0(), amountSpecified); + assertApproxEqAbs( + uint256(-int256(result.amount1())), FullMath.mulDiv(uint256(amountSpecified), (1e6 - 3000), 1e6), 1 wei + ); + } + + function test_dynamicReturnSwapFee_notStored() public { + // fees returned by beforeSwap are not written to storage + + // create a new pool with an initial fee of 123 + key.parameters = CLPoolParametersHelper.setTickSpacing( + bytes32(uint256(dynamicReturnsFeesHook.getHooksRegistrationBitmap())), 10 + ); + poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + + IERC20(Currency.unwrap(currency0)).approve(address(router), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(router), type(uint256).max); + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -10, tickUpper: 10, liquidityDelta: 10000 ether, salt: 0}), + ZERO_BYTES + ); + uint24 initialFee = 123; + dynamicReturnsFeesHook.forcePoolFeeUpdate(key, initialFee); + assertEq(_fetchPoolSwapFee(key), initialFee); + + // swap with a different fee + uint24 newFee = 3000; + dynamicReturnsFeesHook.setFee(newFee); + + int256 amountSpecified = 10000; + BalanceDelta result = router.swap( + key, + ICLPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: SQRT_RATIO_1_2 + }), + CLPoolManagerRouter.SwapTestSettings({withdrawTokens: true, settleUsingTransfer: true}), + ZERO_BYTES + ); + assertApproxEqAbs( + uint256(-int256(result.amount1())), FullMath.mulDiv(uint256(amountSpecified), (1e6 - newFee), 1e6), 1 wei + ); + + // the fee from beforeSwap is not stored + assertEq(_fetchPoolSwapFee(key), initialFee); + } + + function test_dynamicReturnSwapFee_revertIfFeeTooLarge() public { + assertEq(_fetchPoolSwapFee(key), 0); + + // hook adds the override flag + dynamicReturnsFeesHook.setFee(1000001); + + // a large fee is not used + int256 amountSpecified = 10000; + vm.expectRevert(LPFeeLibrary.FeeTooLarge.selector); + router.swap( + key, + ICLPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: SQRT_RATIO_1_2 + }), + CLPoolManagerRouter.SwapTestSettings({withdrawTokens: true, settleUsingTransfer: true}), + ZERO_BYTES + ); + } + + function _fetchPoolSwapFee(PoolKey memory _key) internal view returns (uint256 swapFee) { + PoolId id = _key.toId(); + (,,, swapFee) = poolManager.getSlot0(id); + } +} diff --git a/test/pool-cl/CLHookSkipCallback.t.sol b/test/pool-cl/CLHookSkipCallback.t.sol index d019e445..95cd91f1 100644 --- a/test/pool-cl/CLHookSkipCallback.t.sol +++ b/test/pool-cl/CLHookSkipCallback.t.sol @@ -74,10 +74,14 @@ contract CLHookSkipCallbackTest is Test, Deployers, TokenFixture, GasSnapshot { // Add and remove liquidity clSkipCallbackHook.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: 1e18}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: 1e18, salt: 0}), + "" ); clSkipCallbackHook.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: -1e18}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: -1e18, salt: 0}), + "" ); assertEq(clSkipCallbackHook.hookCounterCallbackCount(), 0); } @@ -87,10 +91,14 @@ contract CLHookSkipCallbackTest is Test, Deployers, TokenFixture, GasSnapshot { // Add and remove liquidity router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: 1e18}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: 1e18, salt: 0}), + "" ); router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: -1e18}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: -1e18, salt: 0}), + "" ); assertEq(clSkipCallbackHook.hookCounterCallbackCount(), 4); } @@ -100,7 +108,9 @@ contract CLHookSkipCallbackTest is Test, Deployers, TokenFixture, GasSnapshot { // Pre-req add some liqudiity clSkipCallbackHook.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: 1e18}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: 1e18, salt: 0}), + "" ); clSkipCallbackHook.swap( @@ -118,7 +128,9 @@ contract CLHookSkipCallbackTest is Test, Deployers, TokenFixture, GasSnapshot { // Pre-req add some liqudiity clSkipCallbackHook.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: 1e18}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: 1e18, salt: 0}), + "" ); router.swap( @@ -136,7 +148,9 @@ contract CLHookSkipCallbackTest is Test, Deployers, TokenFixture, GasSnapshot { // Pre-req add some liqudiity clSkipCallbackHook.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: 1e18}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: 1e18, salt: 0}), + "" ); clSkipCallbackHook.donate(key, 100, 200, ZERO_BYTES); @@ -149,7 +163,9 @@ contract CLHookSkipCallbackTest is Test, Deployers, TokenFixture, GasSnapshot { // Pre-req add some liqudiity clSkipCallbackHook.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: 1e18}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -100, tickUpper: 100, liquidityDelta: 1e18, salt: 0}), + "" ); router.donate(key, 100, 200, ZERO_BYTES); diff --git a/test/pool-cl/CLPoolManager.t.sol b/test/pool-cl/CLPoolManager.t.sol index d6ce2a93..344537ab 100644 --- a/test/pool-cl/CLPoolManager.t.sol +++ b/test/pool-cl/CLPoolManager.t.sol @@ -16,7 +16,7 @@ import {PoolId, PoolIdLibrary} from "../../src/types/PoolId.sol"; import {IHooks} from "../../src/interfaces/IHooks.sol"; import {TickMath} from "../../src/pool-cl/libraries/TickMath.sol"; import {IProtocolFees} from "../../src/interfaces/IProtocolFees.sol"; -import {ICLHooks, HOOKS_AFTER_INITIALIZE_OFFSET} from "../../src/pool-cl/interfaces/ICLHooks.sol"; +import "../../src/pool-cl/interfaces/ICLHooks.sol"; import {Hooks} from "../../src/libraries/Hooks.sol"; import {CLPoolManagerRouter} from "./helpers/CLPoolManagerRouter.sol"; import {FixedPoint96} from "../../src/pool-cl/libraries/FixedPoint96.sol"; @@ -33,7 +33,6 @@ import {NonStandardERC20} from "./helpers/NonStandardERC20.sol"; import {ProtocolFeeControllerTest} from "./helpers/ProtocolFeeControllerTest.sol"; import {IProtocolFeeController} from "../../src/interfaces/IProtocolFeeController.sol"; import {CLFeeManagerHook} from "./helpers/CLFeeManagerHook.sol"; -import {CLNoOpTestHook} from "./helpers/CLNoOpTestHook.sol"; import {ProtocolFeeLibrary} from "../../src/libraries/ProtocolFeeLibrary.sol"; import {SafeCast} from "../../src/libraries/SafeCast.sol"; import {NoIsolate} from "../helpers/NoIsolate.sol"; @@ -44,6 +43,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps using CLPoolParametersHelper for bytes32; using ParametersHelper for bytes32; using LPFeeLibrary for uint24; + using Hooks for bytes32; event Initialize( PoolId indexed id, @@ -54,7 +54,12 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps ICLHooks hooks ); event ModifyLiquidity( - PoolId indexed poolId, address indexed sender, int24 tickLower, int24 tickUpper, int256 liquidityDelta + PoolId indexed poolId, + address indexed sender, + int24 tickLower, + int24 tickUpper, + bytes32 salt, + int256 liquidityDelta ); event Swap( PoolId indexed poolId, @@ -242,6 +247,46 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps } } + function testInitialize_HookValidation() external { + MockHooks hookAddr = new MockHooks(); + + // hook config + { + PoolKey memory key = PoolKey({ + currency0: Currency.wrap(makeAddr("token0")), + currency1: Currency.wrap(makeAddr("token1")), + hooks: IHooks(hookAddr), + poolManager: poolManager, + fee: uint24(3000), + // 0 ~ 15 hookRegistrationMap = 0x1 + // 16 ~ 24 tickSpacing = 10 + parameters: bytes32(uint256(0xa0001)) + }); + + vm.expectRevert(abi.encodeWithSelector(Hooks.HookConfigValidationError.selector)); + poolManager.initialize(key, TickMath.MIN_SQRT_RATIO, new bytes(0)); + } + + // hook permission + { + // beforeSwap is disabled but beforeSwapReturnsDelta is enabled + hookAddr.setHooksRegistrationBitmap(uint16(1 << HOOKS_BEFORE_SWAP_RETURNS_DELTA_OFFSET)); + PoolKey memory key = PoolKey({ + currency0: Currency.wrap(makeAddr("token0")), + currency1: Currency.wrap(makeAddr("token1")), + hooks: IHooks(hookAddr), + poolManager: poolManager, + fee: uint24(3000), + // 0 ~ 15 hookRegistrationMap = + // 16 ~ 24 tickSpacing = 10 + parameters: bytes32(uint256(0xa0000) | hookAddr.getHooksRegistrationBitmap()) + }); + + vm.expectRevert(abi.encodeWithSelector(Hooks.HookPermissionsValidationError.selector)); + poolManager.initialize(key, TickMath.MIN_SQRT_RATIO, new bytes(0)); + } + } + function testInitialize_stateCheck() external { PoolKey memory key = PoolKey({ currency0: Currency.wrap(makeAddr("token0")), @@ -311,7 +356,10 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps } else if (!_validateHookConfig(key)) { vm.expectRevert(abi.encodeWithSelector(Hooks.HookConfigValidationError.selector)); poolManager.initialize(key, sqrtPriceX96, ZERO_BYTES); - } else if (key.fee & LPFeeLibrary.STATIC_FEE_MASK > LPFeeLibrary.ONE_HUNDRED_PERCENT_FEE) { + } else if (!_validateHookPermissionsConflict(key)) { + vm.expectRevert(abi.encodeWithSelector(Hooks.HookPermissionsValidationError.selector)); + poolManager.initialize(key, sqrtPriceX96, ZERO_BYTES); + } else if (key.fee & LPFeeLibrary.FEE_MASK > LPFeeLibrary.ONE_HUNDRED_PERCENT_FEE) { vm.expectRevert(abi.encodeWithSelector(IProtocolFees.FeeTooLarge.selector)); poolManager.initialize(key, sqrtPriceX96, ZERO_BYTES); } else { @@ -628,24 +676,6 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); } - function test_initialize_failsNoOpMissingBeforeCall() public { - uint16 bitMap = 0x0400; // 0000 0100 0000 0000 (only noOp) - - CLNoOpTestHook noOpHook = new CLNoOpTestHook(); - noOpHook.setHooksRegistrationBitmap(bitMap); - PoolKey memory key = PoolKey({ - currency0: currency0, - currency1: currency1, - fee: 3000, - hooks: IHooks(noOpHook), - poolManager: poolManager, - parameters: bytes32(uint256((60 << 16) | noOpHook.getHooksRegistrationBitmap())) - }); - - vm.expectRevert(Hooks.NoOpHookMissingBeforeCall.selector); - poolManager.initialize(key, TickMath.MIN_SQRT_RATIO, new bytes(0)); - } - // ************** *************** // // ************** modifyPosition *************** // // ************** *************** // @@ -681,7 +711,8 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps ICLPoolManager.ModifyLiquidityParams({ tickLower: TickMath.MIN_TICK, tickUpper: TickMath.MAX_TICK, - liquidityDelta: 1e24 + liquidityDelta: 1e24, + salt: 0 }), "" ); @@ -698,15 +729,17 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps assertEq(1e10 ether - token1Left, 9999999999999999999945788); assertEq(poolManager.getLiquidity(key.toId()), 1e24); - assertEq(poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK), 1e24); + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, 0), 1e24 + ); assertEq( - poolManager.getPosition(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK) + poolManager.getPosition(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, 0) .feeGrowthInside0LastX128, 0 ); assertEq( - poolManager.getPosition(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK) + poolManager.getPosition(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, 0) .feeGrowthInside1LastX128, 0 ); @@ -718,7 +751,8 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps ICLPoolManager.ModifyLiquidityParams({ tickLower: TickMath.MIN_TICK, tickUpper: TickMath.MAX_TICK, - liquidityDelta: 1e4 + liquidityDelta: 1e4, + salt: 0 }), "" ); @@ -736,16 +770,17 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps assertEq(poolManager.getLiquidity(key.toId()), 1e24 + 1e4); assertEq( - poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK), 1e24 + 1e4 + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, 0), + 1e24 + 1e4 ); assertEq( - poolManager.getPosition(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK) + poolManager.getPosition(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, 0) .feeGrowthInside0LastX128, 0 ); assertEq( - poolManager.getPosition(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK) + poolManager.getPosition(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, 0) .feeGrowthInside1LastX128, 0 ); @@ -783,7 +818,8 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps ICLPoolManager.ModifyLiquidityParams({ tickLower: TickMath.MIN_TICK, tickUpper: TickMath.MAX_TICK, - liquidityDelta: 1e18 + liquidityDelta: 1e18, + salt: 0 }), "" ); @@ -795,7 +831,8 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps ICLPoolManager.ModifyLiquidityParams({ tickLower: TickMath.MIN_TICK, tickUpper: TickMath.MAX_TICK, - liquidityDelta: 1e18 + liquidityDelta: 1e18, + salt: 0 }), "" ); @@ -807,7 +844,8 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps ICLPoolManager.ModifyLiquidityParams({ tickLower: TickMath.MIN_TICK, tickUpper: TickMath.MAX_TICK, - liquidityDelta: -1e18 + liquidityDelta: -1e18, + salt: 0 }), "" ); @@ -829,11 +867,14 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps ICLPoolManager.ModifyLiquidityParams({ tickLower: TickMath.MIN_TICK, tickUpper: TickMath.MAX_TICK, - liquidityDelta: 1e18 + liquidityDelta: 1e18, + salt: 0 }), "" ); - assertApproxEqRel(uint256(int256(feeDelta.amount0())), 0.003 * 0.1 ether, 1e16); // around 0.3% fee + + // amt0 & amt1 are non positive i.e. the pool owes us tokens + assertApproxEqRel(uint256(-int256(feeDelta.amount0())), 0.003 * 0.1 ether, 1e16); // around 0.3% fee // step 5: Add liquidity, verify feeDelta == 0 (, feeDelta) = router.modifyPosition( @@ -841,7 +882,8 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps ICLPoolManager.ModifyLiquidityParams({ tickLower: TickMath.MIN_TICK, tickUpper: TickMath.MAX_TICK, - liquidityDelta: 1e18 + liquidityDelta: 1e18, + salt: 0 }), "" ); @@ -863,11 +905,14 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps ICLPoolManager.ModifyLiquidityParams({ tickLower: TickMath.MIN_TICK, tickUpper: TickMath.MAX_TICK, - liquidityDelta: -1e18 + liquidityDelta: -1e18, + salt: 0 }), "" ); - assertApproxEqRel(uint256(int256(feeDelta.amount0())), 0.003 * 0.1 ether, 1e16); // around 0.3% fee + + // amt0 & amt1 are non positive i.e. the pool owes us tokens + assertApproxEqRel(uint256(-int256(feeDelta.amount0())), 0.003 * 0.1 ether, 1e16); // around 0.3% fee } function testModifyPosition_Liquidity_aboveCurrentTick() external { @@ -896,7 +941,9 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps IERC20(Currency.unwrap(currency1)).approve(address(router), 1e30 ether); router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: 46055, tickUpper: 46060, liquidityDelta: 1e9}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: 46055, tickUpper: 46060, liquidityDelta: 1e9, salt: 0}), + "" ); uint256 token0Left = IERC20(Currency.unwrap(currency0)).balanceOf(address(this)); @@ -910,10 +957,10 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps // no active liquidity assertEq(poolManager.getLiquidity(key.toId()), 0); - assertEq(poolManager.getLiquidity(key.toId(), address(router), 46055, 46060), 1e9); + assertEq(poolManager.getLiquidity(key.toId(), address(router), 46055, 46060, 0), 1e9); - assertEq(poolManager.getPosition(key.toId(), address(this), 46055, 46060).feeGrowthInside0LastX128, 0); - assertEq(poolManager.getPosition(key.toId(), address(this), 46055, 46060).feeGrowthInside1LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(this), 46055, 46060, 0).feeGrowthInside0LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(this), 46055, 46060, 0).feeGrowthInside1LastX128, 0); } function testModifyPosition_addLiquidity_belowCurrentTick() external { @@ -942,7 +989,9 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps IERC20(Currency.unwrap(currency1)).approve(address(router), 1e30 ether); router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: 46000, tickUpper: 46050, liquidityDelta: 1e9}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: 46000, tickUpper: 46050, liquidityDelta: 1e9, salt: 0}), + "" ); uint256 token0Left = IERC20(Currency.unwrap(currency0)).balanceOf(address(this)); @@ -956,10 +1005,10 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps // no active liquidity assertEq(poolManager.getLiquidity(key.toId()), 0); - assertEq(poolManager.getLiquidity(key.toId(), address(router), 46000, 46050), 1e9); + assertEq(poolManager.getLiquidity(key.toId(), address(router), 46000, 46050, 0), 1e9); - assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050).feeGrowthInside0LastX128, 0); - assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050).feeGrowthInside1LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050, 0).feeGrowthInside0LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050, 0).feeGrowthInside1LastX128, 0); } function testModifyPosition_removeLiquidity_fromEmpty() external { @@ -989,7 +1038,9 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps vm.expectRevert(SafeCast.SafeCastOverflow.selector); router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: 46000, tickUpper: 46050, liquidityDelta: -1}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: 46000, tickUpper: 46050, liquidityDelta: -1, salt: 0}), + "" ); } @@ -1020,7 +1071,9 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps vm.expectRevert(CLPosition.CannotUpdateEmptyPosition.selector); router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: 46000, tickUpper: 46050, liquidityDelta: 0}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: 46000, tickUpper: 46050, liquidityDelta: 0, salt: 0}), + "" ); } @@ -1050,33 +1103,39 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps IERC20(Currency.unwrap(currency1)).approve(address(router), 1e30 ether); router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: -1, tickUpper: 1, liquidityDelta: 100 ether}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -1, tickUpper: 1, liquidityDelta: 100 ether, salt: 0}), + "" ); assertEq(poolManager.getLiquidity(key.toId()), 100 ether, "total liquidity should be 1000"); assertEq( - poolManager.getLiquidity(key.toId(), address(router), -1, 1), 100 ether, "router's liquidity should be 1000" + poolManager.getLiquidity(key.toId(), address(router), -1, 1, 0), + 100 ether, + "router's liquidity should be 1000" ); assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)), 4999625031247266); assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)), 4999625031247266); - assertEq(poolManager.getPosition(key.toId(), address(router), -1, 1).feeGrowthInside0LastX128, 0); - assertEq(poolManager.getPosition(key.toId(), address(router), -1, 1).feeGrowthInside1LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(router), -1, 1, 0).feeGrowthInside0LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(router), -1, 1, 0).feeGrowthInside1LastX128, 0); router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: -1, tickUpper: 1, liquidityDelta: -100 ether}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -1, tickUpper: 1, liquidityDelta: -100 ether, salt: 0}), + "" ); assertEq(poolManager.getLiquidity(key.toId()), 0); - assertEq(poolManager.getLiquidity(key.toId(), address(router), -1, 1), 0); + assertEq(poolManager.getLiquidity(key.toId(), address(router), -1, 1, 0), 0); // expected to receive 0, but got 1 because of precision loss assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)), 1); assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)), 1); - assertEq(poolManager.getPosition(key.toId(), address(router), -1, 1).feeGrowthInside0LastX128, 0); - assertEq(poolManager.getPosition(key.toId(), address(router), -1, 1).feeGrowthInside1LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(router), -1, 1, 0).feeGrowthInside0LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(router), -1, 1, 0).feeGrowthInside1LastX128, 0); } function testModifyPosition_removeLiquidity_halfAndThenAll() external { @@ -1105,7 +1164,9 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps IERC20(Currency.unwrap(currency1)).approve(address(router), 1e30 ether); router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: 46000, tickUpper: 46050, liquidityDelta: 1e9}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: 46000, tickUpper: 46050, liquidityDelta: 1e9, salt: 0}), + "" ); { @@ -1120,17 +1181,17 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps // no active liquidity assertEq(poolManager.getLiquidity(key.toId()), 0); - assertEq(poolManager.getLiquidity(key.toId(), address(router), 46000, 46050), 1e9); + assertEq(poolManager.getLiquidity(key.toId(), address(router), 46000, 46050, 0), 1e9); - assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050).feeGrowthInside0LastX128, 0); - assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050).feeGrowthInside1LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050, 0).feeGrowthInside0LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050, 0).feeGrowthInside1LastX128, 0); } // remove half snapStart("CLPoolManagerTest#removeLiquidity_toNonEmpty"); router.modifyPosition( key, - ICLPoolManager.ModifyLiquidityParams({tickLower: 46000, tickUpper: 46050, liquidityDelta: -5 * 1e8}), + ICLPoolManager.ModifyLiquidityParams({tickLower: 46000, tickUpper: 46050, liquidityDelta: -5 * 1e8, salt: 0}), "" ); snapEnd(); @@ -1145,15 +1206,15 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps // no active liquidity assertEq(poolManager.getLiquidity(key.toId()), 0); - assertEq(poolManager.getLiquidity(key.toId(), address(router), 46000, 46050), 5 * 1e8); + assertEq(poolManager.getLiquidity(key.toId(), address(router), 46000, 46050, 0), 5 * 1e8); - assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050).feeGrowthInside0LastX128, 0); - assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050).feeGrowthInside1LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050, 0).feeGrowthInside0LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050, 0).feeGrowthInside1LastX128, 0); } router.modifyPosition( key, - ICLPoolManager.ModifyLiquidityParams({tickLower: 46000, tickUpper: 46050, liquidityDelta: -5 * 1e8}), + ICLPoolManager.ModifyLiquidityParams({tickLower: 46000, tickUpper: 46050, liquidityDelta: -5 * 1e8, salt: 0}), "" ); @@ -1169,10 +1230,10 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps // no active liquidity assertEq(poolManager.getLiquidity(key.toId()), 0); - assertEq(poolManager.getLiquidity(key.toId(), address(router), 46000, 46050), 0); + assertEq(poolManager.getLiquidity(key.toId(), address(router), 46000, 46050, 0), 0); - assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050).feeGrowthInside0LastX128, 0); - assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050).feeGrowthInside1LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050, 0).feeGrowthInside0LastX128, 0); + assertEq(poolManager.getPosition(key.toId(), address(router), 46000, 46050, 0).feeGrowthInside1LastX128, 0); } } @@ -1187,7 +1248,9 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); vm.expectRevert(); router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100}), ZERO_BYTES + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100, salt: 0}), + ZERO_BYTES ); } @@ -1206,10 +1269,12 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps poolManager.initialize(key, sqrtPriceX96, ZERO_BYTES); vm.expectEmit(true, true, true, true); - emit ModifyLiquidity(key.toId(), address(router), 0, 60, 100); + emit ModifyLiquidity(key.toId(), address(router), 0, 60, 0, 100); router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100}), ZERO_BYTES + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100, salt: 0}), + ZERO_BYTES ); } @@ -1227,10 +1292,12 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps poolManager.initialize(key, sqrtPriceX96, ZERO_BYTES); vm.expectEmit(true, true, true, true); - emit ModifyLiquidity(key.toId(), address(router), 0, 60, 100); + emit ModifyLiquidity(key.toId(), address(router), 0, 60, 0, 100); router.modifyPosition{value: 100}( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100}), ZERO_BYTES + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100, salt: 0}), + ZERO_BYTES ); } @@ -1249,7 +1316,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); ICLPoolManager.ModifyLiquidityParams memory params = - ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100}); + ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100, salt: 0}); poolManager.initialize(key, sqrtPriceX96, ZERO_BYTES); @@ -1289,7 +1356,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); ICLPoolManager.ModifyLiquidityParams memory params = - ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100}); + ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100, salt: 0}); poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); @@ -1319,7 +1386,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); ICLPoolManager.ModifyLiquidityParams memory params = - ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100}); + ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100, salt: 0}); poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); @@ -1327,7 +1394,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps mockHooks.setReturnValue(mockHooks.afterAddLiquidity.selector, mockHooks.afterAddLiquidity.selector); vm.expectEmit(true, true, true, true); - emit ModifyLiquidity(key.toId(), address(router), 0, 60, 100); + emit ModifyLiquidity(key.toId(), address(router), 0, 60, 0, 100); router.modifyPosition(key, params, ZERO_BYTES); } @@ -1346,11 +1413,213 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps snapStart("CLPoolManagerTest#addLiquidity_nativeToken"); router.modifyPosition{value: 100}( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100}), ZERO_BYTES + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100, salt: 0}), + ZERO_BYTES ); snapEnd(); } + function testModifyPosition_withSalt_addAndRemove() external { + bytes32 salt = bytes32(uint256(0x1234)); + Currency currency0 = Currency.wrap(address(new ERC20PresetFixedSupply("C0", "C0", 1e10 ether, address(this)))); + Currency currency1 = Currency.wrap(address(new ERC20PresetFixedSupply("C1", "C1", 1e10 ether, address(this)))); + + if (currency0 > currency1) { + (currency0, currency1) = (currency1, currency0); + } + + PoolKey memory key = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: IHooks(address(0)), + poolManager: poolManager, + fee: uint24(3000), + // 0 ~ 15 hookRegistrationMap = nil + // 16 ~ 24 tickSpacing = 1 + parameters: bytes32(uint256(0x10000)) + }); + + // price = 100 tick roughly 46054 + poolManager.initialize(key, uint160(10 * FixedPoint96.Q96), new bytes(0)); + + IERC20(Currency.unwrap(currency0)).approve(address(router), 1e10 ether); + IERC20(Currency.unwrap(currency1)).approve(address(router), 1e10 ether); + + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({ + tickLower: TickMath.MIN_TICK, + tickUpper: TickMath.MAX_TICK, + liquidityDelta: 1e24, + salt: salt + }), + "" + ); + + { + assertEq(poolManager.getLiquidity(key.toId()), 1e24); + + // salt = 0 returns nothing + assertEq(poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, 0), 0); + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, salt), 1e24 + ); + } + + // add into that position + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({ + tickLower: TickMath.MIN_TICK, + tickUpper: TickMath.MAX_TICK, + liquidityDelta: 1e4, + salt: salt + }), + "" + ); + + { + assertEq(poolManager.getLiquidity(key.toId()), 1e24 + 1e4); + assertEq(poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, 0), 0); + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, salt), + 1e24 + 1e4 + ); + } + + // decrease liquidity + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({ + tickLower: TickMath.MIN_TICK, + tickUpper: TickMath.MAX_TICK, + liquidityDelta: -1e4, + salt: salt + }), + "" + ); + + { + assertEq(poolManager.getLiquidity(key.toId()), 1e24); + assertEq(poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, 0), 0); + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, salt), 1e24 + ); + } + } + + function testModifyPosition_mixWithAndWithoutSalt() external { + bytes32 salt0 = bytes32(0); + bytes32 salt1 = bytes32(uint256(0x1234)); + bytes32 salt2 = bytes32(uint256(0x5678)); + + Currency currency0 = Currency.wrap(address(new ERC20PresetFixedSupply("C0", "C0", 1e10 ether, address(this)))); + Currency currency1 = Currency.wrap(address(new ERC20PresetFixedSupply("C1", "C1", 1e10 ether, address(this)))); + + if (currency0 > currency1) { + (currency0, currency1) = (currency1, currency0); + } + + PoolKey memory key = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: IHooks(address(0)), + poolManager: poolManager, + fee: uint24(3000), + // 0 ~ 15 hookRegistrationMap = nil + // 16 ~ 24 tickSpacing = 1 + parameters: bytes32(uint256(0x10000)) + }); + + // price = 100 tick roughly 46054 + poolManager.initialize(key, uint160(10 * FixedPoint96.Q96), new bytes(0)); + + IERC20(Currency.unwrap(currency0)).approve(address(router), 1e10 ether); + IERC20(Currency.unwrap(currency1)).approve(address(router), 1e10 ether); + + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({ + tickLower: TickMath.MIN_TICK, + tickUpper: TickMath.MAX_TICK, + liquidityDelta: 1e24, + salt: salt1 + }), + "" + ); + + { + assertEq(poolManager.getLiquidity(key.toId()), 1e24); + + // both salt0 & salt2 remains 0 + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, salt0), 0 + ); + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, salt1), 1e24 + ); + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, salt2), 0 + ); + } + + // add into another salt + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({ + tickLower: TickMath.MIN_TICK, + tickUpper: TickMath.MAX_TICK, + liquidityDelta: 1e4, + salt: salt2 + }), + "" + ); + + { + assertEq(poolManager.getLiquidity(key.toId()), 1e24 + 1e4); + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, salt0), 0 + ); + // salt1 position should be untouched + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, salt1), 1e24 + ); + + // salt2 position should be updated + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, salt2), 1e4 + ); + } + + // add into another salt + router.modifyPosition( + key, + ICLPoolManager.ModifyLiquidityParams({ + tickLower: TickMath.MIN_TICK, + tickUpper: TickMath.MAX_TICK, + liquidityDelta: 1e10, + salt: salt0 + }), + "" + ); + + { + assertEq(poolManager.getLiquidity(key.toId()), 1e24 + 1e4 + 1e10); + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, salt0), 1e10 + ); + // salt1 & salt2 positions should be untouched + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, salt1), 1e24 + ); + + assertEq( + poolManager.getLiquidity(key.toId(), address(router), TickMath.MIN_TICK, TickMath.MAX_TICK, salt2), 1e4 + ); + } + } + // ************** *************** // // ************** swap *************** // // ************** *************** // @@ -1382,7 +1651,12 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps router.modifyPosition( key, - ICLPoolManager.ModifyLiquidityParams({tickLower: 46053, tickUpper: 46055, liquidityDelta: 1000000 ether}), + ICLPoolManager.ModifyLiquidityParams({ + tickLower: 46053, + tickUpper: 46055, + liquidityDelta: 1000000 ether, + salt: 0 + }), "" ); @@ -1444,7 +1718,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); ICLPoolManager.ModifyLiquidityParams memory modifyPositionParams = - ICLPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 1 ether}); + ICLPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 1 ether, salt: 0}); router.modifyPosition(key, modifyPositionParams, ZERO_BYTES); @@ -1479,7 +1753,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); ICLPoolManager.ModifyLiquidityParams memory modifyPositionParams = - ICLPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 1 ether}); + ICLPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 1 ether, salt: 0}); router.modifyPosition(key, modifyPositionParams, ZERO_BYTES); @@ -1520,7 +1794,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps ); ICLPoolManager.ModifyLiquidityParams memory modifyPositionParams = - ICLPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 1 ether}); + ICLPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 1 ether, salt: 0}); router.modifyPosition(key, modifyPositionParams, ZERO_BYTES); @@ -1628,7 +1902,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); ICLPoolManager.ModifyLiquidityParams memory params = - ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100}); + ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100, salt: 0}); ICLPoolManager.SwapParams memory swapParams = ICLPoolManager.SwapParams({zeroForOne: true, amountSpecified: 10, sqrtPriceLimitX96: SQRT_RATIO_1_2}); @@ -1665,7 +1939,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); ICLPoolManager.ModifyLiquidityParams memory params = - ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100}); + ICLPoolManager.ModifyLiquidityParams({tickLower: 0, tickUpper: 60, liquidityDelta: 100, salt: 0}); ICLPoolManager.SwapParams memory swapParams = ICLPoolManager.SwapParams({zeroForOne: true, amountSpecified: 10, sqrtPriceLimitX96: SQRT_RATIO_1_2}); @@ -1704,7 +1978,12 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); router.modifyPosition( key, - ICLPoolManager.ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 1000000000000000000}), + ICLPoolManager.ModifyLiquidityParams({ + tickLower: -120, + tickUpper: 120, + liquidityDelta: 1000000000000000000, + salt: 0 + }), ZERO_BYTES ); @@ -1735,7 +2014,12 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); router.modifyPosition( key, - ICLPoolManager.ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 1000000000000000000}), + ICLPoolManager.ModifyLiquidityParams({ + tickLower: -120, + tickUpper: 120, + liquidityDelta: 1000000000000000000, + salt: 0 + }), ZERO_BYTES ); vm.expectEmit(true, true, true, true); @@ -1864,7 +2148,12 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); router.modifyPosition( key, - ICLPoolManager.ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 1000000000000000000}), + ICLPoolManager.ModifyLiquidityParams({ + tickLower: -120, + tickUpper: 120, + liquidityDelta: 1000000000000000000, + salt: 0 + }), ZERO_BYTES ); @@ -1892,7 +2181,12 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); router.modifyPosition( key, - ICLPoolManager.ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 1000000000000000000}), + ICLPoolManager.ModifyLiquidityParams({ + tickLower: -120, + tickUpper: 120, + liquidityDelta: 1000000000000000000, + salt: 0 + }), ZERO_BYTES ); router.swap(key, params, testSettings, ZERO_BYTES); @@ -1933,7 +2227,12 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); router.modifyPosition( key, - ICLPoolManager.ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 1000000000000000000}), + ICLPoolManager.ModifyLiquidityParams({ + tickLower: -120, + tickUpper: 120, + liquidityDelta: 1000000000000000000, + salt: 0 + }), ZERO_BYTES ); @@ -1965,7 +2264,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); router.modifyPosition{value: 1 ether}( key, - ICLPoolManager.ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 1 ether}), + ICLPoolManager.ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 1 ether, salt: 0}), ZERO_BYTES ); @@ -2023,7 +2322,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100); + ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100, 0); router.modifyPosition(key, params, ZERO_BYTES); snapStart("CLPoolManagerTest#donateBothTokens"); router.donate(key, 100, 200, ZERO_BYTES); @@ -2047,7 +2346,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100); + ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100, 0); router.modifyPosition{value: 1}(key, params, ZERO_BYTES); router.donate{value: 100}(key, 100, 200, ZERO_BYTES); @@ -2073,7 +2372,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100); + ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100, 0); router.modifyPosition(key, params, ZERO_BYTES); mockHooks.setReturnValue(mockHooks.beforeDonate.selector, bytes4(0xdeadbeef)); mockHooks.setReturnValue(mockHooks.afterDonate.selector, bytes4(0xdeadbeef)); @@ -2105,7 +2404,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100); + ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100, 0); router.modifyPosition(key, params, ZERO_BYTES); mockHooks.setReturnValue(mockHooks.beforeDonate.selector, mockHooks.beforeDonate.selector); @@ -2125,7 +2424,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100); + ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100, 0); router.modifyPosition(key, params, ZERO_BYTES); (, int24 tick,,) = poolManager.getSlot0(key.toId()); @@ -2147,7 +2446,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100); + ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100, 0); router.modifyPosition(key, params, ZERO_BYTES); snapStart("CLPoolManagerTest#gasDonateOneToken"); @@ -2173,7 +2472,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps MockERC20(Currency.unwrap(currency0)).approve(address(router), type(uint256).max); poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 1000); + ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 1000, 0); router.modifyPosition(key, params, ZERO_BYTES); (uint256 amount0, uint256 amount1) = currency0Invalid ? (1, 0) : (0, 1); @@ -2197,7 +2496,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100); + ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100, 0); router.modifyPosition(key, params, ZERO_BYTES); router.take(key, 1, 1); // assertions inside router because it takes then settles } @@ -2213,7 +2512,7 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps }); poolManager.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100); + ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-60, 60, 100, 0); router.modifyPosition{value: 100}(key, params, ZERO_BYTES); router.take{value: 1}(key, 1, 1); // assertions inside router because it takes then settles } @@ -2282,7 +2581,8 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps (CLPool.Slot0 memory slot0,,,) = poolManager.pools(key.toId()); assertEq(slot0.protocolFee, protocolFee); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-120, 120, 10 ether); + ICLPoolManager.ModifyLiquidityParams memory params = + ICLPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0); router.modifyPosition(key, params, ZERO_BYTES); router.swap( key, @@ -2320,7 +2620,8 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps (CLPool.Slot0 memory slot0,,,) = poolManager.pools(key.toId()); assertEq(slot0.protocolFee, protocolFee); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-120, 120, 10 ether); + ICLPoolManager.ModifyLiquidityParams memory params = + ICLPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0); router.modifyPosition(key, params, ZERO_BYTES); router.swap( key, @@ -2359,7 +2660,8 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps (CLPool.Slot0 memory slot0,,,) = poolManager.pools(key.toId()); assertEq(slot0.protocolFee, protocolFee); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-120, 120, 10 ether); + ICLPoolManager.ModifyLiquidityParams memory params = + ICLPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0); router.modifyPosition{value: 10 ether}(key, params, ZERO_BYTES); router.swap{value: 10000}( key, @@ -2398,7 +2700,8 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps (CLPool.Slot0 memory slot0,,,) = poolManager.pools(key.toId()); assertEq(slot0.protocolFee, protocolFee); - ICLPoolManager.ModifyLiquidityParams memory params = ICLPoolManager.ModifyLiquidityParams(-120, 120, 10 ether); + ICLPoolManager.ModifyLiquidityParams memory params = + ICLPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0); router.modifyPosition{value: 10 ether}(key, params, ZERO_BYTES); router.swap{value: 10000}( key, @@ -2477,54 +2780,6 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps assertEq(swapFee, _swapFee); } - function testNoOp_gas() public { - uint16 bitMap = 0x0550; // 0000 0101 0101 0000 (only noOp, beforeRemoveLiquidity, beforeSwap, beforeDonate) - - // pre-req create pool - CLNoOpTestHook noOpHook = new CLNoOpTestHook(); - noOpHook.setHooksRegistrationBitmap(bitMap); - PoolKey memory key = PoolKey({ - currency0: currency0, - currency1: currency1, - fee: 3000, - hooks: IHooks(noOpHook), - poolManager: poolManager, - parameters: bytes32(uint256((60 << 16) | noOpHook.getHooksRegistrationBitmap())) - }); - - snapStart("CLPoolManagerTest#testNoOp_gas_Initialize"); - poolManager.initialize(key, TickMath.MIN_SQRT_RATIO, new bytes(0)); - snapEnd(); - - BalanceDelta delta; - BalanceDelta feeDelta; - - // Action 1: modify - ICLPoolManager.ModifyLiquidityParams memory params; - snapStart("CLPoolManagerTest#testNoOp_gas_ModifyPosition"); - (delta, feeDelta) = router.modifyPosition(key, params, ZERO_BYTES); - snapEnd(); - assertTrue(delta == BalanceDeltaLibrary.MAXIMUM_DELTA); - assertTrue(feeDelta == BalanceDeltaLibrary.ZERO_DELTA); - - // Action 2: swap - snapStart("CLPoolManagerTest#testNoOp_gas_Swap"); - delta = router.swap( - key, - ICLPoolManager.SwapParams(true, 10000, SQRT_RATIO_1_2), - CLPoolManagerRouter.SwapTestSettings(true, true), - ZERO_BYTES - ); - snapEnd(); - assertTrue(delta == BalanceDeltaLibrary.MAXIMUM_DELTA); - - // Action 3: donate - snapStart("CLPoolManagerTest#testNoOp_gas_Donate"); - delta = router.donate(key, 100, 100, ZERO_BYTES); - snapEnd(); - assertTrue(delta == BalanceDeltaLibrary.MAXIMUM_DELTA); - } - function testModifyLiquidity_Add_WhenPaused() public { Currency currency0 = Currency.wrap(address(new ERC20PresetFixedSupply("C0", "C0", 1e10 ether, address(this)))); Currency currency1 = Currency.wrap(address(new ERC20PresetFixedSupply("C1", "C1", 1e10 ether, address(this)))); @@ -2554,7 +2809,8 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps ICLPoolManager.ModifyLiquidityParams({ tickLower: TickMath.MIN_TICK, tickUpper: TickMath.MAX_TICK, - liquidityDelta: 1e24 + liquidityDelta: 1e24, + salt: 0 }), "" ); @@ -2582,7 +2838,9 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps // pre-req add liquidity router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 1e24}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 1e24, salt: 0}), + "" ); // pause @@ -2590,7 +2848,9 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps // verify no revert router.modifyPosition( - key, ICLPoolManager.ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: -1e24}), "" + key, + ICLPoolManager.ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: -1e24, salt: 0}), + "" ); } @@ -2664,6 +2924,37 @@ contract CLPoolManagerTest is Test, NoIsolate, Deployers, TokenFixture, GasSnaps return true; } + function _validateHookPermissionsConflict(PoolKey memory key) internal pure returns (bool) { + if ( + key.parameters.hasOffsetEnabled(HOOKS_BEFORE_SWAP_RETURNS_DELTA_OFFSET) + && !key.parameters.hasOffsetEnabled(HOOKS_BEFORE_SWAP_OFFSET) + ) { + return false; + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_SWAP_RETURNS_DELTA_OFFSET) + && !key.parameters.hasOffsetEnabled(HOOKS_AFTER_SWAP_OFFSET) + ) { + return false; + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_ADD_LIQUIDIY_RETURNS_DELTA_OFFSET) + && !key.parameters.hasOffsetEnabled(HOOKS_AFTER_ADD_LIQUIDITY_OFFSET) + ) { + return false; + } + + if ( + key.parameters.hasOffsetEnabled(HOOKS_AFTER_REMOVE_LIQUIDIY_RETURNS_DELTA_OFFSET) + && !key.parameters.hasOffsetEnabled(HOOKS_AFTER_REMOVE_LIQUIDITY_OFFSET) + ) { + return false; + } + return true; + } + function tryExecute(address target, bytes memory msgData) external { (bool success, bytes memory result) = target.call(msgData); if (!success) { diff --git a/test/pool-cl/helpers/BaseCLTestHook.sol b/test/pool-cl/helpers/BaseCLTestHook.sol index ebc52da4..bdecb524 100644 --- a/test/pool-cl/helpers/BaseCLTestHook.sol +++ b/test/pool-cl/helpers/BaseCLTestHook.sol @@ -1,23 +1,12 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import { - HOOKS_BEFORE_INITIALIZE_OFFSET, - HOOKS_AFTER_INITIALIZE_OFFSET, - HOOKS_BEFORE_ADD_LIQUIDITY_OFFSET, - HOOKS_AFTER_ADD_LIQUIDITY_OFFSET, - HOOKS_BEFORE_REMOVE_LIQUIDITY_OFFSET, - HOOKS_AFTER_REMOVE_LIQUIDITY_OFFSET, - HOOKS_BEFORE_SWAP_OFFSET, - HOOKS_AFTER_SWAP_OFFSET, - HOOKS_BEFORE_DONATE_OFFSET, - HOOKS_AFTER_DONATE_OFFSET, - HOOKS_NO_OP_OFFSET -} from "../../../src/pool-cl/interfaces/ICLHooks.sol"; +import "../../../src/pool-cl/interfaces/ICLHooks.sol"; import {PoolKey} from "../../../src/types/PoolKey.sol"; import {BalanceDelta} from "../../../src/types/BalanceDelta.sol"; import {ICLHooks} from "../../../src/pool-cl/interfaces/ICLHooks.sol"; import {ICLPoolManager} from "../../../src/pool-cl/interfaces/ICLPoolManager.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../../../src/types/BeforeSwapDelta.sol"; contract BaseCLTestHook is ICLHooks { error HookNotImplemented(); @@ -33,7 +22,10 @@ contract BaseCLTestHook is ICLHooks { bool afterSwap; bool beforeDonate; bool afterDonate; - bool noOp; + bool befreSwapReturnsDelta; + bool afterSwapReturnsDelta; + bool afterAddLiquidityReturnsDelta; + bool afterRemoveLiquidityReturnsDelta; } function getHooksRegistrationBitmap() external view virtual returns (uint16) { @@ -67,7 +59,7 @@ contract BaseCLTestHook is ICLHooks { ICLPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata - ) external virtual returns (bytes4) { + ) external virtual returns (bytes4, BalanceDelta) { revert HookNotImplemented(); } @@ -86,14 +78,14 @@ contract BaseCLTestHook is ICLHooks { ICLPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata - ) external virtual returns (bytes4) { + ) external virtual returns (bytes4, BalanceDelta) { revert HookNotImplemented(); } function beforeSwap(address, PoolKey calldata, ICLPoolManager.SwapParams calldata, bytes calldata) external virtual - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { revert HookNotImplemented(); } @@ -101,7 +93,7 @@ contract BaseCLTestHook is ICLHooks { function afterSwap(address, PoolKey calldata, ICLPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) external virtual - returns (bytes4) + returns (bytes4, int128) { revert HookNotImplemented(); } @@ -134,7 +126,10 @@ contract BaseCLTestHook is ICLHooks { | (permissions.afterSwap ? 1 << HOOKS_AFTER_SWAP_OFFSET : 0) | (permissions.beforeDonate ? 1 << HOOKS_BEFORE_DONATE_OFFSET : 0) | (permissions.afterDonate ? 1 << HOOKS_AFTER_DONATE_OFFSET : 0) - | (permissions.noOp ? 1 << HOOKS_NO_OP_OFFSET : 0) + | (permissions.befreSwapReturnsDelta ? 1 << HOOKS_BEFORE_SWAP_RETURNS_DELTA_OFFSET : 0) + | (permissions.afterSwapReturnsDelta ? 1 << HOOKS_AFTER_SWAP_RETURNS_DELTA_OFFSET : 0) + | (permissions.afterAddLiquidityReturnsDelta ? 1 << HOOKS_AFTER_ADD_LIQUIDIY_RETURNS_DELTA_OFFSET : 0) + | (permissions.afterRemoveLiquidityReturnsDelta ? 1 << HOOKS_AFTER_REMOVE_LIQUIDIY_RETURNS_DELTA_OFFSET : 0) ); } } diff --git a/test/pool-cl/helpers/CLDynamicReturnsFeeHook.sol b/test/pool-cl/helpers/CLDynamicReturnsFeeHook.sol new file mode 100644 index 00000000..84e22609 --- /dev/null +++ b/test/pool-cl/helpers/CLDynamicReturnsFeeHook.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {BaseCLTestHook} from "./BaseCLTestHook.sol"; +import {PoolKey} from "../../../src/types/PoolKey.sol"; +import {ICLPoolManager} from "../../../src/pool-cl/interfaces/ICLPoolManager.sol"; +import {ICLHooks} from "../../../src/pool-cl/interfaces/ICLHooks.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../../../src/types/BeforeSwapDelta.sol"; +import {LPFeeLibrary} from "../../../src/libraries/LPFeeLibrary.sol"; + +contract CLDynamicReturnsFeeHook is BaseCLTestHook { + using LPFeeLibrary for uint24; + + uint24 internal fee; + ICLPoolManager manager; + + function getHooksRegistrationBitmap() external pure override returns (uint16) { + return _hooksRegistrationBitmapFrom( + Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + befreSwapReturnsDelta: false, + afterSwapReturnsDelta: false, + afterAddLiquidityReturnsDelta: false, + afterRemoveLiquidityReturnsDelta: false + }) + ); + } + + function setManager(ICLPoolManager _manager) external { + manager = _manager; + } + + function setFee(uint24 _fee) external { + fee = _fee; + } + + function beforeSwap(address, PoolKey calldata, ICLPoolManager.SwapParams calldata, bytes calldata) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { + // attach the fee flag to `fee` to enable overriding the pool's stored fee + return (ICLHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, fee | LPFeeLibrary.OVERRIDE_FEE_FLAG); + } + + function forcePoolFeeUpdate(PoolKey calldata _key, uint24 _fee) external { + manager.updateDynamicLPFee(_key, _fee); + } +} diff --git a/test/pool-cl/helpers/CLFeeManagerHook.sol b/test/pool-cl/helpers/CLFeeManagerHook.sol index 6502f0a2..d06aeb87 100644 --- a/test/pool-cl/helpers/CLFeeManagerHook.sol +++ b/test/pool-cl/helpers/CLFeeManagerHook.sol @@ -8,6 +8,7 @@ import {IHooks} from "../../../src/interfaces/IHooks.sol"; import {PoolId, PoolIdLibrary} from "../../../src/types/PoolId.sol"; import {PoolKey} from "../../../src/types/PoolKey.sol"; import {BaseCLTestHook} from "./BaseCLTestHook.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../../../src/types/BeforeSwapDelta.sol"; contract CLFeeManagerHook is BaseCLTestHook { using PoolIdLibrary for PoolKey; @@ -49,7 +50,7 @@ contract CLFeeManagerHook is BaseCLTestHook { function beforeSwap(address, PoolKey calldata key, ICLPoolManager.SwapParams calldata, bytes calldata hookData) external override - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { if (hookData.length > 0) { (bool _update, uint24 _fee) = abi.decode(hookData, (bool, uint24)); @@ -59,6 +60,6 @@ contract CLFeeManagerHook is BaseCLTestHook { } } - return ICLHooks.beforeSwap.selector; + return (ICLHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } } diff --git a/test/pool-cl/helpers/CLNoOpTestHook.sol b/test/pool-cl/helpers/CLNoOpTestHook.sol deleted file mode 100644 index 429ff8e4..00000000 --- a/test/pool-cl/helpers/CLNoOpTestHook.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {Hooks} from "../../../src/libraries/Hooks.sol"; -import {ICLPoolManager} from "../../../src/pool-cl/interfaces/ICLPoolManager.sol"; -import {PoolKey} from "../../../src/types/PoolKey.sol"; - -import {BaseCLTestHook} from "./BaseCLTestHook.sol"; - -contract CLNoOpTestHook is BaseCLTestHook { - uint16 bitmap; - - function setHooksRegistrationBitmap(uint16 _bitmap) external { - bitmap = _bitmap; - } - - function getHooksRegistrationBitmap() external view override returns (uint16) { - return bitmap; - } - - function beforeAddLiquidity( - address, - PoolKey calldata, - ICLPoolManager.ModifyLiquidityParams calldata, - bytes calldata - ) external pure override returns (bytes4) { - return Hooks.NO_OP_SELECTOR; - } - - function beforeRemoveLiquidity( - address, - PoolKey calldata, - ICLPoolManager.ModifyLiquidityParams calldata, - bytes calldata - ) external pure override returns (bytes4) { - return Hooks.NO_OP_SELECTOR; - } - - function beforeSwap(address, PoolKey calldata, ICLPoolManager.SwapParams calldata, bytes calldata) - external - pure - override - returns (bytes4) - { - return Hooks.NO_OP_SELECTOR; - } - - function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) - external - pure - override - returns (bytes4) - { - return Hooks.NO_OP_SELECTOR; - } -} diff --git a/test/pool-cl/helpers/CLPoolManagerRouter.sol b/test/pool-cl/helpers/CLPoolManagerRouter.sol index c97afd78..61c4c518 100644 --- a/test/pool-cl/helpers/CLPoolManagerRouter.sol +++ b/test/pool-cl/helpers/CLPoolManagerRouter.sol @@ -9,7 +9,6 @@ import {ICLPoolManager} from "../../../src/pool-cl/interfaces/ICLPoolManager.sol import {console2} from "forge-std/console2.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {BalanceDelta, BalanceDeltaLibrary} from "../../../src/types/BalanceDelta.sol"; -import {HOOKS_NO_OP_OFFSET} from "../../../src/pool-cl/interfaces/ICLHooks.sol"; import {Hooks} from "../../../src/libraries/Hooks.sol"; contract CLPoolManagerRouter { @@ -61,47 +60,37 @@ contract CLPoolManagerRouter { function modifyPositionCallback(bytes memory rawData) private returns (bytes memory) { ModifyPositionCallbackData memory data = abi.decode(rawData, (ModifyPositionCallbackData)); + // delta already takes feeDelta into account (BalanceDelta delta, BalanceDelta feeDelta) = poolManager.modifyLiquidity(data.key, data.params, data.hookData); - if (delta == BalanceDeltaLibrary.MAXIMUM_DELTA) { - // check if the hook has permission to no-op, if true, return early - if (!data.key.parameters.shouldCall(HOOKS_NO_OP_OFFSET, data.key.hooks)) { - revert HookMissingNoOpPermission(); - } - return abi.encode(delta, feeDelta); - } - // For now assume to always settle feeDelta in the same way as delta - BalanceDelta totalDelta = delta + feeDelta; - - if (totalDelta.amount0() > 0) { + if (delta.amount0() > 0) { if (data.key.currency0.isNative()) { - vault.settle{value: uint128(totalDelta.amount0())}(data.key.currency0); + vault.settle{value: uint128(delta.amount0())}(data.key.currency0); } else { vault.sync(data.key.currency0); IERC20(Currency.unwrap(data.key.currency0)).transferFrom( - data.sender, address(vault), uint128(totalDelta.amount0()) + data.sender, address(vault), uint128(delta.amount0()) ); vault.settle(data.key.currency0); } } - - if (totalDelta.amount1() > 0) { + if (delta.amount1() > 0) { if (data.key.currency1.isNative()) { - vault.settle{value: uint128(totalDelta.amount1())}(data.key.currency1); + vault.settle{value: uint128(delta.amount1())}(data.key.currency1); } else { vault.sync(data.key.currency1); IERC20(Currency.unwrap(data.key.currency1)).transferFrom( - data.sender, address(vault), uint128(totalDelta.amount1()) + data.sender, address(vault), uint128(delta.amount1()) ); vault.settle(data.key.currency1); } } - if (totalDelta.amount0() < 0) { - vault.take(data.key.currency0, data.sender, uint128(-totalDelta.amount0())); + if (delta.amount0() < 0) { + vault.take(data.key.currency0, data.sender, uint128(-delta.amount0())); } - if (totalDelta.amount1() < 0) { - vault.take(data.key.currency1, data.sender, uint128(-totalDelta.amount1())); + if (delta.amount1() < 0) { + vault.take(data.key.currency1, data.sender, uint128(-delta.amount1())); } return abi.encode(delta, feeDelta); @@ -142,14 +131,6 @@ contract CLPoolManagerRouter { BalanceDelta delta = poolManager.swap(data.key, data.params, data.hookData); - if (delta == BalanceDeltaLibrary.MAXIMUM_DELTA) { - // check if the hook has permission to no-op, if true, return early - if (!data.key.parameters.shouldCall(HOOKS_NO_OP_OFFSET, data.key.hooks)) { - revert HookMissingNoOpPermission(); - } - return abi.encode(delta); - } - if (data.params.zeroForOne) { if (delta.amount0() > 0) { if (data.testSettings.settleUsingTransfer) { @@ -236,14 +217,6 @@ contract CLPoolManagerRouter { BalanceDelta delta = poolManager.donate(data.key, data.amount0, data.amount1, data.hookData); - if (delta == BalanceDeltaLibrary.MAXIMUM_DELTA) { - // check if the hook has permission to no-op, if true, return early - if (!data.key.parameters.shouldCall(HOOKS_NO_OP_OFFSET, data.key.hooks)) { - revert HookMissingNoOpPermission(); - } - return abi.encode(delta); - } - if (delta.amount0() > 0) { if (data.key.currency0.isNative()) { vault.settle{value: uint128(delta.amount0())}(data.key.currency0); diff --git a/test/pool-cl/helpers/CLReturnsDeltaHook.sol b/test/pool-cl/helpers/CLReturnsDeltaHook.sol new file mode 100644 index 00000000..2c7a8985 --- /dev/null +++ b/test/pool-cl/helpers/CLReturnsDeltaHook.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IVault} from "../../../src/interfaces/IVault.sol"; +import {Hooks} from "../../../src/libraries/Hooks.sol"; +import {ICLPoolManager} from "../../../src/pool-cl/interfaces/ICLPoolManager.sol"; +import {PoolKey} from "../../../src/types/PoolKey.sol"; +import {Currency, CurrencyLibrary} from "../../../src/types/Currency.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "../../../src/types/BalanceDelta.sol"; +import {BaseCLTestHook} from "./BaseCLTestHook.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary, toBeforeSwapDelta} from "../../../src/types/BeforeSwapDelta.sol"; + +contract CLReturnsDeltaHook is BaseCLTestHook { + error InvalidAction(); + + using CurrencyLibrary for Currency; + using Hooks for bytes32; + + IVault public immutable vault; + ICLPoolManager public immutable poolManager; + + constructor(IVault _vault, ICLPoolManager _poolManager) { + vault = _vault; + poolManager = _poolManager; + } + + function getHooksRegistrationBitmap() external pure override returns (uint16) { + return _hooksRegistrationBitmapFrom( + Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: true, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: true, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + befreSwapReturnsDelta: true, + afterSwapReturnsDelta: true, + afterAddLiquidityReturnsDelta: true, + afterRemoveLiquidityReturnsDelta: true + }) + ); + } + + function afterAddLiquidity( + address, + PoolKey calldata key, + ICLPoolManager.ModifyLiquidityParams memory params, + BalanceDelta, + bytes calldata data + ) external override returns (bytes4, BalanceDelta) { + (int256 liquidityDelta) = abi.decode(data, (int256)); + if (liquidityDelta == 0) { + return (this.afterAddLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); + } + params.liquidityDelta = liquidityDelta; + (BalanceDelta delta,) = poolManager.modifyLiquidity(key, params, new bytes(0)); + return (this.afterAddLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA - delta); + } + + function afterRemoveLiquidity( + address, + PoolKey calldata key, + ICLPoolManager.ModifyLiquidityParams memory params, + BalanceDelta, + bytes calldata data + ) external override returns (bytes4, BalanceDelta) { + (int256 liquidityDelta) = abi.decode(data, (int256)); + if (liquidityDelta == 0) { + return (this.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); + } + params.liquidityDelta = liquidityDelta; + (BalanceDelta delta,) = poolManager.modifyLiquidity(key, params, new bytes(0)); + return (this.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA - delta); + } + + function beforeSwap(address, PoolKey calldata key, ICLPoolManager.SwapParams calldata params, bytes calldata data) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { + (int128 hookDeltaSpecified, int128 hookDeltaUnspecified,) = abi.decode(data, (int128, int128, int128)); + + if (params.zeroForOne == params.amountSpecified > 0) { + // the specified token is token0 + if (hookDeltaSpecified > 0) { + vault.sync(key.currency0); + key.currency0.transfer(address(vault), uint128(hookDeltaSpecified)); + vault.settle(key.currency0); + } else { + vault.take(key.currency0, address(this), uint128(-hookDeltaSpecified)); + } + + if (hookDeltaUnspecified > 0) { + vault.sync(key.currency1); + key.currency1.transfer(address(vault), uint128(hookDeltaUnspecified)); + vault.settle(key.currency1); + } else { + vault.take(key.currency1, address(this), uint128(-hookDeltaUnspecified)); + } + } else { + // the specified token is token1 + if (hookDeltaSpecified > 0) { + vault.sync(key.currency1); + key.currency1.transfer(address(vault), uint128(hookDeltaSpecified)); + vault.settle(key.currency1); + } else { + vault.take(key.currency1, address(this), uint128(-hookDeltaSpecified)); + } + + if (hookDeltaUnspecified > 0) { + vault.sync(key.currency0); + key.currency0.transfer(address(vault), uint128(hookDeltaUnspecified)); + vault.settle(key.currency0); + } else { + vault.take(key.currency0, address(this), uint128(-hookDeltaUnspecified)); + } + } + return (this.beforeSwap.selector, toBeforeSwapDelta(hookDeltaSpecified, hookDeltaUnspecified), 0); + } + + function afterSwap( + address, + PoolKey calldata key, + ICLPoolManager.SwapParams calldata params, + BalanceDelta, + bytes calldata data + ) external override returns (bytes4, int128) { + (,, int128 hookDeltaUnspecified) = abi.decode(data, (int128, int128, int128)); + + if (hookDeltaUnspecified == 0) { + return (this.afterSwap.selector, 0); + } + + if (params.zeroForOne == params.amountSpecified > 0) { + // the unspecified token is token1 + if (hookDeltaUnspecified > 0) { + vault.sync(key.currency1); + key.currency1.transfer(address(vault), uint128(hookDeltaUnspecified)); + vault.settle(key.currency1); + } else { + vault.take(key.currency1, address(this), uint128(-hookDeltaUnspecified)); + } + } else { + // the unspecified token is token0 + if (hookDeltaUnspecified > 0) { + vault.sync(key.currency0); + key.currency0.transfer(address(vault), uint128(hookDeltaUnspecified)); + vault.settle(key.currency0); + } else { + vault.take(key.currency0, address(this), uint128(-hookDeltaUnspecified)); + } + } + + return (this.afterSwap.selector, hookDeltaUnspecified); + } +} diff --git a/test/pool-cl/helpers/CLSkipCallbackHook.sol b/test/pool-cl/helpers/CLSkipCallbackHook.sol index daaa2d5b..318f5e7f 100644 --- a/test/pool-cl/helpers/CLSkipCallbackHook.sol +++ b/test/pool-cl/helpers/CLSkipCallbackHook.sol @@ -9,6 +9,7 @@ import {Currency, CurrencyLibrary} from "../../../src/types/Currency.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {BalanceDelta, BalanceDeltaLibrary} from "../../../src/types/BalanceDelta.sol"; import {BaseCLTestHook} from "./BaseCLTestHook.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../../../src/types/BeforeSwapDelta.sol"; /// @notice CL hook which does a callback contract CLSkipCallbackHook is BaseCLTestHook { @@ -41,7 +42,10 @@ contract CLSkipCallbackHook is BaseCLTestHook { afterSwap: true, beforeDonate: true, afterDonate: true, - noOp: false + befreSwapReturnsDelta: true, + afterSwapReturnsDelta: true, + afterAddLiquidityReturnsDelta: true, + afterRemoveLiquidityReturnsDelta: true }) ); } @@ -86,9 +90,6 @@ contract CLSkipCallbackHook is BaseCLTestHook { (BalanceDelta delta,) = poolManager.modifyLiquidity(data.key, data.params, data.hookData); - // For now assume to always settle feeDelta in the same way as delta - // BalanceDelta totalDelta = delta + feeDelta; - if (delta.amount0() > 0) { if (data.key.currency0.isNative()) { vault.settle{value: uint128(delta.amount0())}(data.key.currency0); @@ -311,9 +312,9 @@ contract CLSkipCallbackHook is BaseCLTestHook { ICLPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata - ) external override returns (bytes4) { + ) external override returns (bytes4, BalanceDelta) { hookCounterCallbackCount++; - return CLSkipCallbackHook.afterAddLiquidity.selector; + return (CLSkipCallbackHook.afterAddLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); } function beforeRemoveLiquidity( @@ -332,27 +333,27 @@ contract CLSkipCallbackHook is BaseCLTestHook { ICLPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata - ) external override returns (bytes4) { + ) external override returns (bytes4, BalanceDelta) { hookCounterCallbackCount++; - return CLSkipCallbackHook.afterRemoveLiquidity.selector; + return (CLSkipCallbackHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); } function beforeSwap(address, PoolKey calldata, ICLPoolManager.SwapParams calldata, bytes calldata) external override - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { hookCounterCallbackCount++; - return CLSkipCallbackHook.beforeSwap.selector; + return (CLSkipCallbackHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } function afterSwap(address, PoolKey calldata, ICLPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) external override - returns (bytes4) + returns (bytes4, int128) { hookCounterCallbackCount++; - return CLSkipCallbackHook.afterSwap.selector; + return (CLSkipCallbackHook.afterSwap.selector, 0); } function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) diff --git a/test/pool-cl/helpers/MockHooks.sol b/test/pool-cl/helpers/MockHooks.sol index 1496b1f9..6516d08e 100644 --- a/test/pool-cl/helpers/MockHooks.sol +++ b/test/pool-cl/helpers/MockHooks.sol @@ -5,7 +5,8 @@ import {Hooks} from "../../../src/libraries/Hooks.sol"; import {ICLHooks} from "../../../src/pool-cl/interfaces/ICLHooks.sol"; import {ICLPoolManager} from "../../../src/pool-cl/interfaces/ICLPoolManager.sol"; import {PoolKey} from "../../../src/types/PoolKey.sol"; -import {BalanceDelta} from "../../../src/types/BalanceDelta.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "../../../src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../../../src/types/BeforeSwapDelta.sol"; import {PoolId, PoolIdLibrary} from "../../../src/types/PoolId.sol"; contract MockHooks is ICLHooks { @@ -27,8 +28,14 @@ contract MockHooks is ICLHooks { mapping(PoolId => uint16) public swapFees; - function getHooksRegistrationBitmap() external pure returns (uint16) { - return 0xffff; + uint16 public bitmap; + + function getHooksRegistrationBitmap() external view returns (uint16) { + return bitmap != 0 ? bitmap : 0xffff; + } + + function setHooksRegistrationBitmap(uint16 newBitmapValue) external { + bitmap = newBitmapValue; } function beforeInitialize(address, PoolKey calldata, uint160, bytes calldata hookData) @@ -68,10 +75,10 @@ contract MockHooks is ICLHooks { ICLPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata hookData - ) external override returns (bytes4) { + ) external override returns (bytes4, BalanceDelta) { afterAddLiquidityData = hookData; bytes4 selector = MockHooks.afterAddLiquidity.selector; - return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; + return (returnValues[selector] == bytes4(0) ? selector : returnValues[selector], BalanceDeltaLibrary.ZERO_DELTA); } function beforeRemoveLiquidity( @@ -91,20 +98,24 @@ contract MockHooks is ICLHooks { ICLPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata hookData - ) external override returns (bytes4) { + ) external override returns (bytes4, BalanceDelta) { afterRemoveLiquidityData = hookData; bytes4 selector = MockHooks.afterRemoveLiquidity.selector; - return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; + return (returnValues[selector] == bytes4(0) ? selector : returnValues[selector], BalanceDeltaLibrary.ZERO_DELTA); } function beforeSwap(address, PoolKey calldata, ICLPoolManager.SwapParams calldata, bytes calldata hookData) external override - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { beforeSwapData = hookData; bytes4 selector = MockHooks.beforeSwap.selector; - return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; + return ( + returnValues[selector] == bytes4(0) ? selector : returnValues[selector], + BeforeSwapDeltaLibrary.ZERO_DELTA, + 0 + ); } function afterSwap( @@ -113,10 +124,10 @@ contract MockHooks is ICLHooks { ICLPoolManager.SwapParams calldata, BalanceDelta, bytes calldata hookData - ) external override returns (bytes4) { + ) external override returns (bytes4, int128) { afterSwapData = hookData; bytes4 selector = MockHooks.afterSwap.selector; - return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; + return (returnValues[selector] == bytes4(0) ? selector : returnValues[selector], 0); } function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata hookData) diff --git a/test/pool-cl/libraries/CLPool.t.sol b/test/pool-cl/libraries/CLPool.t.sol index 5f5160e7..3bff44ed 100644 --- a/test/pool-cl/libraries/CLPool.t.sol +++ b/test/pool-cl/libraries/CLPool.t.sol @@ -15,38 +15,43 @@ import {LiquidityAmounts} from "../helpers/LiquidityAmounts.sol"; import {LPFeeLibrary} from "../../../src/libraries/LPFeeLibrary.sol"; import {FullMath} from "../../../src/pool-cl/libraries/FullMath.sol"; import {FixedPoint128} from "../../../src/pool-cl/libraries/FixedPoint128.sol"; +import {ICLPoolManager} from "../../../src/pool-cl/interfaces/ICLPoolManager.sol"; +import {LPFeeLibrary} from "../../../src/libraries/LPFeeLibrary.sol"; +import {ProtocolFeeLibrary} from "../../../src/libraries/ProtocolFeeLibrary.sol"; contract PoolTest is Test { using CLPool for CLPool.State; + using LPFeeLibrary for uint24; + using ProtocolFeeLibrary for uint24; CLPool.State state; - function testPoolInitialize(uint160 sqrtPriceX96, uint16 protocolFee, uint24 swapFee) public { + function testPoolInitialize(uint160 sqrtPriceX96, uint16 protocolFee, uint24 lpFee) public { protocolFee = uint16(bound(protocolFee, 0, 2 ** 16 - 1)); - swapFee = uint24(bound(swapFee, 0, 999999)); + lpFee = uint24(bound(lpFee, 0, 999999)); if (sqrtPriceX96 < TickMath.MIN_SQRT_RATIO || sqrtPriceX96 >= TickMath.MAX_SQRT_RATIO) { vm.expectRevert(TickMath.InvalidSqrtRatio.selector); - state.initialize(sqrtPriceX96, protocolFee, swapFee); + state.initialize(sqrtPriceX96, protocolFee, lpFee); } else { - state.initialize(sqrtPriceX96, protocolFee, swapFee); + state.initialize(sqrtPriceX96, protocolFee, lpFee); assertEq(state.slot0.sqrtPriceX96, sqrtPriceX96); assertEq(state.slot0.protocolFee, protocolFee); assertEq(state.slot0.tick, TickMath.getTickAtSqrtRatio(sqrtPriceX96)); assertLt(state.slot0.tick, TickMath.MAX_TICK); assertGt(state.slot0.tick, TickMath.MIN_TICK - 1); - assertEq(state.slot0.lpFee, swapFee); + assertEq(state.slot0.lpFee, lpFee); } } - function testModifyPosition(uint160 sqrtPriceX96, CLPool.ModifyLiquidityParams memory params, uint24 swapFee) + function testModifyPosition(uint160 sqrtPriceX96, CLPool.ModifyLiquidityParams memory params, uint24 lpFee) public { // Assumptions tested in PoolManager.t.sol params.tickSpacing = int24(bound(params.tickSpacing, 1, 32767)); - swapFee = uint24(bound(swapFee, 0, LPFeeLibrary.ONE_HUNDRED_PERCENT_FEE - 1)); + lpFee = uint24(bound(lpFee, 0, LPFeeLibrary.ONE_HUNDRED_PERCENT_FEE - 1)); - testPoolInitialize(sqrtPriceX96, 0, swapFee); + testPoolInitialize(sqrtPriceX96, 0, lpFee); if (params.tickLower >= params.tickUpper) { vm.expectRevert(abi.encodeWithSelector(Tick.TicksMisordered.selector, params.tickLower, params.tickUpper)); @@ -91,18 +96,28 @@ contract PoolTest is Test { uint160 sqrtPriceX96, CLPool.ModifyLiquidityParams memory modifyLiquidityParams, CLPool.SwapParams memory swapParams, - uint24 swapFee + uint24 lpFee ) public { swapParams.amountSpecified = int256(bound(swapParams.amountSpecified, 0, type(int128).max)); - testModifyPosition(sqrtPriceX96, modifyLiquidityParams, swapFee); + testModifyPosition(sqrtPriceX96, modifyLiquidityParams, lpFee); swapParams.tickSpacing = modifyLiquidityParams.tickSpacing; CLPool.Slot0 memory slot0 = state.slot0; - if (swapParams.amountSpecified == 0) { - vm.expectRevert(CLPool.SwapAmountCannotBeZero.selector); - } else if (swapParams.zeroForOne) { + // avoid lpFee override valid + if ( + swapParams.lpFeeOverride.isOverride() + && swapParams.lpFeeOverride.removeOverrideFlag() > LPFeeLibrary.ONE_HUNDRED_PERCENT_FEE + ) { + return; + } + + uint24 swapFee = swapParams.lpFeeOverride.isOverride() + ? swapParams.lpFeeOverride.removeOverrideAndValidate(LPFeeLibrary.ONE_HUNDRED_PERCENT_FEE) + : lpFee; + + if (swapParams.zeroForOne) { if (swapParams.sqrtPriceLimitX96 >= slot0.sqrtPriceX96) { vm.expectRevert( abi.encodeWithSelector( @@ -130,6 +145,8 @@ contract PoolTest is Test { ) ); } + } else if (swapParams.amountSpecified <= 0 && swapFee == LPFeeLibrary.ONE_HUNDRED_PERCENT_FEE) { + vm.expectRevert(CLPool.InvalidFeeForExactOut.selector); } state.swap(swapParams); diff --git a/test/pool-cl/libraries/CLPoolSwapFee.t.sol b/test/pool-cl/libraries/CLPoolSwapFee.t.sol index c3988adb..59f04537 100644 --- a/test/pool-cl/libraries/CLPoolSwapFee.t.sol +++ b/test/pool-cl/libraries/CLPoolSwapFee.t.sol @@ -103,7 +103,7 @@ contract CLPoolSwapFeeTest is Deployers, TokenFixture, Test { poolManager.initialize(dynamicFeeKey, SQRT_RATIO_1_1, ZERO_BYTES); ICLPoolManager.ModifyLiquidityParams memory modifyPositionParams = - ICLPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 1 ether}); + ICLPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 1 ether, salt: 0}); router.modifyPosition(dynamicFeeKey, modifyPositionParams, ZERO_BYTES); vm.expectEmit(true, true, true, true); @@ -133,7 +133,7 @@ contract CLPoolSwapFeeTest is Deployers, TokenFixture, Test { poolManager.initialize(staticFeeKey, SQRT_RATIO_1_1, ZERO_BYTES); ICLPoolManager.ModifyLiquidityParams memory modifyPositionParams = - ICLPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 1 ether}); + ICLPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 1 ether, salt: 0}); router.modifyPosition(staticFeeKey, modifyPositionParams, ZERO_BYTES); vm.expectEmit(true, true, true, true); @@ -166,7 +166,7 @@ contract CLPoolSwapFeeTest is Deployers, TokenFixture, Test { poolManager.initialize(dynamicFeeKey, SQRT_RATIO_1_1, ZERO_BYTES); ICLPoolManager.ModifyLiquidityParams memory modifyPositionParams = - ICLPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 1 ether}); + ICLPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 1 ether, salt: 0}); router.modifyPosition(dynamicFeeKey, modifyPositionParams, ZERO_BYTES); vm.expectEmit(true, true, true, true); diff --git a/test/pool-cl/libraries/CLPosition.t.sol b/test/pool-cl/libraries/CLPosition.t.sol index cd97adba..ab2b1581 100644 --- a/test/pool-cl/libraries/CLPosition.t.sol +++ b/test/pool-cl/libraries/CLPosition.t.sol @@ -16,7 +16,7 @@ contract CLPositionTest is Test, GasSnapshot { CLPool.State public pool; function test_get_emptyPosition() public { - CLPosition.Info memory info = pool.positions.get(address(this), 1, 2); + CLPosition.Info memory info = pool.positions.get(address(this), 1, 2, 0); assertEq(info.liquidity, 0); assertEq(info.feeGrowthInside0LastX128, 0); assertEq(info.feeGrowthInside1LastX128, 0); @@ -27,7 +27,7 @@ contract CLPositionTest is Test, GasSnapshot { uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128 ) public { - CLPosition.Info storage info = pool.positions.get(address(this), 1, 2); + CLPosition.Info storage info = pool.positions.get(address(this), 1, 2, 0); if (liquidityDelta == 0) { vm.expectRevert(CLPosition.CannotUpdateEmptyPosition.selector); @@ -44,7 +44,7 @@ contract CLPositionTest is Test, GasSnapshot { } function test_set_updateNonEmptyPosition() public { - CLPosition.Info storage info = pool.positions.get(address(this), 1, 2); + CLPosition.Info storage info = pool.positions.get(address(this), 1, 2, 0); // init { @@ -90,4 +90,15 @@ contract CLPositionTest is Test, GasSnapshot { assertEq(info.feeGrowthInside1LastX128, 15 * FixedPoint128.Q128); } } + + function test_MixFuzz(address owner, int24 tickLower, int24 tickUpper, bytes32 salt, int128 liquidityDelta) + public + { + liquidityDelta = int128(bound(liquidityDelta, 1, type(int128).max)); + CLPosition.Info storage info = pool.positions.get(owner, tickLower, tickUpper, salt); + info.update(liquidityDelta, 0, 0); + + bytes32 key = keccak256(abi.encodePacked(owner, tickLower, tickUpper, salt)); + assertEq(pool.positions[key].liquidity, uint128(liquidityDelta)); + } }