diff --git a/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap b/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap index cc51933b..c92766c8 100644 --- a/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap +++ b/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap @@ -1 +1 @@ -1185338 \ No newline at end of file +1185462 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap b/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap index d8268ee5..9c2a5192 100644 --- a/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap +++ b/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap @@ -1 +1 @@ -1052048 \ No newline at end of file +1052172 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap b/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap index 7913665f..50c1ffec 100644 --- a/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap +++ b/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap @@ -1 +1 @@ -1092839 \ No newline at end of file +1092895 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap b/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap index beb61674..b6f74479 100644 --- a/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap +++ b/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap @@ -1 +1 @@ -1257692 \ No newline at end of file +1257816 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap b/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap index 810644bc..81bf889e 100644 --- a/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap +++ b/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap @@ -1 +1 @@ -1124491 \ No newline at end of file +1124615 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap b/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap index ba1ea4d8..cd6460e3 100644 --- a/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap +++ b/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap @@ -1 +1 @@ -1159276 \ No newline at end of file +1159332 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap b/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap index cc51933b..c92766c8 100644 --- a/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap +++ b/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap @@ -1 +1 @@ -1185338 \ No newline at end of file +1185462 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap b/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap index d8268ee5..9c2a5192 100644 --- a/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap +++ b/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap @@ -1 +1 @@ -1052048 \ No newline at end of file +1052172 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap b/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap index b1cd9e6a..fb8069a3 100644 --- a/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap +++ b/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap @@ -1 +1 @@ -1092835 \ No newline at end of file +1092891 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap b/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap index 73a57276..8314f75b 100644 --- a/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap +++ b/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap @@ -1 +1 @@ -1255674 \ No newline at end of file +1255798 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap b/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap index 76c94701..e0a0fba3 100644 --- a/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap +++ b/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap @@ -1 +1 @@ -1122473 \ No newline at end of file +1122597 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap b/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap index 66b3d01c..1e3ec3ad 100644 --- a/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap +++ b/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap @@ -1 +1 @@ -1157254 \ No newline at end of file +1157310 \ No newline at end of file diff --git a/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_OutsideActiveId_ExistingId.snap b/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_OutsideActiveId_ExistingId.snap index 582e6f17..9945d0b5 100644 --- a/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_OutsideActiveId_ExistingId.snap +++ b/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_OutsideActiveId_ExistingId.snap @@ -1 +1 @@ -296005 \ No newline at end of file +296102 \ No newline at end of file diff --git a/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_OutsideActiveId_NewId.snap b/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_OutsideActiveId_NewId.snap index f6e96bfd..ef11346d 100644 --- a/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_OutsideActiveId_NewId.snap +++ b/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_OutsideActiveId_NewId.snap @@ -1 +1 @@ -1134777 \ No newline at end of file +1134888 \ No newline at end of file diff --git a/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_SingleBin.snap b/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_SingleBin.snap index f61d5bbb..f1c6ce22 100644 --- a/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_SingleBin.snap +++ b/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_SingleBin.snap @@ -1 +1 @@ -541350 \ No newline at end of file +541445 \ No newline at end of file diff --git a/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_ThreeBins.snap b/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_ThreeBins.snap index efced8fc..d32d250b 100644 --- a/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_ThreeBins.snap +++ b/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_addLiquidity_ThreeBins.snap @@ -1 +1 @@ -915643 \ No newline at end of file +915740 \ No newline at end of file diff --git a/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_decreaseLiquidity_threeBins.snap b/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_decreaseLiquidity_threeBins.snap index f7497cd7..e58ce26d 100644 --- a/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_decreaseLiquidity_threeBins.snap +++ b/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_decreaseLiquidity_threeBins.snap @@ -1 +1 @@ -189126 \ No newline at end of file +189202 \ No newline at end of file diff --git a/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_decreaseLiquidity_threeBins_half.snap b/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_decreaseLiquidity_threeBins_half.snap index 56d80f39..1877677f 100644 --- a/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_decreaseLiquidity_threeBins_half.snap +++ b/.forge-snapshots/BinPositionManager_ModifyLiquidityTest#test_decreaseLiquidity_threeBins_half.snap @@ -1 +1 @@ -208315 \ No newline at end of file +208411 \ No newline at end of file diff --git a/.forge-snapshots/BinPositionManager_NativeTokenTest#test_addLiquidity.snap b/.forge-snapshots/BinPositionManager_NativeTokenTest#test_addLiquidity.snap index dcb33cc6..6abc877f 100644 --- a/.forge-snapshots/BinPositionManager_NativeTokenTest#test_addLiquidity.snap +++ b/.forge-snapshots/BinPositionManager_NativeTokenTest#test_addLiquidity.snap @@ -1 +1 @@ -867152 \ No newline at end of file +867326 \ No newline at end of file diff --git a/.forge-snapshots/BinPositionManager_NativeTokenTest#test_decreaseLiquidity.snap b/.forge-snapshots/BinPositionManager_NativeTokenTest#test_decreaseLiquidity.snap index bc420a5f..9cb3a16a 100644 --- a/.forge-snapshots/BinPositionManager_NativeTokenTest#test_decreaseLiquidity.snap +++ b/.forge-snapshots/BinPositionManager_NativeTokenTest#test_decreaseLiquidity.snap @@ -1 +1 @@ -193782 \ No newline at end of file +193867 \ No newline at end of file diff --git a/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_DifferentRecipient.snap b/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_DifferentRecipient.snap index 689d7b6f..5935d9ae 100644 --- a/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_DifferentRecipient.snap +++ b/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_DifferentRecipient.snap @@ -1 +1 @@ -142488 \ No newline at end of file +142659 \ No newline at end of file diff --git a/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_EthPool_SwapEthForToken.snap b/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_EthPool_SwapEthForToken.snap index 842c0649..3548cd1f 100644 --- a/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_EthPool_SwapEthForToken.snap +++ b/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_EthPool_SwapEthForToken.snap @@ -1 +1 @@ -137514 \ No newline at end of file +137685 \ No newline at end of file diff --git a/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_EthPool_SwapTokenForEth.snap b/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_EthPool_SwapTokenForEth.snap index b37afbea..22afc0db 100644 --- a/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_EthPool_SwapTokenForEth.snap +++ b/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_EthPool_SwapTokenForEth.snap @@ -1 +1 @@ -116562 \ No newline at end of file +116733 \ No newline at end of file diff --git a/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_SwapForY_1.snap b/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_SwapForY_1.snap index 2c6fd46a..2e3dc3ef 100644 --- a/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_SwapForY_1.snap +++ b/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_SwapForY_1.snap @@ -1 +1 @@ -142484 \ No newline at end of file +142655 \ No newline at end of file diff --git a/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_SwapForY_2.snap b/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_SwapForY_2.snap index f267e0d1..e9ea6a09 100644 --- a/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_SwapForY_2.snap +++ b/.forge-snapshots/BinSwapRouterTest#testExactInputSingle_SwapForY_2.snap @@ -1 +1 @@ -142544 \ No newline at end of file +142715 \ No newline at end of file diff --git a/.forge-snapshots/BinSwapRouterTest#testExactInput_MultiHopDifferentRecipient.snap b/.forge-snapshots/BinSwapRouterTest#testExactInput_MultiHopDifferentRecipient.snap index 241de7bc..aed0bf9b 100644 --- a/.forge-snapshots/BinSwapRouterTest#testExactInput_MultiHopDifferentRecipient.snap +++ b/.forge-snapshots/BinSwapRouterTest#testExactInput_MultiHopDifferentRecipient.snap @@ -1 +1 @@ -174111 \ No newline at end of file +174282 \ No newline at end of file diff --git a/.forge-snapshots/BinSwapRouterTest#testExactOutputSingle_DifferentRecipient.snap b/.forge-snapshots/BinSwapRouterTest#testExactOutputSingle_DifferentRecipient.snap index 24995181..43116cff 100644 --- a/.forge-snapshots/BinSwapRouterTest#testExactOutputSingle_DifferentRecipient.snap +++ b/.forge-snapshots/BinSwapRouterTest#testExactOutputSingle_DifferentRecipient.snap @@ -1 +1 @@ -146766 \ No newline at end of file +146925 \ No newline at end of file diff --git a/.forge-snapshots/BinSwapRouterTest#testExactOutputSingle_SwapForY_1.snap b/.forge-snapshots/BinSwapRouterTest#testExactOutputSingle_SwapForY_1.snap index 19fa02e1..24ec8e92 100644 --- a/.forge-snapshots/BinSwapRouterTest#testExactOutputSingle_SwapForY_1.snap +++ b/.forge-snapshots/BinSwapRouterTest#testExactOutputSingle_SwapForY_1.snap @@ -1 +1 @@ -146762 \ No newline at end of file +146921 \ No newline at end of file diff --git a/.forge-snapshots/BinSwapRouterTest#testExactOutputSingle_SwapForY_2.snap b/.forge-snapshots/BinSwapRouterTest#testExactOutputSingle_SwapForY_2.snap index 4f6b97b1..b87d9e72 100644 --- a/.forge-snapshots/BinSwapRouterTest#testExactOutputSingle_SwapForY_2.snap +++ b/.forge-snapshots/BinSwapRouterTest#testExactOutputSingle_SwapForY_2.snap @@ -1 +1 @@ -146819 \ No newline at end of file +146978 \ No newline at end of file diff --git a/.forge-snapshots/BinSwapRouterTest#testExactOutput_MultiHopDifferentRecipient.snap b/.forge-snapshots/BinSwapRouterTest#testExactOutput_MultiHopDifferentRecipient.snap index 21604856..4a472721 100644 --- a/.forge-snapshots/BinSwapRouterTest#testExactOutput_MultiHopDifferentRecipient.snap +++ b/.forge-snapshots/BinSwapRouterTest#testExactOutput_MultiHopDifferentRecipient.snap @@ -1 +1 @@ -177739 \ No newline at end of file +177910 \ No newline at end of file diff --git a/.forge-snapshots/BinSwapRouterTest#testExactOutput_SingleHop.snap b/.forge-snapshots/BinSwapRouterTest#testExactOutput_SingleHop.snap index f83acacc..763e3692 100644 --- a/.forge-snapshots/BinSwapRouterTest#testExactOutput_SingleHop.snap +++ b/.forge-snapshots/BinSwapRouterTest#testExactOutput_SingleHop.snap @@ -1 +1 @@ -148450 \ No newline at end of file +148621 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testCLMigrateFromV2IncludingInit.snap b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testCLMigrateFromV2IncludingInit.snap index 7563a218..9966035c 100644 --- a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testCLMigrateFromV2IncludingInit.snap +++ b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testCLMigrateFromV2IncludingInit.snap @@ -1 +1 @@ -794971 \ No newline at end of file +795140 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testCLMigrateFromV2WithoutInit.snap b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testCLMigrateFromV2WithoutInit.snap index 3f615ac2..ecc05b10 100644 --- a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testCLMigrateFromV2WithoutInit.snap +++ b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testCLMigrateFromV2WithoutInit.snap @@ -1 +1 @@ -660366 \ No newline at end of file +660535 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testCLMigrateFromV2WithoutNativeToken.snap b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testCLMigrateFromV2WithoutNativeToken.snap index 97cd1418..22b45184 100644 --- a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testCLMigrateFromV2WithoutNativeToken.snap +++ b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testCLMigrateFromV2WithoutNativeToken.snap @@ -1 +1 @@ -730660 \ No newline at end of file +730829 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testCLMigrateFromV3IncludingInit.snap b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testCLMigrateFromV3IncludingInit.snap index 52110a08..2d652120 100644 --- a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testCLMigrateFromV3IncludingInit.snap +++ b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testCLMigrateFromV3IncludingInit.snap @@ -1 +1 @@ -844890 \ No newline at end of file +845059 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testCLMigrateFromV3WithoutInit.snap b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testCLMigrateFromV3WithoutInit.snap index 67bfe710..c6539a3c 100644 --- a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testCLMigrateFromV3WithoutInit.snap +++ b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testCLMigrateFromV3WithoutInit.snap @@ -1 +1 @@ -712814 \ No newline at end of file +712983 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testCLMigrateFromV3WithoutNativeToken.snap b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testCLMigrateFromV3WithoutNativeToken.snap index 3fc89aa8..37124a72 100644 --- a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testCLMigrateFromV3WithoutNativeToken.snap +++ b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testCLMigrateFromV3WithoutNativeToken.snap @@ -1 +1 @@ -753655 \ No newline at end of file +753824 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromUniswapV2Test#testCLMigrateFromV2IncludingInit.snap b/.forge-snapshots/CLMigratorFromUniswapV2Test#testCLMigrateFromV2IncludingInit.snap index 71d45f47..8b79faf9 100644 --- a/.forge-snapshots/CLMigratorFromUniswapV2Test#testCLMigrateFromV2IncludingInit.snap +++ b/.forge-snapshots/CLMigratorFromUniswapV2Test#testCLMigrateFromV2IncludingInit.snap @@ -1 +1 @@ -794983 \ No newline at end of file +795152 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromUniswapV2Test#testCLMigrateFromV2WithoutInit.snap b/.forge-snapshots/CLMigratorFromUniswapV2Test#testCLMigrateFromV2WithoutInit.snap index 7ed90cac..560772de 100644 --- a/.forge-snapshots/CLMigratorFromUniswapV2Test#testCLMigrateFromV2WithoutInit.snap +++ b/.forge-snapshots/CLMigratorFromUniswapV2Test#testCLMigrateFromV2WithoutInit.snap @@ -1 +1 @@ -660378 \ No newline at end of file +660547 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromUniswapV2Test#testCLMigrateFromV2WithoutNativeToken.snap b/.forge-snapshots/CLMigratorFromUniswapV2Test#testCLMigrateFromV2WithoutNativeToken.snap index 97cd1418..22b45184 100644 --- a/.forge-snapshots/CLMigratorFromUniswapV2Test#testCLMigrateFromV2WithoutNativeToken.snap +++ b/.forge-snapshots/CLMigratorFromUniswapV2Test#testCLMigrateFromV2WithoutNativeToken.snap @@ -1 +1 @@ -730660 \ No newline at end of file +730829 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromUniswapV3Test#testCLMigrateFromV3IncludingInit.snap b/.forge-snapshots/CLMigratorFromUniswapV3Test#testCLMigrateFromV3IncludingInit.snap index 855f8349..b61f768d 100644 --- a/.forge-snapshots/CLMigratorFromUniswapV3Test#testCLMigrateFromV3IncludingInit.snap +++ b/.forge-snapshots/CLMigratorFromUniswapV3Test#testCLMigrateFromV3IncludingInit.snap @@ -1 +1 @@ -842872 \ No newline at end of file +843041 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromUniswapV3Test#testCLMigrateFromV3WithoutInit.snap b/.forge-snapshots/CLMigratorFromUniswapV3Test#testCLMigrateFromV3WithoutInit.snap index 4b065f09..a70c7d5c 100644 --- a/.forge-snapshots/CLMigratorFromUniswapV3Test#testCLMigrateFromV3WithoutInit.snap +++ b/.forge-snapshots/CLMigratorFromUniswapV3Test#testCLMigrateFromV3WithoutInit.snap @@ -1 +1 @@ -710796 \ No newline at end of file +710965 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromUniswapV3Test#testCLMigrateFromV3WithoutNativeToken.snap b/.forge-snapshots/CLMigratorFromUniswapV3Test#testCLMigrateFromV3WithoutNativeToken.snap index 350df46e..09faaeb6 100644 --- a/.forge-snapshots/CLMigratorFromUniswapV3Test#testCLMigrateFromV3WithoutNativeToken.snap +++ b/.forge-snapshots/CLMigratorFromUniswapV3Test#testCLMigrateFromV3WithoutNativeToken.snap @@ -1 +1 @@ -751637 \ No newline at end of file +751806 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_burn_empty.snap b/.forge-snapshots/CLPositionManager_burn_empty.snap index 3a33d406..afefe08d 100644 --- a/.forge-snapshots/CLPositionManager_burn_empty.snap +++ b/.forge-snapshots/CLPositionManager_burn_empty.snap @@ -1 +1 @@ -60459 \ No newline at end of file +60740 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_burn_empty_native.snap b/.forge-snapshots/CLPositionManager_burn_empty_native.snap index 3a33d406..afefe08d 100644 --- a/.forge-snapshots/CLPositionManager_burn_empty_native.snap +++ b/.forge-snapshots/CLPositionManager_burn_empty_native.snap @@ -1 +1 @@ -60459 \ No newline at end of file +60740 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_burn_nonEmpty_native_withClose.snap b/.forge-snapshots/CLPositionManager_burn_nonEmpty_native_withClose.snap index bfad1fec..ca7016c6 100644 --- a/.forge-snapshots/CLPositionManager_burn_nonEmpty_native_withClose.snap +++ b/.forge-snapshots/CLPositionManager_burn_nonEmpty_native_withClose.snap @@ -1 +1 @@ -173977 \ No newline at end of file +174525 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_burn_nonEmpty_native_withTakePair.snap b/.forge-snapshots/CLPositionManager_burn_nonEmpty_native_withTakePair.snap index 88b45e96..015c3831 100644 --- a/.forge-snapshots/CLPositionManager_burn_nonEmpty_native_withTakePair.snap +++ b/.forge-snapshots/CLPositionManager_burn_nonEmpty_native_withTakePair.snap @@ -1 +1 @@ -173330 \ No newline at end of file +173837 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_burn_nonEmpty_withClose.snap b/.forge-snapshots/CLPositionManager_burn_nonEmpty_withClose.snap index a03c2b0b..1c82a9a0 100644 --- a/.forge-snapshots/CLPositionManager_burn_nonEmpty_withClose.snap +++ b/.forge-snapshots/CLPositionManager_burn_nonEmpty_withClose.snap @@ -1 +1 @@ -182556 \ No newline at end of file +183104 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_burn_nonEmpty_withTakePair.snap b/.forge-snapshots/CLPositionManager_burn_nonEmpty_withTakePair.snap index 573119e2..34067747 100644 --- a/.forge-snapshots/CLPositionManager_burn_nonEmpty_withTakePair.snap +++ b/.forge-snapshots/CLPositionManager_burn_nonEmpty_withTakePair.snap @@ -1 +1 @@ -181909 \ No newline at end of file +182416 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_collect_native.snap b/.forge-snapshots/CLPositionManager_collect_native.snap index af076ec1..88afcfad 100644 --- a/.forge-snapshots/CLPositionManager_collect_native.snap +++ b/.forge-snapshots/CLPositionManager_collect_native.snap @@ -1 +1 @@ -205407 \ No newline at end of file +205654 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_collect_sameRange.snap b/.forge-snapshots/CLPositionManager_collect_sameRange.snap index 6f0121bf..a30d89c3 100644 --- a/.forge-snapshots/CLPositionManager_collect_sameRange.snap +++ b/.forge-snapshots/CLPositionManager_collect_sameRange.snap @@ -1 +1 @@ -213986 \ No newline at end of file +214233 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_collect_withClose.snap b/.forge-snapshots/CLPositionManager_collect_withClose.snap index 6f0121bf..a30d89c3 100644 --- a/.forge-snapshots/CLPositionManager_collect_withClose.snap +++ b/.forge-snapshots/CLPositionManager_collect_withClose.snap @@ -1 +1 @@ -213986 \ No newline at end of file +214233 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_collect_withTakePair.snap b/.forge-snapshots/CLPositionManager_collect_withTakePair.snap index 7da2aa61..8e2db6a4 100644 --- a/.forge-snapshots/CLPositionManager_collect_withTakePair.snap +++ b/.forge-snapshots/CLPositionManager_collect_withTakePair.snap @@ -1 +1 @@ -213351 \ No newline at end of file +213557 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_decreaseLiquidity_native.snap b/.forge-snapshots/CLPositionManager_decreaseLiquidity_native.snap index 0acbf1ae..25b3f6d8 100644 --- a/.forge-snapshots/CLPositionManager_decreaseLiquidity_native.snap +++ b/.forge-snapshots/CLPositionManager_decreaseLiquidity_native.snap @@ -1 +1 @@ -170700 \ No newline at end of file +170947 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_decreaseLiquidity_withClose.snap b/.forge-snapshots/CLPositionManager_decreaseLiquidity_withClose.snap index 601944b5..1de39d92 100644 --- a/.forge-snapshots/CLPositionManager_decreaseLiquidity_withClose.snap +++ b/.forge-snapshots/CLPositionManager_decreaseLiquidity_withClose.snap @@ -1 +1 @@ -179279 \ No newline at end of file +179526 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_decreaseLiquidity_withTakePair.snap b/.forge-snapshots/CLPositionManager_decreaseLiquidity_withTakePair.snap index 210966ce..62ad1fd2 100644 --- a/.forge-snapshots/CLPositionManager_decreaseLiquidity_withTakePair.snap +++ b/.forge-snapshots/CLPositionManager_decreaseLiquidity_withTakePair.snap @@ -1 +1 @@ -178644 \ No newline at end of file +178850 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_decrease_burnEmpty.snap b/.forge-snapshots/CLPositionManager_decrease_burnEmpty.snap index f12ab19e..573f4ab6 100644 --- a/.forge-snapshots/CLPositionManager_decrease_burnEmpty.snap +++ b/.forge-snapshots/CLPositionManager_decrease_burnEmpty.snap @@ -1 +1 @@ -186292 \ No newline at end of file +186820 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_decrease_burnEmpty_native.snap b/.forge-snapshots/CLPositionManager_decrease_burnEmpty_native.snap index 832b26a8..1e568657 100644 --- a/.forge-snapshots/CLPositionManager_decrease_burnEmpty_native.snap +++ b/.forge-snapshots/CLPositionManager_decrease_burnEmpty_native.snap @@ -1 +1 @@ -177713 \ No newline at end of file +178241 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_decrease_sameRange_allLiquidity.snap b/.forge-snapshots/CLPositionManager_decrease_sameRange_allLiquidity.snap index 7368026d..ebd4c75c 100644 --- a/.forge-snapshots/CLPositionManager_decrease_sameRange_allLiquidity.snap +++ b/.forge-snapshots/CLPositionManager_decrease_sameRange_allLiquidity.snap @@ -1 +1 @@ -191904 \ No newline at end of file +192151 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_decrease_take_take.snap b/.forge-snapshots/CLPositionManager_decrease_take_take.snap index 627a94ce..cccf1135 100644 --- a/.forge-snapshots/CLPositionManager_decrease_take_take.snap +++ b/.forge-snapshots/CLPositionManager_decrease_take_take.snap @@ -1 +1 @@ -179916 \ No newline at end of file +180193 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_increaseLiquidity_erc20_withClose.snap b/.forge-snapshots/CLPositionManager_increaseLiquidity_erc20_withClose.snap index 41d4bb36..4a3f523c 100644 --- a/.forge-snapshots/CLPositionManager_increaseLiquidity_erc20_withClose.snap +++ b/.forge-snapshots/CLPositionManager_increaseLiquidity_erc20_withClose.snap @@ -1 +1 @@ -220615 \ No newline at end of file +220862 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_increaseLiquidity_erc20_withSettlePair.snap b/.forge-snapshots/CLPositionManager_increaseLiquidity_erc20_withSettlePair.snap index e3f7c88c..bde243e8 100644 --- a/.forge-snapshots/CLPositionManager_increaseLiquidity_erc20_withSettlePair.snap +++ b/.forge-snapshots/CLPositionManager_increaseLiquidity_erc20_withSettlePair.snap @@ -1 +1 @@ -219588 \ No newline at end of file +219779 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_increaseLiquidity_native.snap b/.forge-snapshots/CLPositionManager_increaseLiquidity_native.snap index a73a9886..28c8478b 100644 --- a/.forge-snapshots/CLPositionManager_increaseLiquidity_native.snap +++ b/.forge-snapshots/CLPositionManager_increaseLiquidity_native.snap @@ -1 +1 @@ -203963 \ No newline at end of file +204210 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_increase_autocompoundExactUnclaimedFees.snap b/.forge-snapshots/CLPositionManager_increase_autocompoundExactUnclaimedFees.snap index db0b12fe..edba8833 100644 --- a/.forge-snapshots/CLPositionManager_increase_autocompoundExactUnclaimedFees.snap +++ b/.forge-snapshots/CLPositionManager_increase_autocompoundExactUnclaimedFees.snap @@ -1 +1 @@ -165587 \ No newline at end of file +165716 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_increase_autocompoundExcessFeesCredit.snap b/.forge-snapshots/CLPositionManager_increase_autocompoundExcessFeesCredit.snap index ae5735b1..a53cc72f 100644 --- a/.forge-snapshots/CLPositionManager_increase_autocompoundExcessFeesCredit.snap +++ b/.forge-snapshots/CLPositionManager_increase_autocompoundExcessFeesCredit.snap @@ -1 +1 @@ -238976 \ No newline at end of file +239223 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_increase_autocompound_clearExcess.snap b/.forge-snapshots/CLPositionManager_increase_autocompound_clearExcess.snap index 55d9f415..a3569c79 100644 --- a/.forge-snapshots/CLPositionManager_increase_autocompound_clearExcess.snap +++ b/.forge-snapshots/CLPositionManager_increase_autocompound_clearExcess.snap @@ -1 +1 @@ -209828 \ No newline at end of file +210093 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_mint_native.snap b/.forge-snapshots/CLPositionManager_mint_native.snap index ce0f2ac2..bd310e86 100644 --- a/.forge-snapshots/CLPositionManager_mint_native.snap +++ b/.forge-snapshots/CLPositionManager_mint_native.snap @@ -1 +1 @@ -540281 \ No newline at end of file +540506 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_mint_nativeWithSweep_withClose.snap b/.forge-snapshots/CLPositionManager_mint_nativeWithSweep_withClose.snap index 1cb05bb3..af7f724d 100644 --- a/.forge-snapshots/CLPositionManager_mint_nativeWithSweep_withClose.snap +++ b/.forge-snapshots/CLPositionManager_mint_nativeWithSweep_withClose.snap @@ -1 +1 @@ -548803 \ No newline at end of file +549102 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_mint_nativeWithSweep_withSettlePair.snap b/.forge-snapshots/CLPositionManager_mint_nativeWithSweep_withSettlePair.snap index c6a36ac1..91747cf8 100644 --- a/.forge-snapshots/CLPositionManager_mint_nativeWithSweep_withSettlePair.snap +++ b/.forge-snapshots/CLPositionManager_mint_nativeWithSweep_withSettlePair.snap @@ -1 +1 @@ -548060 \ No newline at end of file +548303 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_mint_onSameTickLower.snap b/.forge-snapshots/CLPositionManager_mint_onSameTickLower.snap index cae6b231..83589503 100644 --- a/.forge-snapshots/CLPositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/CLPositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -393705 \ No newline at end of file +393930 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/CLPositionManager_mint_onSameTickUpper.snap index 0c555fb3..63c9ec41 100644 --- a/.forge-snapshots/CLPositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/CLPositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -394142 \ No newline at end of file +394367 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_mint_sameRange.snap b/.forge-snapshots/CLPositionManager_mint_sameRange.snap index 9b408a9d..31a5da85 100644 --- a/.forge-snapshots/CLPositionManager_mint_sameRange.snap +++ b/.forge-snapshots/CLPositionManager_mint_sameRange.snap @@ -1 +1 @@ -302930 \ No newline at end of file +303155 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_mint_settleWithBalance_sweep.snap b/.forge-snapshots/CLPositionManager_mint_settleWithBalance_sweep.snap index 59fddf44..7ca8f059 100644 --- a/.forge-snapshots/CLPositionManager_mint_settleWithBalance_sweep.snap +++ b/.forge-snapshots/CLPositionManager_mint_settleWithBalance_sweep.snap @@ -1 +1 @@ -593021 \ No newline at end of file +593376 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_mint_warmedPool_differentRange.snap b/.forge-snapshots/CLPositionManager_mint_warmedPool_differentRange.snap index 597a3a52..5afd8690 100644 --- a/.forge-snapshots/CLPositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/CLPositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -399417 \ No newline at end of file +399642 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_mint_withClose.snap b/.forge-snapshots/CLPositionManager_mint_withClose.snap index d982607d..7179b854 100644 --- a/.forge-snapshots/CLPositionManager_mint_withClose.snap +++ b/.forge-snapshots/CLPositionManager_mint_withClose.snap @@ -1 +1 @@ -594173 \ No newline at end of file +594398 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_mint_withSettlePair.snap b/.forge-snapshots/CLPositionManager_mint_withSettlePair.snap index 9321b1e3..7e2ddaa7 100644 --- a/.forge-snapshots/CLPositionManager_mint_withSettlePair.snap +++ b/.forge-snapshots/CLPositionManager_mint_withSettlePair.snap @@ -1 +1 @@ -593288 \ No newline at end of file +593457 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_multicall_initialize_mint.snap b/.forge-snapshots/CLPositionManager_multicall_initialize_mint.snap index ad06fbdb..01c18d5a 100644 --- a/.forge-snapshots/CLPositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/CLPositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -652186 \ No newline at end of file +652411 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_subscribe.snap b/.forge-snapshots/CLPositionManager_subscribe.snap index a56ff566..89efd410 100644 --- a/.forge-snapshots/CLPositionManager_subscribe.snap +++ b/.forge-snapshots/CLPositionManager_subscribe.snap @@ -1 +1 @@ -87786 \ No newline at end of file +87808 \ No newline at end of file diff --git a/.forge-snapshots/CLPositionManager_unsubscribe.snap b/.forge-snapshots/CLPositionManager_unsubscribe.snap index b8ae81fb..9aab9024 100644 --- a/.forge-snapshots/CLPositionManager_unsubscribe.snap +++ b/.forge-snapshots/CLPositionManager_unsubscribe.snap @@ -1 +1 @@ -62634 \ No newline at end of file +62678 \ No newline at end of file diff --git a/.forge-snapshots/CLSwapRouterTest#ExactInput.snap b/.forge-snapshots/CLSwapRouterTest#ExactInput.snap index 56b09335..02197716 100644 --- a/.forge-snapshots/CLSwapRouterTest#ExactInput.snap +++ b/.forge-snapshots/CLSwapRouterTest#ExactInput.snap @@ -1 +1 @@ -239536 \ No newline at end of file +239707 \ No newline at end of file diff --git a/.forge-snapshots/CLSwapRouterTest#ExactInputSingle.snap b/.forge-snapshots/CLSwapRouterTest#ExactInputSingle.snap index b4b50d7b..6b10ac88 100644 --- a/.forge-snapshots/CLSwapRouterTest#ExactInputSingle.snap +++ b/.forge-snapshots/CLSwapRouterTest#ExactInputSingle.snap @@ -1 +1 @@ -175058 \ No newline at end of file +175229 \ No newline at end of file diff --git a/.forge-snapshots/CLSwapRouterTest#ExactOutput.snap b/.forge-snapshots/CLSwapRouterTest#ExactOutput.snap index d78d6cfc..92f7ced8 100644 --- a/.forge-snapshots/CLSwapRouterTest#ExactOutput.snap +++ b/.forge-snapshots/CLSwapRouterTest#ExactOutput.snap @@ -1 +1 @@ -238436 \ No newline at end of file +238607 \ No newline at end of file diff --git a/.forge-snapshots/CLSwapRouterTest#ExactOutputSingle.snap b/.forge-snapshots/CLSwapRouterTest#ExactOutputSingle.snap index 73c54812..7131cc98 100644 --- a/.forge-snapshots/CLSwapRouterTest#ExactOutputSingle.snap +++ b/.forge-snapshots/CLSwapRouterTest#ExactOutputSingle.snap @@ -1 +1 @@ -174773 \ No newline at end of file +174932 \ No newline at end of file diff --git a/script/04_DeployCLQuoter.s.sol b/script/04_DeployCLQuoter.s.sol index e85d89b0..2607b958 100644 --- a/script/04_DeployCLQuoter.s.sol +++ b/script/04_DeployCLQuoter.s.sol @@ -19,7 +19,7 @@ import {Create3Factory} from "pancake-create3-factory/src/Create3Factory.sol"; */ contract DeployCLQuoterScript is BaseScript { function getDeploymentSalt() public pure override returns (bytes32) { - return keccak256("PANCAKE-V4-PERIPHERY/CLQuoter/0.90"); + return keccak256("PANCAKE-V4-PERIPHERY/CLQuoter/0.92"); } function run() public { diff --git a/script/05_DeployBinQuoter.s.sol b/script/05_DeployBinQuoter.s.sol index 31653e27..e75ff243 100644 --- a/script/05_DeployBinQuoter.s.sol +++ b/script/05_DeployBinQuoter.s.sol @@ -19,7 +19,7 @@ import {Create3Factory} from "pancake-create3-factory/src/Create3Factory.sol"; */ contract DeployBinQuoterScript is BaseScript { function getDeploymentSalt() public pure override returns (bytes32) { - return keccak256("PANCAKE-V4-PERIPHERY/BinQuoter/0.90"); + return keccak256("PANCAKE-V4-PERIPHERY/BinQuoter/0.92"); } function run() public { diff --git a/script/08_DeployMixedQuoter.s.sol b/script/08_DeployMixedQuoter.s.sol index c13cb2cd..69376845 100644 --- a/script/08_DeployMixedQuoter.s.sol +++ b/script/08_DeployMixedQuoter.s.sol @@ -21,7 +21,7 @@ import {Create3Factory} from "pancake-create3-factory/src/Create3Factory.sol"; */ contract DeployMixedQuoterScript is BaseScript { function getDeploymentSalt() public pure override returns (bytes32) { - return keccak256("PANCAKE-V4-PERIPHERY/MixedQuoter/0.90"); + return keccak256("PANCAKE-V4-PERIPHERY/MixedQuoter/0.92"); } function run() public { diff --git a/script/config/bsc-testnet.json b/script/config/bsc-testnet.json index 3daff738..b96323ec 100644 --- a/script/config/bsc-testnet.json +++ b/script/config/bsc-testnet.json @@ -11,13 +11,13 @@ "clPositionManagerUnsubscribeGasLimit": 200000, "clPositionManager": "0x7E7856fBE18cd868dc9E2C161a7a78c53074D106", "binPositionManager": "0x69317a4bF9Cd6bED6ea9b5C61ebcf78b5994A63E", - "clQuoter": "0x0ecFc10603eB4D0E2259315a591Cad5fF7CDCc88", - "binQuoter": "0xAC2c2B2Be97D8c54Cf37556E4c859B1F27940bF9", + "clQuoter": "0xEc9f9fEaA3e59EbAe99AeEE6b1a96BbE12eAacf3", + "binQuoter": "0x1eF329F725301459756686253c98A64b694C179F", "clMigrator": "0x55C4AEf425b3B67a4d94A43e911cfDf6F09b75E3.", "binMigrator": "0x4EEa46056249331D233d9AD34922fBE327D7bF43", "factoryV3": "0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865", "factoryV2": "0x6725F303b657a9451d8BA641348b6761A6CC7a17", "factoryStable": "0xe6A00f8b819244e8Ab9Ea930e46449C2F20B6609", - "mixedQuoter": "0xf7F39B8e1FeEde7aa8BB90e90D42bd3aC657AD89", + "mixedQuoter": "0xF430913B5E626d6001a4f9989C90C45fBBd1Ac37", "clTickLens": "0x2F416FC37e47624daf8fEB5b0805eA5f3F49A0Cb" } diff --git a/src/MixedQuoter.sol b/src/MixedQuoter.sol index dcdd2fbd..1a8a64aa 100644 --- a/src/MixedQuoter.sol +++ b/src/MixedQuoter.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity 0.8.26; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; @@ -15,13 +16,15 @@ import {V3PoolTicksCounter} from "./libraries/external/V3PoolTicksCounter.sol"; import {V3SmartRouterHelper} from "./libraries/external/V3SmartRouterHelper.sol"; import {IMixedQuoter} from "./interfaces/IMixedQuoter.sol"; import {MixedQuoterActions} from "./libraries/MixedQuoterActions.sol"; +import {MixedQuoterRecorder} from "./libraries/MixedQuoterRecorder.sol"; +import {Multicall_v4} from "./base/Multicall_v4.sol"; /// @title Provides on chain quotes for v4, V3, V2, Stable and MixedRoute exact input swaps /// @notice Allows getting the expected amount out for a given swap without executing the swap /// @notice Does not support exact output swaps since using the contract balance between exactOut swaps is not supported /// @dev These functions are not gas efficient and should _not_ be called on chain. Instead, optimistically execute /// the swap and check the amounts in the callback. -contract MixedQuoter is IMixedQuoter, IPancakeV3SwapCallback { +contract MixedQuoter is IMixedQuoter, IPancakeV3SwapCallback, Multicall_v4 { using SafeCast for *; using V3PoolTicksCounter for IPancakeV3Pool; @@ -158,10 +161,21 @@ contract MixedQuoter is IMixedQuoter, IPancakeV3SwapCallback { override returns (uint256 amountOut, uint256 gasEstimate) { + return quoteExactInputSingleV2WithAccumulation(params, 0, 0); + } + + /// @dev Fetch an exactIn quote for a V2 pair on chain with token accumulation + function quoteExactInputSingleV2WithAccumulation( + QuoteExactInputSingleV2Params memory params, + uint256 accTokenInAmount, + uint256 accTokenOutAmount + ) internal view returns (uint256 amountOut, uint256 gasEstimate) { uint256 gasBefore = gasleft(); (uint256 reserveIn, uint256 reserveOut) = V3SmartRouterHelper.getReserves(factoryV2, params.tokenIn, params.tokenOut); - amountOut = V3SmartRouterHelper.getAmountOut(params.amountIn, reserveIn, reserveOut); + amountOut = V3SmartRouterHelper.getAmountOut( + params.amountIn, reserveIn + accTokenInAmount, reserveOut - accTokenOutAmount + ); gasEstimate = gasBefore - gasleft(); } @@ -186,12 +200,35 @@ contract MixedQuoter is IMixedQuoter, IPancakeV3SwapCallback { /** * Mixed ************************************************* */ + /// @dev All swap results will influence the outcome of subsequent swaps within the same pool + function quoteMixedExactInputSharedContext( + address[] calldata paths, + bytes calldata actions, + bytes[] calldata params, + uint256 amountIn + ) external override returns (uint256 amountOut, uint256 gasEstimate) { + return quoteMixedExactInputWithContext(paths, actions, params, amountIn, true); + } + function quoteMixedExactInput( address[] calldata paths, bytes calldata actions, bytes[] calldata params, uint256 amountIn ) external override returns (uint256 amountOut, uint256 gasEstimate) { + return quoteMixedExactInputWithContext(paths, actions, params, amountIn, false); + } + + /// @dev if withContext is false, each swap is isolated and does not influence the outcome of subsequent swaps within the same pool + /// @dev if withContext is true, all swap results will influence the outcome of subsequent swaps within the same pool + /// @dev if withContext is true, non-v4 pools only support one swap direction for same pool + function quoteMixedExactInputWithContext( + address[] calldata paths, + bytes calldata actions, + bytes[] calldata params, + uint256 amountIn, + bool withContext + ) private returns (uint256 amountOut, uint256 gasEstimate) { uint256 numActions = actions.length; if (numActions == 0) revert NoActions(); if (numActions != params.length || numActions != paths.length - 1) revert InputLengthMismatch(); @@ -206,62 +243,174 @@ contract MixedQuoter is IMixedQuoter, IPancakeV3SwapCallback { if (action == MixedQuoterActions.V2_EXACT_INPUT_SINGLE) { (tokenIn, tokenOut) = convertNativeToWETH(tokenIn, tokenOut); // params[actionIndex] is zero bytes - (amountIn, gasEstimateForCurAction) = quoteExactInputSingleV2( - QuoteExactInputSingleV2Params({tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn}) - ); + if (!withContext) { + (amountIn, gasEstimateForCurAction) = quoteExactInputSingleV2( + QuoteExactInputSingleV2Params({tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn}) + ); + } else { + bool zeroForOne = tokenIn < tokenOut; + bytes32 poolHash = MixedQuoterRecorder.getV2PoolHash(tokenIn, tokenOut); + // update v2 pool swap direction, only allow one direction in one transaction + MixedQuoterRecorder.setAndCheckSwapDirection(poolHash, zeroForOne); + (uint256 accAmountIn, uint256 accAmountOut) = + MixedQuoterRecorder.getPoolSwapTokenAccumulation(poolHash, zeroForOne); + + uint256 swapAmountOut; + (swapAmountOut, gasEstimateForCurAction) = quoteExactInputSingleV2WithAccumulation( + QuoteExactInputSingleV2Params({tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn}), + accAmountIn, + accAmountOut + ); + MixedQuoterRecorder.setPoolSwapTokenAccumulation( + poolHash, amountIn + accAmountIn, swapAmountOut + accAmountOut, zeroForOne + ); + amountIn = swapAmountOut; + } } else if (action == MixedQuoterActions.V3_EXACT_INPUT_SINGLE) { (tokenIn, tokenOut) = convertNativeToWETH(tokenIn, tokenOut); // params[actionIndex]: abi.encode(fee) uint24 fee = abi.decode(params[actionIndex], (uint24)); - (amountIn,,, gasEstimateForCurAction) = quoteExactInputSingleV3( - QuoteExactInputSingleV3Params({ - tokenIn: tokenIn, - tokenOut: tokenOut, - amountIn: amountIn, - fee: fee, - sqrtPriceLimitX96: 0 - }) - ); + if (!withContext) { + (amountIn,,, gasEstimateForCurAction) = quoteExactInputSingleV3( + QuoteExactInputSingleV3Params({ + tokenIn: tokenIn, + tokenOut: tokenOut, + amountIn: amountIn, + fee: fee, + sqrtPriceLimitX96: 0 + }) + ); + } else { + bool zeroForOne = tokenIn < tokenOut; + bytes32 poolHash = MixedQuoterRecorder.getV3PoolHash(tokenIn, tokenOut, fee); + // update v3 pool swap direction, only allow one direction in one transaction + MixedQuoterRecorder.setAndCheckSwapDirection(poolHash, zeroForOne); + (uint256 accAmountIn, uint256 accAmountOut) = + MixedQuoterRecorder.getPoolSwapTokenAccumulation(poolHash, zeroForOne); + + uint256 swapAmountOut; + amountIn += accAmountIn; + (swapAmountOut,,, gasEstimateForCurAction) = quoteExactInputSingleV3( + QuoteExactInputSingleV3Params({ + tokenIn: tokenIn, + tokenOut: tokenOut, + amountIn: amountIn, + fee: fee, + sqrtPriceLimitX96: 0 + }) + ); + MixedQuoterRecorder.setPoolSwapTokenAccumulation(poolHash, amountIn, swapAmountOut, zeroForOne); + amountIn = swapAmountOut - accAmountOut; + } } else if (action == MixedQuoterActions.V4_CL_EXACT_INPUT_SINGLE) { QuoteMixedV4ExactInputSingleParams memory clParams = abi.decode(params[actionIndex], (QuoteMixedV4ExactInputSingleParams)); (tokenIn, tokenOut) = convertWETHToV4NativeCurency(clParams.poolKey, tokenIn, tokenOut); bool zeroForOne = tokenIn < tokenOut; checkV4PoolKeyCurrency(clParams.poolKey, zeroForOne, tokenIn, tokenOut); - (amountIn, gasEstimateForCurAction) = clQuoter.quoteExactInputSingle( - IQuoter.QuoteExactSingleParams({ - poolKey: clParams.poolKey, - zeroForOne: zeroForOne, - exactAmount: amountIn.toUint128(), - hookData: clParams.hookData - }) - ); + + IQuoter.QuoteExactSingleParams memory swapParams = IQuoter.QuoteExactSingleParams({ + poolKey: clParams.poolKey, + zeroForOne: zeroForOne, + exactAmount: amountIn.toUint128(), + hookData: clParams.hookData + }); + // will execute all swap history of same v4 pool in one transaction if withContext is true + if (withContext) { + bytes32 poolHash = MixedQuoterRecorder.getV4CLPoolHash(clParams.poolKey); + bytes memory swapListBytes = MixedQuoterRecorder.getV4PoolSwapList(poolHash); + IQuoter.QuoteExactSingleParams[] memory swapHistoryList; + uint256 swapHistoryListLength; + if (swapListBytes.length > 0) { + swapHistoryList = abi.decode(swapListBytes, (IQuoter.QuoteExactSingleParams[])); + + swapHistoryListLength = swapHistoryList.length; + } + IQuoter.QuoteExactSingleParams[] memory swapList = + new IQuoter.QuoteExactSingleParams[](swapHistoryListLength + 1); + for (uint256 i = 0; i < swapHistoryListLength; i++) { + swapList[i] = swapHistoryList[i]; + } + swapList[swapHistoryListLength] = swapParams; + + (amountIn, gasEstimateForCurAction) = clQuoter.quoteExactInputSingleList(swapList); + swapListBytes = abi.encode(swapList); + MixedQuoterRecorder.setV4PoolSwapList(poolHash, swapListBytes); + } else { + (amountIn, gasEstimateForCurAction) = clQuoter.quoteExactInputSingle(swapParams); + } } else if (action == MixedQuoterActions.V4_BIN_EXACT_INPUT_SINGLE) { QuoteMixedV4ExactInputSingleParams memory binParams = abi.decode(params[actionIndex], (QuoteMixedV4ExactInputSingleParams)); (tokenIn, tokenOut) = convertWETHToV4NativeCurency(binParams.poolKey, tokenIn, tokenOut); bool zeroForOne = tokenIn < tokenOut; checkV4PoolKeyCurrency(binParams.poolKey, zeroForOne, tokenIn, tokenOut); - (amountIn, gasEstimateForCurAction) = binQuoter.quoteExactInputSingle( - IQuoter.QuoteExactSingleParams({ - poolKey: binParams.poolKey, - zeroForOne: zeroForOne, - exactAmount: amountIn.toUint128(), - hookData: binParams.hookData - }) - ); + + IQuoter.QuoteExactSingleParams memory swapParams = IQuoter.QuoteExactSingleParams({ + poolKey: binParams.poolKey, + zeroForOne: zeroForOne, + exactAmount: amountIn.toUint128(), + hookData: binParams.hookData + }); + // will execute all swap history of same v4 pool in one transaction if withContext is true + if (withContext) { + bytes32 poolHash = MixedQuoterRecorder.getV4BinPoolHash(binParams.poolKey); + bytes memory swapListBytes = MixedQuoterRecorder.getV4PoolSwapList(poolHash); + IQuoter.QuoteExactSingleParams[] memory swapHistoryList; + uint256 swapHistoryListLength; + if (swapListBytes.length > 0) { + swapHistoryList = abi.decode(swapListBytes, (IQuoter.QuoteExactSingleParams[])); + + swapHistoryListLength = swapHistoryList.length; + } + IQuoter.QuoteExactSingleParams[] memory swapList = + new IQuoter.QuoteExactSingleParams[](swapHistoryListLength + 1); + for (uint256 i = 0; i < swapHistoryListLength; i++) { + swapList[i] = swapHistoryList[i]; + } + swapList[swapHistoryListLength] = swapParams; + + (amountIn, gasEstimateForCurAction) = binQuoter.quoteExactInputSingleList(swapList); + swapListBytes = abi.encode(swapList); + MixedQuoterRecorder.setV4PoolSwapList(poolHash, swapListBytes); + } else { + (amountIn, gasEstimateForCurAction) = binQuoter.quoteExactInputSingle(swapParams); + } } else if (action == MixedQuoterActions.SS_2_EXACT_INPUT_SINGLE) { (tokenIn, tokenOut) = convertNativeToWETH(tokenIn, tokenOut); // params[actionIndex] is zero bytes - (amountIn, gasEstimateForCurAction) = quoteExactInputSingleStable( - QuoteExactInputSingleStableParams({ - tokenIn: tokenIn, - tokenOut: tokenOut, - amountIn: amountIn, - flag: 2 - }) - ); + + if (!withContext) { + (amountIn, gasEstimateForCurAction) = quoteExactInputSingleStable( + QuoteExactInputSingleStableParams({ + tokenIn: tokenIn, + tokenOut: tokenOut, + amountIn: amountIn, + flag: 2 + }) + ); + } else { + bool zeroForOne = tokenIn < tokenOut; + bytes32 poolHash = MixedQuoterRecorder.getSSPoolHash(tokenIn, tokenOut); + // update stable pool swap direction, only allow one direction in one transaction + MixedQuoterRecorder.setAndCheckSwapDirection(poolHash, zeroForOne); + (uint256 accAmountIn, uint256 accAmountOut) = + MixedQuoterRecorder.getPoolSwapTokenAccumulation(poolHash, zeroForOne); + uint256 swapAmountOut; + amountIn += accAmountIn; + (swapAmountOut, gasEstimateForCurAction) = quoteExactInputSingleStable( + QuoteExactInputSingleStableParams({ + tokenIn: tokenIn, + tokenOut: tokenOut, + amountIn: amountIn, + flag: 2 + }) + ); + MixedQuoterRecorder.setPoolSwapTokenAccumulation(poolHash, amountIn, swapAmountOut, zeroForOne); + amountIn = swapAmountOut - accAmountOut; + } } else if (action == MixedQuoterActions.SS_3_EXACT_INPUT_SINGLE) { + /// @dev PCS do not support three pool stable swap, so will skip context mode (tokenIn, tokenOut) = convertNativeToWETH(tokenIn, tokenOut); // params[actionIndex] is zero bytes (amountIn, gasEstimateForCurAction) = quoteExactInputSingleStable( diff --git a/src/V4Router.sol b/src/V4Router.sol index 2314afcf..0b1065fc 100644 --- a/src/V4Router.sol +++ b/src/V4Router.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.24; import {IVault} from "pancake-v4-core/src/interfaces/IVault.sol"; diff --git a/src/base/BaseActionsRouter.sol b/src/base/BaseActionsRouter.sol index c8da5287..f8d3d929 100644 --- a/src/base/BaseActionsRouter.sol +++ b/src/base/BaseActionsRouter.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.24; import {IVault} from "pancake-v4-core/src/interfaces/IVault.sol"; diff --git a/src/base/DeltaResolver.sol b/src/base/DeltaResolver.sol index 87301ad7..afaeec54 100644 --- a/src/base/DeltaResolver.sol +++ b/src/base/DeltaResolver.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.24; import {Currency} from "pancake-v4-core/src/types/Currency.sol"; diff --git a/src/base/ImmutableState.sol b/src/base/ImmutableState.sol index e957b886..2871c936 100644 --- a/src/base/ImmutableState.sol +++ b/src/base/ImmutableState.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; import {IVault} from "pancake-v4-core/src/interfaces/IVault.sol"; diff --git a/src/base/Multicall_v4.sol b/src/base/Multicall_v4.sol index e9be9fb5..e7dd25f1 100644 --- a/src/base/Multicall_v4.sol +++ b/src/base/Multicall_v4.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; import {IMulticall_v4} from "../interfaces/IMulticall_v4.sol"; diff --git a/src/base/NativeWrapper.sol b/src/base/NativeWrapper.sol index 1a36e035..7e44b2c0 100644 --- a/src/base/NativeWrapper.sol +++ b/src/base/NativeWrapper.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; import {IWETH9} from "../interfaces/external/IWETH9.sol"; diff --git a/src/base/Permit2Forwarder.sol b/src/base/Permit2Forwarder.sol index c90b406a..65a08837 100644 --- a/src/base/Permit2Forwarder.sol +++ b/src/base/Permit2Forwarder.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; diff --git a/src/base/ReentrancyLock.sol b/src/base/ReentrancyLock.sol index 1e9621d7..092fae24 100644 --- a/src/base/ReentrancyLock.sol +++ b/src/base/ReentrancyLock.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.24; /// @notice A transient reentrancy lock, that stores the caller's address as the lock diff --git a/src/base/SafeCallback.sol b/src/base/SafeCallback.sol index 43e0c9c3..d1951b61 100644 --- a/src/base/SafeCallback.sol +++ b/src/base/SafeCallback.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.24; import {ILockCallback} from "pancake-v4-core/src/interfaces/ILockCallback.sol"; diff --git a/src/interfaces/IBaseMigrator.sol b/src/interfaces/IBaseMigrator.sol index 4c4661c7..b30409ed 100644 --- a/src/interfaces/IBaseMigrator.sol +++ b/src/interfaces/IBaseMigrator.sol @@ -1,5 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright (C) 2024 PancakeSwap +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; diff --git a/src/interfaces/IImmutableState.sol b/src/interfaces/IImmutableState.sol index e5b963d8..cea968fc 100644 --- a/src/interfaces/IImmutableState.sol +++ b/src/interfaces/IImmutableState.sol @@ -1,5 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright (C) 2024 PancakeSwap +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IVault} from "pancake-v4-core/src/interfaces/IVault.sol"; diff --git a/src/interfaces/IMixedQuoter.sol b/src/interfaces/IMixedQuoter.sol index 0d363641..bea3ca46 100644 --- a/src/interfaces/IMixedQuoter.sol +++ b/src/interfaces/IMixedQuoter.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; @@ -62,6 +62,27 @@ interface IMixedQuoter { uint256 amountIn ) external returns (uint256 amountOut, uint256 gasEstimate); + /// @notice Returns the amount out received for a given exact input swap without executing the swap + /// @dev All swap results will influence the outcome of subsequent swaps within the same pool + /// @param paths The path of the swap, i.e. each token pair in the path + /// @param actions The actions to take for each pair in the path + /// @param params The params for each action in the path + /// SS_2_EXACT_INPUT_SINGLE params are zero bytes + /// SS_3_EXACT_INPUT_SINGLE params are zero bytes + /// V2_EXACT_INPUT_SINGLE params are zero bytes + /// V3_EXACT_INPUT_SINGLE params are encoded as `uint24 fee` + /// V4_CL_EXACT_INPUT_SINGLE params are encoded as `QuoteMixedV4ExactInputSingleParams` + /// V4_EXACT_INPUT_SINGLE params are encoded as `QuoteMixedV4ExactInputSingleParams` + /// @param amountIn The amount of the first token to swap + /// @return amountOut The amount of the last token that would be received + /// @return gasEstimate The estimate of the gas that the swap consumes + function quoteMixedExactInputSharedContext( + address[] calldata paths, + bytes calldata actions, + bytes[] calldata params, + uint256 amountIn + ) external returns (uint256 amountOut, uint256 gasEstimate); + /// @notice Returns the amount out received for a given exact input but for a swap of a single pool /// @param params The params for the quote, encoded as `QuoteExactInputSingleParams` /// tokenIn The token being swapped in diff --git a/src/interfaces/IMulticall_v4.sol b/src/interfaces/IMulticall_v4.sol index 1d053a97..07c321b3 100644 --- a/src/interfaces/IMulticall_v4.sol +++ b/src/interfaces/IMulticall_v4.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @title Multicall_v4 interface diff --git a/src/interfaces/IPoolManager.sol b/src/interfaces/IPoolManager.sol index 1c9e08f3..b84e5a66 100644 --- a/src/interfaces/IPoolManager.sol +++ b/src/interfaces/IPoolManager.sol @@ -1,5 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright (C) 2024 PancakeSwap +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {IVault} from "pancake-v4-core/src/interfaces/IVault.sol"; diff --git a/src/interfaces/IPositionManager.sol b/src/interfaces/IPositionManager.sol index b6b8a0cf..d24c0fc1 100644 --- a/src/interfaces/IPositionManager.sol +++ b/src/interfaces/IPositionManager.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {IImmutableState} from "./IImmutableState.sol"; diff --git a/src/interfaces/IPositionManagerPermit2.sol b/src/interfaces/IPositionManagerPermit2.sol index 6abc37ab..988ed826 100644 --- a/src/interfaces/IPositionManagerPermit2.sol +++ b/src/interfaces/IPositionManagerPermit2.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; diff --git a/src/interfaces/IQuoter.sol b/src/interfaces/IQuoter.sol index fd6c4478..a6328362 100644 --- a/src/interfaces/IQuoter.sol +++ b/src/interfaces/IQuoter.sol @@ -1,5 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright (C) 2024 PancakeSwap +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Currency} from "pancake-v4-core/src/types/Currency.sol"; diff --git a/src/interfaces/IV4Router.sol b/src/interfaces/IV4Router.sol index 5c307f0a..dfeec0c6 100644 --- a/src/interfaces/IV4Router.sol +++ b/src/interfaces/IV4Router.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; diff --git a/src/libraries/ActionConstants.sol b/src/libraries/ActionConstants.sol index 182ba7a9..dade3cd3 100644 --- a/src/libraries/ActionConstants.sol +++ b/src/libraries/ActionConstants.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; /// @title Action Constants diff --git a/src/libraries/Actions.sol b/src/libraries/Actions.sol index ad912ac8..0d35f6a1 100644 --- a/src/libraries/Actions.sol +++ b/src/libraries/Actions.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; /// @notice Library to define different pool actions. diff --git a/src/libraries/BipsLibrary.sol b/src/libraries/BipsLibrary.sol index 5521c603..dd36d96c 100644 --- a/src/libraries/BipsLibrary.sol +++ b/src/libraries/BipsLibrary.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; /// @title For calculating a percentage of an amount, using bips diff --git a/src/libraries/CalldataDecoder.sol b/src/libraries/CalldataDecoder.sol index 731827a8..44b44a75 100644 --- a/src/libraries/CalldataDecoder.sol +++ b/src/libraries/CalldataDecoder.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; import {Currency} from "pancake-v4-core/src/types/Currency.sol"; @@ -71,6 +72,10 @@ library CalldataDecoder { /// @dev equivalent to: abi.decode(params, (Currency)) in calldata function decodeCurrency(bytes calldata params) internal pure returns (Currency currency) { assembly ("memory-safe") { + if lt(params.length, 0x20) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency := calldataload(params.offset) } } @@ -78,6 +83,10 @@ library CalldataDecoder { /// @dev equivalent to: abi.decode(params, (Currency, Currency)) in calldata function decodeCurrencyPair(bytes calldata params) internal pure returns (Currency currency0, Currency currency1) { assembly ("memory-safe") { + if lt(params.length, 0x40) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency0 := calldataload(params.offset) currency1 := calldataload(add(params.offset, 0x20)) } @@ -90,6 +99,10 @@ library CalldataDecoder { returns (Currency currency0, Currency currency1, address _address) { assembly ("memory-safe") { + if lt(params.length, 0x60) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency0 := calldataload(params.offset) currency1 := calldataload(add(params.offset, 0x20)) _address := calldataload(add(params.offset, 0x40)) @@ -103,6 +116,10 @@ library CalldataDecoder { returns (Currency currency, address _address) { assembly ("memory-safe") { + if lt(params.length, 0x40) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency := calldataload(params.offset) _address := calldataload(add(params.offset, 0x20)) } @@ -115,6 +132,10 @@ library CalldataDecoder { returns (Currency currency, address _address, uint256 amount) { assembly ("memory-safe") { + if lt(params.length, 0x60) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency := calldataload(params.offset) _address := calldataload(add(params.offset, 0x20)) amount := calldataload(add(params.offset, 0x40)) @@ -128,6 +149,10 @@ library CalldataDecoder { returns (Currency currency, uint256 amount) { assembly ("memory-safe") { + if lt(params.length, 0x40) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency := calldataload(params.offset) amount := calldataload(add(params.offset, 0x20)) } @@ -136,6 +161,10 @@ library CalldataDecoder { /// @dev equivalent to: abi.decode(params, (uint256)) in calldata function decodeUint256(bytes calldata params) internal pure returns (uint256 amount) { assembly ("memory-safe") { + if lt(params.length, 0x20) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } amount := calldataload(params.offset) } } @@ -147,6 +176,10 @@ library CalldataDecoder { returns (Currency currency, uint256 amount, bool boolean) { assembly ("memory-safe") { + if lt(params.length, 0x60) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency := calldataload(params.offset) amount := calldataload(add(params.offset, 0x20)) boolean := calldataload(add(params.offset, 0x40)) diff --git a/src/libraries/MixedQuoterActions.sol b/src/libraries/MixedQuoterActions.sol index 53518774..3c44d049 100644 --- a/src/libraries/MixedQuoterActions.sol +++ b/src/libraries/MixedQuoterActions.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; /// @notice Library to define different mixed quoter actions. diff --git a/src/libraries/MixedQuoterRecorder.sol b/src/libraries/MixedQuoterRecorder.sol new file mode 100644 index 00000000..7f8e7dfd --- /dev/null +++ b/src/libraries/MixedQuoterRecorder.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap +pragma solidity 0.8.26; + +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; + +/// @dev Record all token accumulation and swap direction of the transaction for non-v4 pools. +/// @dev Record v4 swap history list for v4 pools. +library MixedQuoterRecorder { + /// @dev uint256 internal constant SWAP_DIRECTION = uint256(keccak256("MIXED_QUOTER_SWAP_DIRECTION")) - 1; + uint256 internal constant SWAP_DIRECTION = 0x420071594cddc2905acbd674683749db4c139d373cc290ba8d49c75296a9f1f9; + + /// @dev uint256 internal constant SWAP_TOKEN0_ACCUMULATION = uint256(keccak256("MIXED_QUOTER_SWAP_TOKEN0_ACCUMULATION")) - 1; + uint256 internal constant SWAP_TOKEN0_ACCUMULATION = + 0x6859b060ba2f84c00df66c40e8848222c89b2fcc89d5edc84074b9878818ea86; + + /// @dev uint256 internal constant SWAP_TOKEN1_ACCUMULATION = uint256(keccak256("MIXED_QUOTER_SWAP_TOKEN1_ACCUMULATION")) - 1; + uint256 internal constant SWAP_TOKEN1_ACCUMULATION = + 0x8039a0cfe43b448f327ddf378771d67fba431d4dbc5c8f9531fa80f8a45125e9; + + /// @dev uint256 internal SWAP_SS = uint256(keccak256("MIXED_QUOTER_SWAP_SS")) - 1; + uint256 internal constant SWAP_SS = 0x0b6c8b64c3ab4ac7b96ca59ae1454278ba2d62c99873c03d98ae968df846210a; + + /// @dev uint256 internal SWAP_V2 = uint256(keccak256("MIXED_QUOTER_SWAP_V2")) - 1; + uint256 internal constant SWAP_V2 = 0xfb50ad98219c08ac49c2f2012c28ee455be42a0adc9a9a5df9e0882de4cf56b5; + + /// @dev uint256 internal constant SWAP_V3 = uint256(keccak256("MIXED_QUOTER_SWAP_V3")) - 1; + uint256 internal constant SWAP_V3 = 0xd9d373c35d602baa7832c86d4af60fe46a2e18634c87bebc20d0050afb7633b3; + + /// @dev uint256 internal constant SWAP_V4_CL = uint256(keccak256("MIXED_QUOTER_SWAP_V4_CL")) - 1; + uint256 internal constant SWAP_V4_CL = 0x1a7c9a13842b613486d9207eda875c24e33425305b8b8df2e040c19ef2ae3088; + + /// @dev uint256 internal constant SWAP_V4_BIN = uint256(keccak256("MIXED_QUOTER_SWAP_V4_BIN")) - 1; + uint256 internal constant SWAP_V4_BIN = 0xea33987d3dc3e2595aa727354eec3d9b92d4061c1331c4a19f9862248f2e1040; + + /// @dev uint256 internal constant SWAP_V4_LIST = uint256(keccak256("MIXED_QUOTER_SWAP_V4_LIST")) - 1; + uint256 internal constant SWAP_V4_LIST = 0xecc1e5328541d701c0936cbe59876b89db17cc11dfd146412e855a9a2e1ecbd3; + + enum SwapDirection { + NONE, + ZeroForOne, + OneForZero + } + + error INVALID_SWAP_DIRECTION(); + + /// @dev Record and check the swap direction of the transaction. + /// @dev Only support one direction for same non-v4 pool in one transaction. + /// @param poolHash The hash of the pool. + /// @param isZeroForOne The direction of the swap. + function setAndCheckSwapDirection(bytes32 poolHash, bool isZeroForOne) internal { + uint256 swapDirection = isZeroForOne ? uint256(SwapDirection.ZeroForOne) : uint256(SwapDirection.OneForZero); + + uint256 currentDirection = getSwapDirection(poolHash); + if (currentDirection == uint256(SwapDirection.NONE)) { + uint256 directionSlot = uint256(keccak256(abi.encode(poolHash, SWAP_DIRECTION))); + assembly ("memory-safe") { + tstore(directionSlot, swapDirection) + } + } else if (currentDirection != swapDirection) { + revert INVALID_SWAP_DIRECTION(); + } + } + + /// @dev Get the swap direction of the transaction. + /// @param poolHash The hash of the pool. + /// @return swapDirection The direction of the swap. + function getSwapDirection(bytes32 poolHash) internal view returns (uint256 swapDirection) { + uint256 directionSlot = uint256(keccak256(abi.encode(poolHash, SWAP_DIRECTION))); + assembly ("memory-safe") { + swapDirection := tload(directionSlot) + } + } + + /// @dev Record the swap token accumulation of the pool. + /// @param poolHash The hash of the pool. + /// @param amountIn The amount of tokenIn. + /// @param amountOut The amount of tokenOut. + /// @param isZeroForOne The direction of the swap. + function setPoolSwapTokenAccumulation(bytes32 poolHash, uint256 amountIn, uint256 amountOut, bool isZeroForOne) + internal + { + uint256 token0Slot = uint256(keccak256(abi.encode(poolHash, SWAP_TOKEN0_ACCUMULATION))); + uint256 token1Slot = uint256(keccak256(abi.encode(poolHash, SWAP_TOKEN1_ACCUMULATION))); + uint256 amount0; + uint256 amount1; + if (isZeroForOne) { + amount0 = amountIn; + amount1 = amountOut; + } else { + amount0 = amountOut; + amount1 = amountIn; + } + assembly ("memory-safe") { + tstore(token0Slot, amount0) + tstore(token1Slot, amount1) + } + } + + // @dev Get the swap token accumulation of the pool. + // @param poolHash The hash of the pool. + // @param isZeroForOne The direction of the swap. + // @return accAmountIn The accumulation amount of tokenIn. + // @return accAmountOut The accumulation amount of tokenOut. + function getPoolSwapTokenAccumulation(bytes32 poolHash, bool isZeroForOne) + internal + view + returns (uint256, uint256) + { + uint256 token0Slot = uint256(keccak256(abi.encode(poolHash, SWAP_TOKEN0_ACCUMULATION))); + uint256 token1Slot = uint256(keccak256(abi.encode(poolHash, SWAP_TOKEN1_ACCUMULATION))); + uint256 amount0; + uint256 amount1; + assembly ("memory-safe") { + amount0 := tload(token0Slot) + amount1 := tload(token1Slot) + } + if (isZeroForOne) { + return (amount0, amount1); + } else { + return (amount1, amount0); + } + } + + /// @dev Record the swap history list of the v4 pool. + /// @param poolHash The hash of the pool. + /// @param swapListBytes The swap history list bytes. + function setV4PoolSwapList(bytes32 poolHash, bytes memory swapListBytes) internal { + uint256 swapListSlot = uint256(keccak256(abi.encode(poolHash, SWAP_V4_LIST))); + assembly ("memory-safe") { + // save the length of the bytes + tstore(swapListSlot, mload(swapListBytes)) + + // save data in next slot + let dataSlot := add(swapListSlot, 1) + for { let i := 0 } lt(i, mload(swapListBytes)) { i := add(i, 32) } { + tstore(add(dataSlot, div(i, 32)), mload(add(swapListBytes, add(0x20, i)))) + } + } + } + + /// @dev Get the swap history list of the v4 pool. + /// @param poolHash The hash of the pool. + /// @return swapListBytes The swap history list bytes. + function getV4PoolSwapList(bytes32 poolHash) internal view returns (bytes memory swapListBytes) { + uint256 swapListSlot = uint256(keccak256(abi.encode(poolHash, SWAP_V4_LIST))); + assembly ("memory-safe") { + // get the length of the bytes + let length := tload(swapListSlot) + swapListBytes := mload(0x40) + mstore(swapListBytes, length) + let dataSlot := add(swapListSlot, 1) + for { let i := 0 } lt(i, length) { i := add(i, 32) } { + mstore(add(swapListBytes, add(0x20, i)), tload(add(dataSlot, div(i, 32)))) + } + mstore(0x40, add(swapListBytes, add(0x20, length))) + } + } + + /// @dev Get the stable swap pool hash. + /// @param token0 The address of token0. + /// @param token1 The address of token1. + /// @return poolHash The hash of the pool. + function getSSPoolHash(address token0, address token1) internal pure returns (bytes32) { + if (token0 == token1) revert(); + (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); + return keccak256(abi.encode(token0, token1, SWAP_SS)); + } + + /// @dev Get the v2 pool hash. + /// @param token0 The address of token0. + /// @param token1 The address of token1. + /// @return poolHash The hash of the pool. + function getV2PoolHash(address token0, address token1) internal pure returns (bytes32) { + if (token0 == token1) revert(); + (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); + return keccak256(abi.encode(token0, token1, SWAP_V2)); + } + + /// @dev Get the v3 pool hash. + /// @param token0 The address of token0. + /// @param token1 The address of token1. + /// @param fee The fee of the pool. + function getV3PoolHash(address token0, address token1, uint24 fee) internal pure returns (bytes32) { + if (token0 == token1) revert(); + (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); + return keccak256(abi.encode(token0, token1, fee, SWAP_V3)); + } + + /// @dev Get the v4 cl pool hash. + /// @param key The pool key. + /// @return poolHash The hash of the pool. + function getV4CLPoolHash(PoolKey memory key) internal pure returns (bytes32) { + return keccak256(abi.encode(key, SWAP_V4_CL)); + } + + /// @dev Get the v4 bin pool hash. + /// @param key The pool key. + /// @return poolHash The hash of the pool. + function getV4BinPoolHash(PoolKey memory key) internal pure returns (bytes32) { + return keccak256(abi.encode(key, SWAP_V4_BIN)); + } +} diff --git a/src/libraries/PathKey.sol b/src/libraries/PathKey.sol index a14c58cf..af4c325a 100644 --- a/src/libraries/PathKey.sol +++ b/src/libraries/PathKey.sol @@ -1,4 +1,5 @@ -//SPDX-License-Identifier: UNLICENSED +//SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; import {Currency} from "pancake-v4-core/src/types/Currency.sol"; diff --git a/src/libraries/Planner.sol b/src/libraries/Planner.sol index 1f0ff849..af63a87a 100644 --- a/src/libraries/Planner.sol +++ b/src/libraries/Planner.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; diff --git a/src/libraries/SafeCast.sol b/src/libraries/SafeCast.sol index 5d6ee82c..eecc468e 100644 --- a/src/libraries/SafeCast.sol +++ b/src/libraries/SafeCast.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; /// @title Safe casting methods diff --git a/src/libraries/SlippageCheck.sol b/src/libraries/SlippageCheck.sol index 10c6a761..8b7cf79c 100644 --- a/src/libraries/SlippageCheck.sol +++ b/src/libraries/SlippageCheck.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; import {BalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol"; diff --git a/src/pool-bin/interfaces/IBinFungibleToken.sol b/src/pool-bin/interfaces/IBinFungibleToken.sol index bd501677..95984029 100644 --- a/src/pool-bin/interfaces/IBinFungibleToken.sol +++ b/src/pool-bin/interfaces/IBinFungibleToken.sol @@ -1,5 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright (C) 2024 PancakeSwap +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface IBinFungibleToken { diff --git a/src/pool-bin/interfaces/IBinMigrator.sol b/src/pool-bin/interfaces/IBinMigrator.sol index ea7f061e..194f7bd1 100644 --- a/src/pool-bin/interfaces/IBinMigrator.sol +++ b/src/pool-bin/interfaces/IBinMigrator.sol @@ -1,5 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright (C) 2024 PancakeSwap +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; diff --git a/src/pool-bin/interfaces/IBinPositionManager.sol b/src/pool-bin/interfaces/IBinPositionManager.sol index 8b330931..a015253b 100644 --- a/src/pool-bin/interfaces/IBinPositionManager.sol +++ b/src/pool-bin/interfaces/IBinPositionManager.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PoolId} from "pancake-v4-core/src/types/PoolId.sol"; diff --git a/src/pool-bin/interfaces/IBinQuoter.sol b/src/pool-bin/interfaces/IBinQuoter.sol index 9830e0dc..302dcb15 100644 --- a/src/pool-bin/interfaces/IBinQuoter.sol +++ b/src/pool-bin/interfaces/IBinQuoter.sol @@ -1,5 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright (C) 2024 PancakeSwap +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {IQuoter} from "../../interfaces/IQuoter.sol"; @@ -22,6 +21,18 @@ interface IBinQuoter is IQuoter { external returns (uint256 amountOut, uint256 gasEstimate); + /// @notice Returns the last swap delta amounts for a given exact input in a list of swap + /// @param params The params for the quote, encoded as `QuoteExactSingleParams[]` + /// poolKey The key for identifying a Bin pool + /// zeroForOne If the swap is from currency0 to currency1 + /// exactAmount The desired input amount + /// hookData arbitrary hookData to pass into the associated hooks + /// @return amountOut The last swap output quote for the exactIn swap + /// @return gasEstimate Estimated gas units used for the swap + function quoteExactInputSingleList(QuoteExactSingleParams[] memory params) + external + returns (uint256 amountOut, uint256 gasEstimate); + /// @notice Returns the delta amounts along the swap path for a given exact input swap /// @param params the params for the quote, encoded as 'QuoteExactParams' /// currencyIn The input currency of the swap diff --git a/src/pool-bin/interfaces/IBinRouterBase.sol b/src/pool-bin/interfaces/IBinRouterBase.sol index e87883dd..7f1e9df2 100644 --- a/src/pool-bin/interfaces/IBinRouterBase.sol +++ b/src/pool-bin/interfaces/IBinRouterBase.sol @@ -1,5 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright (C) 2024 PancakeSwap +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; diff --git a/src/pool-bin/lens/BinQuoter.sol b/src/pool-bin/lens/BinQuoter.sol index a5d5119f..6aca78db 100644 --- a/src/pool-bin/lens/BinQuoter.sol +++ b/src/pool-bin/lens/BinQuoter.sol @@ -42,6 +42,20 @@ contract BinQuoter is BaseV4Quoter, IBinQuoter { } } + /// @inheritdoc IBinQuoter + function quoteExactInputSingleList(QuoteExactSingleParams[] memory params) + external + returns (uint256 amountIn, uint256 gasEstimate) + { + uint256 gasBefore = gasleft(); + try vault.lock(abi.encodeCall(this._quoteExactInputSingleList, (params))) {} + catch (bytes memory reason) { + gasEstimate = gasBefore - gasleft(); + // Extract the quote from QuoteSwap error, or throw if the quote failed + amountIn = reason.parseQuoteAmount(); + } + } + /// @inheritdoc IBinQuoter function quoteExactInput(QuoteExactParams memory params) external @@ -118,6 +132,27 @@ contract BinQuoter is BaseV4Quoter, IBinQuoter { amountOut.revertQuote(); } + /// @dev quote ExactInput swap list on a pool, then revert with the result of last swap + function _quoteExactInputSingleList(QuoteExactSingleParams[] calldata swapParamList) + external + selfOnly + returns (bytes memory) + { + uint256 swapLength = swapParamList.length; + if (swapLength == 0) revert(); + uint256 amountOut; + for (uint256 i = 0; i < swapLength; i++) { + QuoteExactSingleParams memory params = swapParamList[i]; + BalanceDelta swapDelta = + _swap(params.poolKey, params.zeroForOne, -(params.exactAmount.safeInt128()), params.hookData); + if (i == swapLength - 1) { + // the output delta of a swap is positive + amountOut = params.zeroForOne ? uint128(swapDelta.amount1()) : uint128(swapDelta.amount0()); + } + } + amountOut.revertQuote(); + } + /// @dev quote an ExactOutput swap along a path of tokens, then revert with the result function _quoteExactOutput(QuoteExactParams calldata params) external selfOnly returns (bytes memory) { uint256 pathLength = params.path.length; diff --git a/src/pool-bin/libraries/BinCalldataDecoder.sol b/src/pool-bin/libraries/BinCalldataDecoder.sol index 4ee261ad..2b9bc168 100644 --- a/src/pool-bin/libraries/BinCalldataDecoder.sol +++ b/src/pool-bin/libraries/BinCalldataDecoder.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; import {IBinPositionManager} from "../interfaces/IBinPositionManager.sol"; @@ -6,6 +7,9 @@ import {IV4Router} from "../../interfaces/IV4Router.sol"; /// @title Library for abi decoding in bin pool calldata library BinCalldataDecoder { + /// @notice equivalent to SliceOutOfBounds.selector, stored in least-significant bits + uint256 constant SLICE_ERROR_SELECTOR = 0x3b99b53d; + /// todo: see if tweaking to calldataload saves gas /// @dev equivalent to: abi.decode(params, (IBinPositionManager.BinAddLiquidityParams)) function decodeBinAddLiquidityParams(bytes calldata params) @@ -49,6 +53,12 @@ library BinCalldataDecoder { { // BinExactInputParams is a variable length struct so we just have to look up its location assembly ("memory-safe") { + // only safety checks for the minimum length, where path is empty + // 0xa0 = 5 * 0x20 -> 3 elements, path offset, and path length 0 + if lt(params.length, 0xa0) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } swapParams := add(params.offset, calldataload(params.offset)) } } @@ -61,6 +71,12 @@ library BinCalldataDecoder { { // BinExactInputSingleParams is a variable length struct so we just have to look up its location assembly ("memory-safe") { + // only safety checks for the minimum length, where hookData is empty + // 0x160 = 11 * 0x20 -> 9 elements, bytes offset, and bytes length 0 + if lt(params.length, 0x160) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } swapParams := add(params.offset, calldataload(params.offset)) } } @@ -73,6 +89,12 @@ library BinCalldataDecoder { { // BinExactOutputParams is a variable length struct so we just have to look up its location assembly ("memory-safe") { + // only safety checks for the minimum length, where path is empty + // 0xa0 = 5 * 0x20 -> 3 elements, path offset, and path length 0 + if lt(params.length, 0xa0) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } swapParams := add(params.offset, calldataload(params.offset)) } } @@ -85,6 +107,12 @@ library BinCalldataDecoder { { // BinExactOutputSingleParams is a variable length struct so we just have to look up its location assembly ("memory-safe") { + // only safety checks for the minimum length, where hookData is empty + // 0x160 = 9 * 0x20 -> 9 elements, bytes offset, and bytes length 0 + if lt(params.length, 0x160) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } swapParams := add(params.offset, calldataload(params.offset)) } } diff --git a/src/pool-cl/CLPositionManager.sol b/src/pool-cl/CLPositionManager.sol index 91e4fa29..3ad7f650 100644 --- a/src/pool-cl/CLPositionManager.sol +++ b/src/pool-cl/CLPositionManager.sol @@ -366,20 +366,34 @@ contract CLPositionManager is uint256 liquidity = uint256(_getLiquidity(tokenId, poolKey, info.tickLower(), info.tickUpper())); + address owner = ownerOf(tokenId); + // Clear the position info. positionInfo[tokenId] = CLPositionInfoLibrary.EMPTY_POSITION_INFO; // Burn the token. _burn(tokenId); // Can only call modify if there is non zero liquidity. + BalanceDelta feesAccrued; + BalanceDelta liquidityDelta; if (liquidity > 0) { - (BalanceDelta liquidityDelta, BalanceDelta feesAccrued) = - _modifyLiquidity(info, poolKey, -(liquidity.toInt256()), bytes32(tokenId), hookData); + (liquidityDelta, feesAccrued) = clPoolManager.modifyLiquidity( + poolKey, + ICLPoolManager.ModifyLiquidityParams({ + tickLower: info.tickLower(), + tickUpper: info.tickUpper(), + liquidityDelta: -(liquidity.toInt256()), + salt: bytes32(tokenId) + }), + hookData + ); // Slippage checks should be done on the principal liquidityDelta which is the liquidityDelta - feesAccrued (liquidityDelta - feesAccrued).validateMinOut(amount0Min, amount1Min); + + emit ModifyLiquidity(tokenId, -(liquidity.toInt256()), feesAccrued); } - if (info.hasSubscriber()) _unsubscribe(tokenId); + if (info.hasSubscriber()) _removeSubscriberAndNotifyBurn(tokenId, owner, info, liquidity, feesAccrued); } function _settlePair(Currency currency0, Currency currency1) internal { @@ -427,6 +441,7 @@ contract CLPositionManager is if (balance > 0) currency.transfer(to, balance); } + /// @dev if there is a subscriber attached to the position, this function will notify the subscriber function _modifyLiquidity( CLPositionInfo info, PoolKey memory poolKey, diff --git a/src/pool-cl/base/CLNotifier.sol b/src/pool-cl/base/CLNotifier.sol index f53662da..5ed2598b 100644 --- a/src/pool-cl/base/CLNotifier.sol +++ b/src/pool-cl/base/CLNotifier.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; import {ICLSubscriber} from "../interfaces/ICLSubscriber.sol"; @@ -81,6 +82,31 @@ abstract contract CLNotifier is ICLNotifier { emit Unsubscription(tokenId, address(_subscriber)); } + /// @dev note this function also deletes the subscriber address from the mapping + function _removeSubscriberAndNotifyBurn( + uint256 tokenId, + address owner, + CLPositionInfo info, + uint256 liquidity, + BalanceDelta feesAccrued + ) internal { + ICLSubscriber _subscriber = subscriber[tokenId]; + + // remove the subscriber + delete subscriber[tokenId]; + + bool success = _call( + address(_subscriber), + abi.encodeCall(ICLSubscriber.notifyBurn, (tokenId, owner, info, liquidity, feesAccrued)) + ); + + if (!success) { + address(_subscriber).bubbleUpAndRevertWith( + ICLSubscriber.notifyBurn.selector, BurnNotificationReverted.selector + ); + } + } + function _notifyModifyLiquidity(uint256 tokenId, int256 liquidityChange, BalanceDelta feesAccrued) internal { ICLSubscriber _subscriber = subscriber[tokenId]; diff --git a/src/pool-cl/base/ERC721Permit_v4.sol b/src/pool-cl/base/ERC721Permit_v4.sol index 5331c6d6..935620a5 100644 --- a/src/pool-cl/base/ERC721Permit_v4.sol +++ b/src/pool-cl/base/ERC721Permit_v4.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; import {ERC721} from "solmate/src/tokens/ERC721.sol"; diff --git a/src/pool-cl/base/UnorderedNonce.sol b/src/pool-cl/base/UnorderedNonce.sol index 533a4a71..987c9612 100644 --- a/src/pool-cl/base/UnorderedNonce.sol +++ b/src/pool-cl/base/UnorderedNonce.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; /// @title Unordered Nonce diff --git a/src/pool-cl/interfaces/ICLMigrator.sol b/src/pool-cl/interfaces/ICLMigrator.sol index fa6828fa..7c4a36bc 100644 --- a/src/pool-cl/interfaces/ICLMigrator.sol +++ b/src/pool-cl/interfaces/ICLMigrator.sol @@ -1,5 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright (C) 2024 PancakeSwap +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; diff --git a/src/pool-cl/interfaces/ICLNotifier.sol b/src/pool-cl/interfaces/ICLNotifier.sol index 2536e88d..a9af444c 100644 --- a/src/pool-cl/interfaces/ICLNotifier.sol +++ b/src/pool-cl/interfaces/ICLNotifier.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ICLSubscriber} from "./ICLSubscriber.sol"; @@ -16,6 +16,8 @@ interface ICLNotifier { error SubscriptionReverted(address subscriber, bytes reason); /// @notice Wraps the revert message of the subscriber contract on a reverting modify liquidity notification error ModifyLiquidityNotificationReverted(address subscriber, bytes reason); + /// @notice Wraps the revert message of the subscriber contract on a reverting burn notification + error BurnNotificationReverted(address subscriber, bytes reason); /// @notice Wraps the revert message of the subscriber contract on a reverting transfer notification error TransferNotificationReverted(address subscriber, bytes reason); /// @notice Thrown when a tokenId already has a subscriber diff --git a/src/pool-cl/interfaces/ICLPositionDescriptor.sol b/src/pool-cl/interfaces/ICLPositionDescriptor.sol index 8092ac35..e995554c 100644 --- a/src/pool-cl/interfaces/ICLPositionDescriptor.sol +++ b/src/pool-cl/interfaces/ICLPositionDescriptor.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {ICLPositionManager} from "./ICLPositionManager.sol"; diff --git a/src/pool-cl/interfaces/ICLPositionManager.sol b/src/pool-cl/interfaces/ICLPositionManager.sol index 0480f8c7..45ad13f6 100644 --- a/src/pool-cl/interfaces/ICLPositionManager.sol +++ b/src/pool-cl/interfaces/ICLPositionManager.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; diff --git a/src/pool-cl/interfaces/ICLQuoter.sol b/src/pool-cl/interfaces/ICLQuoter.sol index 2e3fa77d..b8951910 100644 --- a/src/pool-cl/interfaces/ICLQuoter.sol +++ b/src/pool-cl/interfaces/ICLQuoter.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {IQuoter} from "../../interfaces/IQuoter.sol"; @@ -21,6 +21,18 @@ interface ICLQuoter is IQuoter { external returns (uint256 amountOut, uint256 gasEstimate); + /// @notice Returns the last swap delta amounts for a given exact input in a list of swap + /// @param params The params for the quote, encoded as `QuoteExactSingleParams[]` + /// poolKey The key for identifying a V4 pool + /// zeroForOne If the swap is from currency0 to currency1 + /// exactAmount The desired input amount + /// hookData arbitrary hookData to pass into the associated hooks + /// @return amountOut The last swap output quote for the exactIn swap + /// @return gasEstimate Estimated gas units used for the swap + function quoteExactInputSingleList(QuoteExactSingleParams[] memory params) + external + returns (uint256 amountOut, uint256 gasEstimate); + /// @notice Returns the delta amounts along the swap path for a given exact input swap /// @param params the params for the quote, encoded as 'QuoteExactParams' /// currencyIn The input currency of the swap diff --git a/src/pool-cl/interfaces/ICLRouterBase.sol b/src/pool-cl/interfaces/ICLRouterBase.sol index dee03641..44615476 100644 --- a/src/pool-cl/interfaces/ICLRouterBase.sol +++ b/src/pool-cl/interfaces/ICLRouterBase.sol @@ -1,5 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright (C) 2024 PancakeSwap +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Currency} from "pancake-v4-core/src/types/Currency.sol"; diff --git a/src/pool-cl/interfaces/ICLSubscriber.sol b/src/pool-cl/interfaces/ICLSubscriber.sol index de1013d2..45962610 100644 --- a/src/pool-cl/interfaces/ICLSubscriber.sol +++ b/src/pool-cl/interfaces/ICLSubscriber.sol @@ -1,7 +1,8 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {BalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol"; +import {CLPositionInfo} from "../libraries/CLPositionInfoLibrary.sol"; /// @notice Interface that a Subscriber contract should implement to receive updates from the v4 cl pool position manager interface ICLSubscriber { @@ -13,6 +14,19 @@ interface ICLSubscriber { /// @dev Because of EIP-150, solidity may only allocate 63/64 of gasleft() /// @param tokenId the token ID of the position function notifyUnsubscribe(uint256 tokenId) external; + /// @notice Called when a position is burned + /// @param tokenId the token ID of the position + /// @param owner the current owner of the tokenId + /// @param info information about the position + /// @param liquidity the amount of liquidity decreased in the position, may be 0 + /// @param feesAccrued the fees accrued by the position if liquidity was decreased + function notifyBurn( + uint256 tokenId, + address owner, + CLPositionInfo info, + uint256 liquidity, + BalanceDelta feesAccrued + ) external; /// @param tokenId the token ID of the position /// @param liquidityChange the change in liquidity on the underlying position /// @param feesAccrued the fees to be collected from the position as a result of the modifyLiquidity call diff --git a/src/pool-cl/interfaces/IERC721Permit.sol b/src/pool-cl/interfaces/IERC721Permit.sol index e4afb8b5..c190b1c1 100644 --- a/src/pool-cl/interfaces/IERC721Permit.sol +++ b/src/pool-cl/interfaces/IERC721Permit.sol @@ -1,5 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright (C) 2024 PancakeSwap +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; diff --git a/src/pool-cl/interfaces/IERC721Permit_v4.sol b/src/pool-cl/interfaces/IERC721Permit_v4.sol index bc4c7aa0..e15e00bd 100644 --- a/src/pool-cl/interfaces/IERC721Permit_v4.sol +++ b/src/pool-cl/interfaces/IERC721Permit_v4.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @title ERC721 with permit diff --git a/src/pool-cl/interfaces/ITickLens.sol b/src/pool-cl/interfaces/ITickLens.sol index 7338d20d..a0a67936 100644 --- a/src/pool-cl/interfaces/ITickLens.sol +++ b/src/pool-cl/interfaces/ITickLens.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; diff --git a/src/pool-cl/lens/CLQuoter.sol b/src/pool-cl/lens/CLQuoter.sol index a947ad6a..58a38a32 100644 --- a/src/pool-cl/lens/CLQuoter.sol +++ b/src/pool-cl/lens/CLQuoter.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity 0.8.26; import {TickMath} from "pancake-v4-core/src/pool-cl/libraries/TickMath.sol"; @@ -38,6 +39,20 @@ contract CLQuoter is ICLQuoter, BaseV4Quoter { } } + /// @inheritdoc ICLQuoter + function quoteExactInputSingleList(QuoteExactSingleParams[] memory params) + external + returns (uint256 amountIn, uint256 gasEstimate) + { + uint256 gasBefore = gasleft(); + try vault.lock(abi.encodeCall(this._quoteExactInputSingleList, (params))) {} + catch (bytes memory reason) { + gasEstimate = gasBefore - gasleft(); + // Extract the quote from QuoteSwap error, or throw if the quote failed + amountIn = reason.parseQuoteAmount(); + } + } + /// @inheritdoc ICLQuoter function quoteExactInput(QuoteExactParams memory params) external @@ -111,6 +126,27 @@ contract CLQuoter is ICLQuoter, BaseV4Quoter { amountOut.revertQuote(); } + /// @dev quote ExactInput swap list on a pool, then revert with the result of last swap + function _quoteExactInputSingleList(QuoteExactSingleParams[] calldata swapParamList) + external + selfOnly + returns (bytes memory) + { + uint256 swapLength = swapParamList.length; + if (swapLength == 0) revert(); + uint256 amountOut; + for (uint256 i = 0; i < swapLength; i++) { + QuoteExactSingleParams memory params = swapParamList[i]; + BalanceDelta swapDelta = + _swap(params.poolKey, params.zeroForOne, -int256(int128(params.exactAmount)), params.hookData); + if (i == swapLength - 1) { + // the output delta of a swap is positive + amountOut = params.zeroForOne ? uint128(swapDelta.amount1()) : uint128(swapDelta.amount0()); + } + } + amountOut.revertQuote(); + } + /// @dev quote an ExactOutput swap along a path of tokens, then revert with the result function _quoteExactOutput(QuoteExactParams calldata params) external selfOnly returns (bytes memory) { uint256 pathLength = params.path.length; diff --git a/src/pool-cl/lens/TickLens.sol b/src/pool-cl/lens/TickLens.sol index 87e9c38a..2bb030fc 100644 --- a/src/pool-cl/lens/TickLens.sol +++ b/src/pool-cl/lens/TickLens.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity 0.8.26; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; diff --git a/src/pool-cl/libraries/CLCalldataDecoder.sol b/src/pool-cl/libraries/CLCalldataDecoder.sol index cd412553..131f6ede 100644 --- a/src/pool-cl/libraries/CLCalldataDecoder.sol +++ b/src/pool-cl/libraries/CLCalldataDecoder.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; import {IV4Router} from "../../interfaces/IV4Router.sol"; @@ -9,6 +10,9 @@ import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; library CLCalldataDecoder { using CalldataDecoder for bytes; + /// @notice equivalent to SliceOutOfBounds.selector, stored in least-significant bits + uint256 constant SLICE_ERROR_SELECTOR = 0x3b99b53d; + /// @dev equivalent to: abi.decode(params, (IV4Router.CLExactInputParams)) function decodeCLSwapExactInParams(bytes calldata params) internal @@ -17,6 +21,12 @@ library CLCalldataDecoder { { // CLExactInputParams is a variable length struct so we just have to look up its location assembly ("memory-safe") { + // only safety checks for the minimum length, where path is empty + // 0xa0 = 5 * 0x20 -> 3 elements, path offset, and path length 0 + if lt(params.length, 0xa0) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } swapParams := add(params.offset, calldataload(params.offset)) } } @@ -29,6 +39,12 @@ library CLCalldataDecoder { { // CLExactInputSingleParams is a variable length struct so we just have to look up its location assembly ("memory-safe") { + // only safety checks for the minimum length, where hookData is empty + // 0x160 = 11 * 0x20 -> 9 elements, bytes offset, and bytes length 0 + if lt(params.length, 0x160) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } swapParams := add(params.offset, calldataload(params.offset)) } } @@ -41,6 +57,12 @@ library CLCalldataDecoder { { // CLExactOutputParams is a variable length struct so we just have to look up its location assembly ("memory-safe") { + // only safety checks for the minimum length, where path is empty + // 0xa0 = 5 * 0x20 -> 3 elements, path offset, and path length 0 + if lt(params.length, 0xa0) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } swapParams := add(params.offset, calldataload(params.offset)) } } @@ -53,6 +75,12 @@ library CLCalldataDecoder { { // CLExactOutputSingleParams is a variable length struct so we just have to look up its location assembly ("memory-safe") { + // only safety checks for the minimum length, where hookData is empty + // 0x160 = 9 * 0x20 -> 9 elements, bytes offset, and bytes length 0 + if lt(params.length, 0x160) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } swapParams := add(params.offset, calldataload(params.offset)) } } @@ -64,6 +92,10 @@ library CLCalldataDecoder { returns (uint256 tokenId, uint256 liquidity, uint128 amount0, uint128 amount1, bytes calldata hookData) { assembly ("memory-safe") { + if lt(params.length, 0x80) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } tokenId := calldataload(params.offset) liquidity := calldataload(add(params.offset, 0x20)) amount0 := calldataload(add(params.offset, 0x40)) diff --git a/src/pool-cl/libraries/CLPositionInfoLibrary.sol b/src/pool-cl/libraries/CLPositionInfoLibrary.sol index 6d1eeb64..0d86b936 100644 --- a/src/pool-cl/libraries/CLPositionInfoLibrary.sol +++ b/src/pool-cl/libraries/CLPositionInfoLibrary.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.24; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; diff --git a/src/pool-cl/libraries/ERC721PermitHash.sol b/src/pool-cl/libraries/ERC721PermitHash.sol index a8c9cfc8..6da35d9a 100644 --- a/src/pool-cl/libraries/ERC721PermitHash.sol +++ b/src/pool-cl/libraries/ERC721PermitHash.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.0; library ERC721PermitHash { diff --git a/src/pool-cl/libraries/PoolTicksCounter.sol b/src/pool-cl/libraries/PoolTicksCounter.sol index 5d6e4713..a85a5067 100644 --- a/src/pool-cl/libraries/PoolTicksCounter.sol +++ b/src/pool-cl/libraries/PoolTicksCounter.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.24; import {CLPoolParametersHelper} from "pancake-v4-core/src/pool-cl/libraries/CLPoolParametersHelper.sol"; diff --git a/test/MixedQuoter.t.sol b/test/MixedQuoter.t.sol index 330c7721..bed7ab19 100644 --- a/test/MixedQuoter.t.sol +++ b/test/MixedQuoter.t.sol @@ -47,6 +47,13 @@ import {DeployStableSwapHelper} from "./helpers/DeployStableSwapHelper.sol"; import {IStableSwapFactory} from "../src/interfaces/external/IStableSwapFactory.sol"; import {IStableSwap} from "../src/interfaces/external/IStableSwap.sol"; import {IWETH9} from "../src/interfaces/external/IWETH9.sol"; +import {MockV4Router} from "./mocks/MockV4Router.sol"; +import {ICLRouterBase} from "../src/pool-cl/interfaces/ICLRouterBase.sol"; +import {IBinRouterBase} from "../src/pool-bin/interfaces/IBinRouterBase.sol"; +import {ActionConstants} from "../src/libraries/ActionConstants.sol"; +import {V3SmartRouterHelper} from "../src/libraries/external/V3SmartRouterHelper.sol"; +import {MixedQuoterRecorder} from "../src/libraries/MixedQuoterRecorder.sol"; +import {PancakeV3Router} from "./helpers/PancakeV3Router.sol"; contract MixedQuoterTest is Test, @@ -73,6 +80,8 @@ contract MixedQuoterTest is MockERC20 token4; MockERC20 token5; + MockV4Router v4Router; + IVault vault; ICLPoolManager clPoolManager; @@ -83,6 +92,7 @@ contract MixedQuoterTest is address v3Deployer; IPancakeV3Factory v3Factory; IV3NonfungiblePositionManager v3Nfpm; + PancakeV3Router v3Router; IStableSwapFactory stableSwapFactory; IStableSwap stableSwapPair; @@ -102,7 +112,11 @@ contract MixedQuoterTest is PoolKey binPoolKey; + Plan plan; + function setUp() public { + plan = Planner.init(); + weth = _WETH9; token2 = new MockERC20("Token0", "TKN2", 18); token3 = new MockERC20("Token1", "TKN3", 18); @@ -116,6 +130,14 @@ contract MixedQuoterTest is binPoolManager = new BinPoolManager(vault); vault.registerApp(address(binPoolManager)); + v4Router = new MockV4Router(vault, clPoolManager, binPoolManager); + MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(v4Router), type(uint256).max); + MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(v4Router), type(uint256).max); + token2.approve(address(v4Router), type(uint256).max); + token3.approve(address(v4Router), type(uint256).max); + token4.approve(address(v4Router), type(uint256).max); + token5.approve(address(v4Router), type(uint256).max); + currency0 = poolKey.currency0; currency1 = poolKey.currency1; token0 = MockERC20(Currency.unwrap(currency0)); @@ -191,10 +213,16 @@ contract MixedQuoterTest is ); } + v3Router = new PancakeV3Router(v3Factory); + // make sure v3Nfpm has allowance weth.approve(address(v3Nfpm), type(uint256).max); token2.approve(address(v3Nfpm), type(uint256).max); token3.approve(address(v3Nfpm), type(uint256).max); + // approve v3Router + weth.approve(address(v3Router), type(uint256).max); + token2.approve(address(v3Router), type(uint256).max); + token3.approve(address(v3Router), type(uint256).max); // 1. mint some liquidity to the v3 pool _mintV3Liquidity(address(weth), address(token2)); @@ -269,6 +297,151 @@ contract MixedQuoterTest is assertLt(gasEstimate, 50000); } + function test_quoteMixedExactInputSharedContext_SS2_revert_INVALID_SWAP_DIRECTION() public { + address[] memory paths = new address[](2); + paths[0] = address(token1); + paths[1] = address(token2); + + address[] memory paths2 = new address[](2); + paths2[0] = address(token2); + paths2[1] = address(token1); + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.SS_2_EXACT_INPUT_SINGLE)); + + bytes[] memory params = new bytes[](1); + + // path 1: (0.5)token1 -> token2 + // path 2: (0.5)token1 -> token2 + bytes[] memory multicallBytes = new bytes[](2); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.5 ether + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths2, actions, params, 0.5 ether + ); + vm.expectRevert(MixedQuoterRecorder.INVALID_SWAP_DIRECTION.selector); + mixedQuoter.multicall(multicallBytes); + } + + function test_quoteMixedExactInputSharedContext_SS2() public { + address[] memory paths = new address[](2); + paths[0] = address(token1); + paths[1] = address(token2); + bool isZeroForOne = token1 < token2; + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.SS_2_EXACT_INPUT_SINGLE)); + + bytes[] memory params = new bytes[](1); + + // swap 0.5 ether + (uint256 amountOut, uint256 gasEstimate) = mixedQuoter.quoteMixedExactInput(paths, actions, params, 0.5 ether); + uint256 swapPath1Output = amountOut; + + // swap 1 ether + (amountOut, gasEstimate) = mixedQuoter.quoteMixedExactInput(paths, actions, params, 1 ether); + uint256 swapPath2Output = amountOut - swapPath1Output; + + // path 1: (0.5)token1 -> token2 + // path 2: (0.5)token1 -> token2 + bytes[] memory multicallBytes = new bytes[](2); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.5 ether + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.5 ether + ); + bytes[] memory results = mixedQuoter.multicall(multicallBytes); + + (uint256 amountOutOfRoute1,) = abi.decode(results[0], (uint256, uint256)); + (uint256 amountOutOfRoute2,) = abi.decode(results[1], (uint256, uint256)); + assertEq(amountOutOfRoute1, swapPath1Output); + assertEq(amountOutOfRoute2, swapPath2Output); + + // swap 0.5 ether in stable swap + uint256 route1TokenOutBalanceBefore = token2.balanceOf(address(this)); + stableSwapPair.exchange(isZeroForOne ? 0 : 1, isZeroForOne ? 1 : 0, 0.5 ether, 0); + uint256 route1TokenOutBalanceAfter = token2.balanceOf(address(this)); + assertEq(route1TokenOutBalanceAfter - route1TokenOutBalanceBefore, amountOutOfRoute1); + + // swap 0.5 ether in stable swap + uint256 route2TokenOutBalanceBefore = token2.balanceOf(address(this)); + stableSwapPair.exchange(isZeroForOne ? 0 : 1, isZeroForOne ? 1 : 0, 0.5 ether, 0); + uint256 route2TokenOutBalanceAfter = token2.balanceOf(address(this)); + // not exactly equal , but difference is very small, less than 1/1000000 + assertApproxEqRel(route2TokenOutBalanceAfter - route2TokenOutBalanceBefore, amountOutOfRoute2, 1e18 / 1000000); + } + + function testFuzz_quoteMixedExactInputSharedContext_SS2(uint8 firstSwapPercent, bool isZeroForOne) public { + uint256 OneHundredPercent = type(uint8).max; + vm.assume(firstSwapPercent > 0 && firstSwapPercent < OneHundredPercent); + uint256 totalSwapAmount = 1 ether; + uint128 firstSwapAmount = uint128((totalSwapAmount * firstSwapPercent) / OneHundredPercent); + uint128 secondSwapAmount = uint128(totalSwapAmount - firstSwapAmount); + (MockERC20 token0OfSS, MockERC20 token1OfSS) = + address(token1) < address(token2) ? (token1, token2) : (token2, token1); + + address[] memory paths = new address[](2); + if (isZeroForOne) { + paths[0] = address(token0OfSS); + paths[1] = address(token1OfSS); + } else { + paths[0] = address(token1OfSS); + paths[1] = address(token0OfSS); + } + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.SS_2_EXACT_INPUT_SINGLE)); + + bytes[] memory params = new bytes[](1); + + bytes[] memory multicallBytes = new bytes[](2); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, firstSwapAmount + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, secondSwapAmount + ); + bytes[] memory results = mixedQuoter.multicall(multicallBytes); + + (uint256 amountOutOfRoute1,) = abi.decode(results[0], (uint256, uint256)); + (uint256 amountOutOfRoute2,) = abi.decode(results[1], (uint256, uint256)); + + // first swap in stable swap + uint256 route1TokenOutBalanceBefore; + if (isZeroForOne) { + route1TokenOutBalanceBefore = token1OfSS.balanceOf(address(this)); + } else { + route1TokenOutBalanceBefore = token0OfSS.balanceOf(address(this)); + } + stableSwapPair.exchange(isZeroForOne ? 0 : 1, isZeroForOne ? 1 : 0, firstSwapAmount, 0); + uint256 route1TokenOutBalanceAfter; + if (isZeroForOne) { + route1TokenOutBalanceAfter = token1OfSS.balanceOf(address(this)); + } else { + route1TokenOutBalanceAfter = token0OfSS.balanceOf(address(this)); + } + assertEq(route1TokenOutBalanceAfter - route1TokenOutBalanceBefore, amountOutOfRoute1); + + // second swap in stable swap + uint256 route2TokenOutBalanceBefore; + if (isZeroForOne) { + route2TokenOutBalanceBefore = token1OfSS.balanceOf(address(this)); + } else { + route2TokenOutBalanceBefore = token0OfSS.balanceOf(address(this)); + } + stableSwapPair.exchange(isZeroForOne ? 0 : 1, isZeroForOne ? 1 : 0, secondSwapAmount, 0); + uint256 route2TokenOutBalanceAfter; + if (isZeroForOne) { + route2TokenOutBalanceAfter = token1OfSS.balanceOf(address(this)); + } else { + route2TokenOutBalanceAfter = token0OfSS.balanceOf(address(this)); + } + // not exactly equal , but difference is very small, less than 1/1000000 + assertApproxEqRel(route2TokenOutBalanceAfter - route2TokenOutBalanceBefore, amountOutOfRoute2, 1e18 / 1000000); + } + function testQuoteExactInputSingleV2() public { address[] memory paths = new address[](2); paths[0] = address(weth); @@ -285,6 +458,156 @@ contract MixedQuoterTest is assertLt(gasEstimate, 20000); } + function test_quoteMixedExactInputSharedContext_V2_revert_INVALID_SWAP_DIRECTION() public { + address[] memory paths = new address[](2); + paths[0] = address(weth); + paths[1] = address(token2); + + address[] memory paths2 = new address[](2); + paths2[0] = address(token2); + paths2[1] = address(weth); + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.V2_EXACT_INPUT_SINGLE)); + bytes[] memory params = new bytes[](1); + + // path 1: (0.5)weth -> token2 + // path 2: (0.5)weth -> token2 + bytes[] memory multicallBytes = new bytes[](2); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.5 ether + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths2, actions, params, 0.5 ether + ); + vm.expectRevert(MixedQuoterRecorder.INVALID_SWAP_DIRECTION.selector); + mixedQuoter.multicall(multicallBytes); + } + + function test_quoteMixedExactInputSharedContext_V2() public { + address[] memory paths = new address[](2); + paths[0] = address(weth); + paths[1] = address(token2); + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.V2_EXACT_INPUT_SINGLE)); + bytes[] memory params = new bytes[](1); + + // path 1: (0.3)weth -> token2 + // path 2: (0.4)weth -> token2 + // path 3: (0.5)weth -> token2 + + bytes[] memory multicallBytes = new bytes[](3); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.3 ether + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.4 ether + ); + multicallBytes[2] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.5 ether + ); + bytes[] memory results = mixedQuoter.multicall(multicallBytes); + + (uint256 amountOutOfRoute1,) = abi.decode(results[0], (uint256, uint256)); + (uint256 amountOutOfRoute2,) = abi.decode(results[1], (uint256, uint256)); + (uint256 amountOutOfRoute3,) = abi.decode(results[2], (uint256, uint256)); + + // swap 0.3 ether in v2 pair + uint256 route1TokenOutBalanceBefore = token2.balanceOf(address(this)); + _swapV2(address(weth), address(token2), 0.3 ether); + uint256 route1TokenOutBalanceAfter = token2.balanceOf(address(this)); + assertEq(route1TokenOutBalanceAfter - route1TokenOutBalanceBefore, amountOutOfRoute1); + + // swap 0.4 ether in v2 pair + uint256 route2TokenOutBalanceBefore = token2.balanceOf(address(this)); + _swapV2(address(weth), address(token2), 0.4 ether); + uint256 route2TokenOutBalanceAfter = token2.balanceOf(address(this)); + assertEq(route2TokenOutBalanceAfter - route2TokenOutBalanceBefore, amountOutOfRoute2); + + // swap 0.5 ether in v2 pair + uint256 route3TokenOutBalanceBefore = token2.balanceOf(address(this)); + _swapV2(address(weth), address(token2), 0.5 ether); + uint256 route3TokenOutBalanceAfter = token2.balanceOf(address(this)); + assertEq(route3TokenOutBalanceAfter - route3TokenOutBalanceBefore, amountOutOfRoute3); + } + + function testFuzz_quoteMixedExactInputSharedContext_V2(uint8 firstSwapPercent, bool isZeroForOne) public { + uint256 OneHundredPercent = type(uint8).max; + vm.assume(firstSwapPercent > 0 && firstSwapPercent < OneHundredPercent); + uint256 totalSwapAmount = 1 ether; + uint128 firstSwapAmount = uint128((totalSwapAmount * firstSwapPercent) / OneHundredPercent); + uint128 secondSwapAmount = uint128(totalSwapAmount - firstSwapAmount); + (MockERC20 token0OfV2, MockERC20 token1OfV2) = + address(weth) < address(token2) ? (MockERC20(address(weth)), token2) : (token2, MockERC20(address(weth))); + + address[] memory paths = new address[](2); + if (isZeroForOne) { + paths[0] = address(token0OfV2); + paths[1] = address(token1OfV2); + } else { + paths[0] = address(token1OfV2); + paths[1] = address(token0OfV2); + } + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.V2_EXACT_INPUT_SINGLE)); + bytes[] memory params = new bytes[](1); + + bytes[] memory multicallBytes = new bytes[](2); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, firstSwapAmount + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, secondSwapAmount + ); + bytes[] memory results = mixedQuoter.multicall(multicallBytes); + + (uint256 amountOutOfRoute1,) = abi.decode(results[0], (uint256, uint256)); + (uint256 amountOutOfRoute2,) = abi.decode(results[1], (uint256, uint256)); + + // first swap in v2 pair + uint256 route1TokenOutBalanceBefore; + if (isZeroForOne) { + route1TokenOutBalanceBefore = token1OfV2.balanceOf(address(this)); + } else { + route1TokenOutBalanceBefore = token0OfV2.balanceOf(address(this)); + } + if (isZeroForOne) { + _swapV2(address(token0OfV2), address(token1OfV2), firstSwapAmount); + } else { + _swapV2(address(token1OfV2), address(token0OfV2), firstSwapAmount); + } + uint256 route1TokenOutBalanceAfter; + if (isZeroForOne) { + route1TokenOutBalanceAfter = token1OfV2.balanceOf(address(this)); + } else { + route1TokenOutBalanceAfter = token0OfV2.balanceOf(address(this)); + } + + assertEq(route1TokenOutBalanceAfter - route1TokenOutBalanceBefore, amountOutOfRoute1); + + // second swap in v2 pair + uint256 route2TokenOutBalanceBefore; + if (isZeroForOne) { + route2TokenOutBalanceBefore = token1OfV2.balanceOf(address(this)); + } else { + route2TokenOutBalanceBefore = token0OfV2.balanceOf(address(this)); + } + if (isZeroForOne) { + _swapV2(address(token0OfV2), address(token1OfV2), secondSwapAmount); + } else { + _swapV2(address(token1OfV2), address(token0OfV2), secondSwapAmount); + } + uint256 route2TokenOutBalanceAfter; + if (isZeroForOne) { + route2TokenOutBalanceAfter = token1OfV2.balanceOf(address(this)); + } else { + route2TokenOutBalanceAfter = token0OfV2.balanceOf(address(this)); + } + assertEq(route2TokenOutBalanceAfter - route2TokenOutBalanceBefore, amountOutOfRoute2); + } + function testQuoteExactInputSingleV3() public { address[] memory paths = new address[](2); paths[0] = address(weth); @@ -304,6 +627,200 @@ contract MixedQuoterTest is assertLt(gasEstimate, 130000); } + function test_quoteMixedExactInputSharedContext_V3_revert_INVALID_SWAP_DIRECTION() public { + address[] memory paths = new address[](2); + paths[0] = address(weth); + paths[1] = address(token2); + + address[] memory paths2 = new address[](2); + paths2[0] = address(token2); + paths2[1] = address(weth); + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.V3_EXACT_INPUT_SINGLE)); + + bytes[] memory params = new bytes[](1); + uint24 fee = 500; + params[0] = abi.encode(fee); + + // path 1: (0.5)weth -> token2 + // path 2: (0.5)weth -> token2 + bytes[] memory multicallBytes = new bytes[](2); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.5 ether + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths2, actions, params, 0.5 ether + ); + vm.expectRevert(MixedQuoterRecorder.INVALID_SWAP_DIRECTION.selector); + mixedQuoter.multicall(multicallBytes); + } + + function test_quoteMixedExactInputSharedContext_V3() public { + address[] memory paths = new address[](2); + paths[0] = address(weth); + paths[1] = address(token2); + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.V3_EXACT_INPUT_SINGLE)); + + bytes[] memory params = new bytes[](1); + uint24 fee = 500; + params[0] = abi.encode(fee); + + // swap 0.3 ether + (uint256 amountOut,) = mixedQuoter.quoteMixedExactInput(paths, actions, params, 0.3 ether); + uint256 swapPath1Output = amountOut; + + // swap 1 ether + (amountOut,) = mixedQuoter.quoteMixedExactInput(paths, actions, params, 1 ether); + uint256 swapPath2Output = amountOut - swapPath1Output; + + // path 1: (0.3)weth -> token2 + // path 2: (0.7)weth -> token2 + + bytes[] memory multicallBytes = new bytes[](2); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.3 ether + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.7 ether + ); + + bytes[] memory results = mixedQuoter.multicall(multicallBytes); + + (uint256 amountOutOfRoute1,) = abi.decode(results[0], (uint256, uint256)); + (uint256 amountOutOfRoute2,) = abi.decode(results[1], (uint256, uint256)); + assertEq(amountOutOfRoute1, swapPath1Output); + assertEq(amountOutOfRoute2, swapPath2Output); + + // swap 0.3 ether in v3 pool + PancakeV3Router.ExactInputSingleParams memory swapParams1 = PancakeV3Router.ExactInputSingleParams({ + tokenIn: address(weth), + tokenOut: address(token2), + fee: fee, + recipient: address(this), + deadline: block.timestamp + 1, + amountIn: 0.3 ether, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }); + uint256 route1TokenOutBalanceBefore = token2.balanceOf(address(this)); + v3Router.exactInputSingle(swapParams1); + uint256 route1TokenOutBalanceAfter = token2.balanceOf(address(this)); + + assertEq(route1TokenOutBalanceAfter - route1TokenOutBalanceBefore, amountOutOfRoute1); + + //swap 0.7 ether in v3 pool + PancakeV3Router.ExactInputSingleParams memory swapParams2 = PancakeV3Router.ExactInputSingleParams({ + tokenIn: address(weth), + tokenOut: address(token2), + fee: fee, + recipient: address(this), + deadline: block.timestamp + 1, + amountIn: 0.7 ether, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }); + uint256 route2TokenOutBalanceBefore = token2.balanceOf(address(this)); + v3Router.exactInputSingle(swapParams2); + uint256 route2TokenOutBalanceAfter = token2.balanceOf(address(this)); + assertEq(route2TokenOutBalanceAfter - route2TokenOutBalanceBefore, amountOutOfRoute2 - 1); + } + + function testFuzz_quoteMixedExactInputSharedContext_V3(uint8 firstSwapPercent, bool isZeroForOne) public { + uint256 OneHundredPercent = type(uint8).max; + vm.assume(firstSwapPercent > 0 && firstSwapPercent < OneHundredPercent); + uint256 totalSwapAmount = 1 ether; + uint128 firstSwapAmount = uint128((totalSwapAmount * firstSwapPercent) / OneHundredPercent); + uint128 secondSwapAmount = uint128(totalSwapAmount - firstSwapAmount); + (MockERC20 token0OfV3, MockERC20 token1OfV3) = + address(weth) < address(token2) ? (MockERC20(address(weth)), token2) : (token2, MockERC20(address(weth))); + + address[] memory paths = new address[](2); + if (isZeroForOne) { + paths[0] = address(token0OfV3); + paths[1] = address(token1OfV3); + } else { + paths[0] = address(token1OfV3); + paths[1] = address(token0OfV3); + } + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.V3_EXACT_INPUT_SINGLE)); + + bytes[] memory params = new bytes[](1); + uint24 fee = 500; + params[0] = abi.encode(fee); + + bytes[] memory multicallBytes = new bytes[](2); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, firstSwapAmount + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, secondSwapAmount + ); + bytes[] memory results = mixedQuoter.multicall(multicallBytes); + + (uint256 amountOutOfRoute1,) = abi.decode(results[0], (uint256, uint256)); + (uint256 amountOutOfRoute2,) = abi.decode(results[1], (uint256, uint256)); + + uint256 route1TokenOutBalanceBefore; + if (isZeroForOne) { + route1TokenOutBalanceBefore = token1OfV3.balanceOf(address(this)); + } else { + route1TokenOutBalanceBefore = token0OfV3.balanceOf(address(this)); + } + PancakeV3Router.ExactInputSingleParams memory swapParams1 = PancakeV3Router.ExactInputSingleParams({ + tokenIn: isZeroForOne ? address(token0OfV3) : address(token1OfV3), + tokenOut: isZeroForOne ? address(token1OfV3) : address(token0OfV3), + fee: fee, + recipient: address(this), + deadline: block.timestamp + 1, + amountIn: firstSwapAmount, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }); + v3Router.exactInputSingle(swapParams1); + uint256 route1TokenOutBalanceAfter; + if (isZeroForOne) { + route1TokenOutBalanceAfter = token1OfV3.balanceOf(address(this)); + } else { + route1TokenOutBalanceAfter = token0OfV3.balanceOf(address(this)); + } + assertEq(route1TokenOutBalanceAfter - route1TokenOutBalanceBefore, amountOutOfRoute1); + + uint256 route2TokenOutBalanceBefore; + if (isZeroForOne) { + route2TokenOutBalanceBefore = token1OfV3.balanceOf(address(this)); + } else { + route2TokenOutBalanceBefore = token0OfV3.balanceOf(address(this)); + } + PancakeV3Router.ExactInputSingleParams memory swapParams2 = PancakeV3Router.ExactInputSingleParams({ + tokenIn: isZeroForOne ? address(token0OfV3) : address(token1OfV3), + tokenOut: isZeroForOne ? address(token1OfV3) : address(token0OfV3), + fee: fee, + recipient: address(this), + deadline: block.timestamp + 1, + amountIn: secondSwapAmount, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }); + v3Router.exactInputSingle(swapParams2); + uint256 route2TokenOutBalanceAfter; + if (isZeroForOne) { + route2TokenOutBalanceAfter = token1OfV3.balanceOf(address(this)); + } else { + route2TokenOutBalanceAfter = token0OfV3.balanceOf(address(this)); + } + uint256 tokenOutReceived = route2TokenOutBalanceAfter - route2TokenOutBalanceBefore; + uint256 diff = tokenOutReceived > amountOutOfRoute2 + ? tokenOutReceived - amountOutOfRoute2 + : amountOutOfRoute2 - tokenOutReceived; + // not exactly equal in some cases , but difference is very small, only 1 wei or 2 wei + assertLe(diff, 2); + } + function testV4CLquoteExactInputSingle_ZeroForOne() public { address[] memory paths = new address[](2); paths[0] = address(Currency.unwrap(poolKey.currency0)); @@ -334,6 +851,166 @@ contract MixedQuoterTest is assertLt(_gasEstimate, 90000); } + function test_quoteMixedExactInputSharedContext_V4CL() public { + address[] memory paths = new address[](2); + paths[0] = address(token0); + paths[1] = address(token1); + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.V4_CL_EXACT_INPUT_SINGLE)); + + bytes[] memory params = new bytes[](1); + params[0] = + abi.encode(IMixedQuoter.QuoteMixedV4ExactInputSingleParams({poolKey: poolKey, hookData: ZERO_BYTES})); + // swap 0.5 ether + (uint256 amountOut, uint256 gasEstimate) = mixedQuoter.quoteMixedExactInput(paths, actions, params, 0.5 ether); + assertEq(amountOut, 498417179678643398); + uint256 swapPath1Output = amountOut; + + // swap 1 ether + (amountOut, gasEstimate) = mixedQuoter.quoteMixedExactInput(paths, actions, params, 1 ether); + + assertEq(amountOut, 996668773744192346); + uint256 swapPath2Output = amountOut - swapPath1Output; + + // path 1: (0.5)token0 -> token1 , tokenOut should be 498417179678643398 + // path 2: (0.5)token0 -> token1, tokenOut should be 996668773744192346 - 498417179678643398 = 498251594065548948 + + bytes[] memory multicallBytes = new bytes[](2); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.5 ether + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.5 ether + ); + bytes[] memory results = mixedQuoter.multicall(multicallBytes); + + (uint256 amountOutOfRoute1,) = abi.decode(results[0], (uint256, uint256)); + (uint256 amountOutOfRoute2,) = abi.decode(results[1], (uint256, uint256)); + assertEq(amountOutOfRoute1, swapPath1Output); + // -1 is due to precision round loss + assertEq(amountOutOfRoute2, swapPath2Output - 1); + + // swap 0.5 ether in v4 pool + ICLRouterBase.CLSwapExactInputSingleParams memory swapParams1 = + ICLRouterBase.CLSwapExactInputSingleParams(poolKey, true, 0.5 ether, 0, ZERO_BYTES); + + plan = plan.add(Actions.CL_SWAP_EXACT_IN_SINGLE, abi.encode(swapParams1)); + bytes memory swapData1 = plan.finalizeSwap(poolKey.currency0, poolKey.currency1, ActionConstants.MSG_SENDER); + uint256 route1Token1BalanceBefore = poolKey.currency1.balanceOf(address(this)); + v4Router.executeActions(swapData1); + uint256 route1Token1BalanceAfter = poolKey.currency1.balanceOf(address(this)); + + uint256 route1Token1Received = route1Token1BalanceAfter - route1Token1BalanceBefore; + assertEq(route1Token1Received, swapPath1Output); + + // swap another 0.5 ether in v4 pool + ICLRouterBase.CLSwapExactInputSingleParams memory swapParams2 = + ICLRouterBase.CLSwapExactInputSingleParams(poolKey, true, 0.5 ether, 0, ZERO_BYTES); + plan = Planner.init(); + plan = plan.add(Actions.CL_SWAP_EXACT_IN_SINGLE, abi.encode(swapParams2)); + bytes memory swapData2 = plan.finalizeSwap(poolKey.currency0, poolKey.currency1, ActionConstants.MSG_SENDER); + uint256 route2Token1BalanceBefore = poolKey.currency1.balanceOf(address(this)); + v4Router.executeActions(swapData2); + uint256 route2Token1BalanceAfter = poolKey.currency1.balanceOf(address(this)); + + uint256 route2Token1Received = route2Token1BalanceAfter - route2Token1BalanceBefore; + // -1 is due to precision round loss + assertEq(route2Token1Received, swapPath2Output - 1); + } + + function testFuzz_quoteMixedExactInputSharedContext_V4CL(uint8 firstSwapPercent, bool isZeroForOne) public { + uint256 OneHundredPercent = type(uint8).max; + vm.assume(firstSwapPercent > 0 && firstSwapPercent < OneHundredPercent); + uint256 totalSwapAmount = 1 ether; + uint128 firstSwapAmount = uint128((totalSwapAmount * firstSwapPercent) / OneHundredPercent); + uint128 secondSwapAmount = uint128(totalSwapAmount - firstSwapAmount); + + address[] memory paths = new address[](2); + if (isZeroForOne) { + paths[0] = address(token0); + paths[1] = address(token1); + } else { + paths[0] = address(token1); + paths[1] = address(token0); + } + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.V4_CL_EXACT_INPUT_SINGLE)); + + bytes[] memory params = new bytes[](1); + params[0] = + abi.encode(IMixedQuoter.QuoteMixedV4ExactInputSingleParams({poolKey: poolKey, hookData: ZERO_BYTES})); + + bytes[] memory multicallBytes = new bytes[](2); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, firstSwapAmount + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, secondSwapAmount + ); + bytes[] memory results = mixedQuoter.multicall(multicallBytes); + + (uint256 amountOutOfRoute1,) = abi.decode(results[0], (uint256, uint256)); + (uint256 amountOutOfRoute2,) = abi.decode(results[1], (uint256, uint256)); + + // first swap in v4 pool + ICLRouterBase.CLSwapExactInputSingleParams memory swapParams1 = + ICLRouterBase.CLSwapExactInputSingleParams(poolKey, isZeroForOne, firstSwapAmount, 0, ZERO_BYTES); + + plan = plan.add(Actions.CL_SWAP_EXACT_IN_SINGLE, abi.encode(swapParams1)); + bytes memory swapData1; + if (isZeroForOne) { + swapData1 = plan.finalizeSwap(poolKey.currency0, poolKey.currency1, ActionConstants.MSG_SENDER); + } else { + swapData1 = plan.finalizeSwap(poolKey.currency1, poolKey.currency0, ActionConstants.MSG_SENDER); + } + uint256 route1TokenOutBalanceBefore; + if (isZeroForOne) { + route1TokenOutBalanceBefore = poolKey.currency1.balanceOf(address(this)); + } else { + route1TokenOutBalanceBefore = poolKey.currency0.balanceOf(address(this)); + } + v4Router.executeActions(swapData1); + uint256 route1TokenOutBalanceAfter; + if (isZeroForOne) { + route1TokenOutBalanceAfter = poolKey.currency1.balanceOf(address(this)); + } else { + route1TokenOutBalanceAfter = poolKey.currency0.balanceOf(address(this)); + } + + uint256 route1TokenOutReceived = route1TokenOutBalanceAfter - route1TokenOutBalanceBefore; + assertEq(route1TokenOutReceived, amountOutOfRoute1); + + // second swap in v4 pool + ICLRouterBase.CLSwapExactInputSingleParams memory swapParams2 = + ICLRouterBase.CLSwapExactInputSingleParams(poolKey, isZeroForOne, secondSwapAmount, 0, ZERO_BYTES); + plan = Planner.init(); + plan = plan.add(Actions.CL_SWAP_EXACT_IN_SINGLE, abi.encode(swapParams2)); + bytes memory swapData2; + if (isZeroForOne) { + swapData2 = plan.finalizeSwap(poolKey.currency0, poolKey.currency1, ActionConstants.MSG_SENDER); + } else { + swapData2 = plan.finalizeSwap(poolKey.currency1, poolKey.currency0, ActionConstants.MSG_SENDER); + } + uint256 route2TokenOutBalanceBefore; + if (isZeroForOne) { + route2TokenOutBalanceBefore = poolKey.currency1.balanceOf(address(this)); + } else { + route2TokenOutBalanceBefore = poolKey.currency0.balanceOf(address(this)); + } + v4Router.executeActions(swapData2); + uint256 route2TokenOutBalanceAfter; + if (isZeroForOne) { + route2TokenOutBalanceAfter = poolKey.currency1.balanceOf(address(this)); + } else { + route2TokenOutBalanceAfter = poolKey.currency0.balanceOf(address(this)); + } + + uint256 route2TokenOutReceived = route2TokenOutBalanceAfter - route2TokenOutBalanceBefore; + assertEq(route2TokenOutReceived, amountOutOfRoute2); + } + function testV4CLquoteExactInputSingle_OneForZero() public { address[] memory paths = new address[](2); paths[0] = address(Currency.unwrap(poolKey.currency1)); @@ -430,6 +1107,159 @@ contract MixedQuoterTest is assertLt(_gasEstimate, 50000); } + function test_quoteMixedExactInputSharedContext_V4Bin() public { + address[] memory paths = new address[](2); + paths[0] = address(token3); + paths[1] = address(token4); + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.V4_BIN_EXACT_INPUT_SINGLE)); + + bytes[] memory params = new bytes[](1); + params[0] = + abi.encode(IMixedQuoter.QuoteMixedV4ExactInputSingleParams({poolKey: binPoolKey, hookData: ZERO_BYTES})); + // swap 0.5 ether + (uint256 amountOut, uint256 gasEstimate) = mixedQuoter.quoteMixedExactInput(paths, actions, params, 0.5 ether); + uint256 swapPath1Output = amountOut; + + // swap 1 ether + (amountOut, gasEstimate) = mixedQuoter.quoteMixedExactInput(paths, actions, params, 1 ether); + uint256 swapPath2Output = amountOut - swapPath1Output; + + bytes[] memory multicallBytes = new bytes[](2); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.5 ether + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, 0.5 ether + ); + bytes[] memory results = mixedQuoter.multicall(multicallBytes); + + (uint256 amountOutOfRoute1,) = abi.decode(results[0], (uint256, uint256)); + (uint256 amountOutOfRoute2,) = abi.decode(results[1], (uint256, uint256)); + assertEq(amountOutOfRoute1, swapPath1Output); + assertEq(amountOutOfRoute2, swapPath2Output); + + // swap 0.5 ether in v4 bin pool + IBinRouterBase.BinSwapExactInputSingleParams memory swapParams1 = + IBinRouterBase.BinSwapExactInputSingleParams(binPoolKey, true, 0.5 ether, 0, ZERO_BYTES); + plan = plan.add(Actions.BIN_SWAP_EXACT_IN_SINGLE, abi.encode(swapParams1)); + bytes memory swapData1 = + plan.finalizeSwap(binPoolKey.currency0, binPoolKey.currency1, ActionConstants.MSG_SENDER); + uint256 route1TokenOutBalanceBefore = binPoolKey.currency1.balanceOf(address(this)); + v4Router.executeActions(swapData1); + uint256 route1TokenOutBalanceAfter = binPoolKey.currency1.balanceOf(address(this)); + + uint256 route1TokenOutReceived = route1TokenOutBalanceAfter - route1TokenOutBalanceBefore; + assertEq(route1TokenOutReceived, amountOutOfRoute1); + + // swap another 0.5 ether in v4 bin pool + IBinRouterBase.BinSwapExactInputSingleParams memory swapParams2 = + IBinRouterBase.BinSwapExactInputSingleParams(binPoolKey, true, 0.5 ether, 0, ZERO_BYTES); + plan = Planner.init(); + plan = plan.add(Actions.BIN_SWAP_EXACT_IN_SINGLE, abi.encode(swapParams2)); + bytes memory swapData2 = + plan.finalizeSwap(binPoolKey.currency0, binPoolKey.currency1, ActionConstants.MSG_SENDER); + uint256 route2TokenOutBalanceBefore = binPoolKey.currency1.balanceOf(address(this)); + v4Router.executeActions(swapData2); + uint256 route2TokenOutBalanceAfter = binPoolKey.currency1.balanceOf(address(this)); + + uint256 route2TokenOutReceived = route2TokenOutBalanceAfter - route2TokenOutBalanceBefore; + assertEq(route2TokenOutReceived, amountOutOfRoute2); + } + + function testFuzz_quoteMixedExactInputSharedContext_V4Bin(uint8 firstSwapPercent, bool isZeroForOne) public { + uint256 OneHundredPercent = type(uint8).max; + vm.assume(firstSwapPercent > 0 && firstSwapPercent < OneHundredPercent); + uint256 totalSwapAmount = 1 ether; + uint128 firstSwapAmount = uint128((totalSwapAmount * firstSwapPercent) / OneHundredPercent); + uint128 secondSwapAmount = uint128(totalSwapAmount - firstSwapAmount); + + address[] memory paths = new address[](2); + if (isZeroForOne) { + paths[0] = address(token3); + paths[1] = address(token4); + } else { + paths[0] = address(token4); + paths[1] = address(token3); + } + + bytes memory actions = new bytes(1); + actions[0] = bytes1(uint8(MixedQuoterActions.V4_BIN_EXACT_INPUT_SINGLE)); + + bytes[] memory params = new bytes[](1); + params[0] = + abi.encode(IMixedQuoter.QuoteMixedV4ExactInputSingleParams({poolKey: binPoolKey, hookData: ZERO_BYTES})); + + bytes[] memory multicallBytes = new bytes[](2); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, firstSwapAmount + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths, actions, params, secondSwapAmount + ); + bytes[] memory results = mixedQuoter.multicall(multicallBytes); + + (uint256 amountOutOfRoute1,) = abi.decode(results[0], (uint256, uint256)); + (uint256 amountOutOfRoute2,) = abi.decode(results[1], (uint256, uint256)); + + // first swap in v4 bin pool + IBinRouterBase.BinSwapExactInputSingleParams memory swapParams1 = + IBinRouterBase.BinSwapExactInputSingleParams(binPoolKey, isZeroForOne, firstSwapAmount, 0, ZERO_BYTES); + + plan = plan.add(Actions.BIN_SWAP_EXACT_IN_SINGLE, abi.encode(swapParams1)); + bytes memory swapData1; + if (isZeroForOne) { + swapData1 = plan.finalizeSwap(binPoolKey.currency0, binPoolKey.currency1, ActionConstants.MSG_SENDER); + } else { + swapData1 = plan.finalizeSwap(binPoolKey.currency1, binPoolKey.currency0, ActionConstants.MSG_SENDER); + } + uint256 route1TokenOutBalanceBefore; + if (isZeroForOne) { + route1TokenOutBalanceBefore = binPoolKey.currency1.balanceOf(address(this)); + } else { + route1TokenOutBalanceBefore = binPoolKey.currency0.balanceOf(address(this)); + } + v4Router.executeActions(swapData1); + uint256 route1TokenOutBalanceAfter; + if (isZeroForOne) { + route1TokenOutBalanceAfter = binPoolKey.currency1.balanceOf(address(this)); + } else { + route1TokenOutBalanceAfter = binPoolKey.currency0.balanceOf(address(this)); + } + + uint256 route1TokenOutReceived = route1TokenOutBalanceAfter - route1TokenOutBalanceBefore; + assertEq(route1TokenOutReceived, amountOutOfRoute1); + + // second swap in v4 bin pool + IBinRouterBase.BinSwapExactInputSingleParams memory swapParams2 = + IBinRouterBase.BinSwapExactInputSingleParams(binPoolKey, isZeroForOne, secondSwapAmount, 0, ZERO_BYTES); + plan = Planner.init(); + plan = plan.add(Actions.BIN_SWAP_EXACT_IN_SINGLE, abi.encode(swapParams2)); + bytes memory swapData2; + if (isZeroForOne) { + swapData2 = plan.finalizeSwap(binPoolKey.currency0, binPoolKey.currency1, ActionConstants.MSG_SENDER); + } else { + swapData2 = plan.finalizeSwap(binPoolKey.currency1, binPoolKey.currency0, ActionConstants.MSG_SENDER); + } + uint256 route2TokenOutBalanceBefore; + if (isZeroForOne) { + route2TokenOutBalanceBefore = binPoolKey.currency1.balanceOf(address(this)); + } else { + route2TokenOutBalanceBefore = binPoolKey.currency0.balanceOf(address(this)); + } + v4Router.executeActions(swapData2); + uint256 route2TokenOutBalanceAfter; + if (isZeroForOne) { + route2TokenOutBalanceAfter = binPoolKey.currency1.balanceOf(address(this)); + } else { + route2TokenOutBalanceAfter = binPoolKey.currency0.balanceOf(address(this)); + } + + uint256 route2TokenOutReceived = route2TokenOutBalanceAfter - route2TokenOutBalanceBefore; + assertEq(route2TokenOutReceived, amountOutOfRoute2); + } + function testBinQuoteExactInputSingle_OneForZero() public { address[] memory paths = new address[](2); paths[0] = address(token4); @@ -460,6 +1290,143 @@ contract MixedQuoterTest is assertLt(_gasEstimate, 50000); } + // route 1: path 1: token0 -> token1 -> token2 -> weth, cl pool -> ss pool -> v3 pool + // route 2: path 2: token0 -> token1 -> token2 -> weth, cl pool -> ss pool -> v2 pool + // route 2: path 3: token2 -> weth, v2 pool + function test_quoteMixedExactInputSharedContext_multi_route() public { + // path: token0 -> token1 -> token2 -> weth + address[] memory paths1 = new address[](4); + paths1[0] = address(token0); + paths1[1] = address(token1); + paths1[2] = address(token2); + paths1[3] = address(weth); + // cl pool -> ss pool -> v3 pool + bytes memory actions1 = new bytes(3); + actions1[0] = bytes1(uint8(MixedQuoterActions.V4_CL_EXACT_INPUT_SINGLE)); + actions1[1] = bytes1(uint8(MixedQuoterActions.SS_2_EXACT_INPUT_SINGLE)); + actions1[2] = bytes1(uint8(MixedQuoterActions.V3_EXACT_INPUT_SINGLE)); + bytes[] memory params1 = new bytes[](3); + params1[0] = + abi.encode(IMixedQuoter.QuoteMixedV4ExactInputSingleParams({poolKey: poolKey, hookData: ZERO_BYTES})); + params1[1] = new bytes(0); + uint24 fee = 500; + params1[2] = abi.encode(fee); + + // path: token0 -> token1 -> token2 -> weth + address[] memory paths2 = new address[](4); + paths2[0] = address(token0); + paths2[1] = address(token1); + paths2[2] = address(token2); + paths2[3] = address(weth); + // cl pool -> ss pool -> v2 pool + bytes memory actions2 = new bytes(3); + actions2[0] = bytes1(uint8(MixedQuoterActions.V4_CL_EXACT_INPUT_SINGLE)); + actions2[1] = bytes1(uint8(MixedQuoterActions.SS_2_EXACT_INPUT_SINGLE)); + actions2[2] = bytes1(uint8(MixedQuoterActions.V2_EXACT_INPUT_SINGLE)); + + bytes[] memory params2 = new bytes[](3); + params2[0] = + abi.encode(IMixedQuoter.QuoteMixedV4ExactInputSingleParams({poolKey: poolKey, hookData: ZERO_BYTES})); + params2[1] = new bytes(0); + params2[2] = new bytes(0); + + // path: token2 -> weth + address[] memory paths3 = new address[](2); + paths3[0] = address(token2); + paths3[1] = address(weth); + // v2 pool + bytes memory actions3 = new bytes(1); + actions3[0] = bytes1(uint8(MixedQuoterActions.V2_EXACT_INPUT_SINGLE)); + bytes[] memory params3 = new bytes[](1); + params3[0] = new bytes(0); + + bytes[] memory multicallBytes = new bytes[](3); + multicallBytes[0] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths1, actions1, params1, 1 ether + ); + multicallBytes[1] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths2, actions2, params2, 1 ether + ); + multicallBytes[2] = abi.encodeWithSelector( + IMixedQuoter.quoteMixedExactInputSharedContext.selector, paths3, actions3, params3, 1 ether + ); + bytes[] memory results = mixedQuoter.multicall(multicallBytes); + + (uint256 amountOutOfRoute1,) = abi.decode(results[0], (uint256, uint256)); + (uint256 amountOutOfRoute2,) = abi.decode(results[1], (uint256, uint256)); + (uint256 amountOutOfRoute3,) = abi.decode(results[2], (uint256, uint256)); + + // route 1: path 1: token0 -> token1 -> token2 -> weth, cl pool -> ss pool -> v3 pool + uint256 route1Token1BalanceBefore = token1.balanceOf(address(this)); + // swap 1 ether in v4 cl pool + ICLRouterBase.CLSwapExactInputSingleParams memory swapParams1 = + ICLRouterBase.CLSwapExactInputSingleParams(poolKey, true, 1 ether, 0, ZERO_BYTES); + plan = plan.add(Actions.CL_SWAP_EXACT_IN_SINGLE, abi.encode(swapParams1)); + bytes memory swapData1 = plan.finalizeSwap(poolKey.currency0, poolKey.currency1, ActionConstants.MSG_SENDER); + v4Router.executeActions(swapData1); + uint256 route1Token1BalanceAfter = token1.balanceOf(address(this)); + uint256 route1Token1Received = route1Token1BalanceAfter - route1Token1BalanceBefore; + + // swap route1Token1Received in ss pool + uint256 route1Token2BalanceBefore = token2.balanceOf(address(this)); + bool isZeroForOneOfRout1SS = address(token1) < address(token2); + stableSwapPair.exchange(isZeroForOneOfRout1SS ? 0 : 1, isZeroForOneOfRout1SS ? 1 : 0, route1Token1Received, 0); + uint256 route1Token2BalanceAfter = token2.balanceOf(address(this)); + uint256 route1Token2Received = route1Token2BalanceAfter - route1Token2BalanceBefore; + + // swap route1Token2Received in v3 pool + uint256 route1WethBalanceBefore = weth.balanceOf(address(this)); + PancakeV3Router.ExactInputSingleParams memory swapParams2 = PancakeV3Router.ExactInputSingleParams({ + tokenIn: address(token2), + tokenOut: address(weth), + fee: fee, + recipient: address(this), + deadline: block.timestamp + 300, + amountIn: route1Token2Received, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }); + v3Router.exactInputSingle(swapParams2); + uint256 route1WethBalanceAfter = weth.balanceOf(address(this)); + uint256 route1WethReceived = route1WethBalanceAfter - route1WethBalanceBefore; + assertEq(route1WethReceived, amountOutOfRoute1); + + // route 2: path 2: token0 -> token1 -> token2 -> weth, cl pool -> ss pool -> v2 pool + uint256 route2Token1BalanceBefore = token1.balanceOf(address(this)); + // swap 1 ether in v4 cl pool + ICLRouterBase.CLSwapExactInputSingleParams memory swapParams3 = + ICLRouterBase.CLSwapExactInputSingleParams(poolKey, true, 1 ether, 0, ZERO_BYTES); + plan = Planner.init(); + plan = plan.add(Actions.CL_SWAP_EXACT_IN_SINGLE, abi.encode(swapParams3)); + bytes memory swapData3 = plan.finalizeSwap(poolKey.currency0, poolKey.currency1, ActionConstants.MSG_SENDER); + v4Router.executeActions(swapData3); + uint256 route2Token1BalanceAfter = token1.balanceOf(address(this)); + uint256 route2Token1Received = route2Token1BalanceAfter - route2Token1BalanceBefore; + + // swap route2Token1Received in ss pool + uint256 route2Token2BalanceBefore = token2.balanceOf(address(this)); + bool isZeroForOneOfRout2SS = address(token1) < address(token2); + stableSwapPair.exchange(isZeroForOneOfRout2SS ? 0 : 1, isZeroForOneOfRout2SS ? 1 : 0, route2Token1Received, 0); + uint256 route2Token2BalanceAfter = token2.balanceOf(address(this)); + uint256 route2Token2Received = route2Token2BalanceAfter - route2Token2BalanceBefore; + + // swap route2Token2Received in v2 pool + uint256 route2WethBalanceBefore = weth.balanceOf(address(this)); + _swapV2(address(token2), address(weth), route2Token2Received); + uint256 route2WethBalanceAfter = weth.balanceOf(address(this)); + uint256 route2WethReceived = route2WethBalanceAfter - route2WethBalanceBefore; + // not exactly equal , but difference is very small, less than 1/1000000 + assertApproxEqRel(route2WethReceived, amountOutOfRoute2, 1e18 / 1000000); + + // route 3: path 3: token2 -> weth, v2 pool + uint256 route3WethBalanceBefore = weth.balanceOf(address(this)); + _swapV2(address(token2), address(weth), 1 ether); + uint256 route3WethBalanceAfter = weth.balanceOf(address(this)); + uint256 route3WethReceived = route3WethBalanceAfter - route3WethBalanceBefore; + // not exactly equal , but difference is very small, less than 1/100000 + assertApproxEqRel(route3WethReceived, amountOutOfRoute3, 1e18 / 100000); + } + // token0 -> token1 -> token2 // V4 CL Pool -> SS Pool function testQuoteMixedTwoHops_V4Cl_SS() public { @@ -660,6 +1627,22 @@ contract MixedQuoterTest is pair.mint(address(this)); } + function _swapV2(address tokenIn, address tokenOut, uint256 amountIn) internal returns (uint256) { + (address v2Token0, address v2Token1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenOut, tokenIn); + IPancakePair pair = IPancakePair(v2Factory.getPair(v2Token0, v2Token1)); + require(address(pair) != address(0), "Pair doesn't exist"); + + IERC20(tokenIn).transfer(address(pair), amountIn); + (uint256 reserveIn, uint256 reserveOut) = V3SmartRouterHelper.getReserves(address(v2Factory), tokenIn, tokenOut); + uint256 amountOut = V3SmartRouterHelper.getAmountOut(amountIn, reserveIn, reserveOut); + + (uint256 amount0Out, uint256 amount1Out) = + tokenIn == v2Token0 ? (uint256(0), amountOut) : (amountOut, uint256(0)); + + pair.swap(amount0Out, amount1Out, address(this), new bytes(0)); + return amountOut; + } + function _getBytecodePath() internal pure returns (string memory) { // Create a Pancakeswap V2 pair // relative to the root of the project diff --git a/test/helpers/PancakeV3Router.sol b/test/helpers/PancakeV3Router.sol new file mode 100644 index 00000000..fd5cc668 --- /dev/null +++ b/test/helpers/PancakeV3Router.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {TickMath} from "pancake-v4-core/src/pool-cl/libraries/TickMath.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {V3SmartRouterHelper} from "../../src/libraries/external/V3SmartRouterHelper.sol"; +import {IPancakeV3Factory} from "../../src/interfaces/external/IPancakeV3Factory.sol"; +import {IPancakeV3Pool} from "../../src/interfaces/external/IPancakeV3Pool.sol"; + +/// @dev A mock PancakeV3Router contract that can be used to test v3 swap. +/// @dev Only support exactInputSingle for now. +/// @dev This contract is only used for testing, and should not be deployed in production. +contract PancakeV3Router { + IPancakeV3Factory public factory; + + constructor(IPancakeV3Factory _factory) { + factory = _factory; + } + + /// @dev Returns the pool for the given token pair and fee. The pool contract may or may not exist. + function getPool(address tokenA, address tokenB, uint24 fee) private view returns (IPancakeV3Pool) { + if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); + return IPancakeV3Pool(factory.getPool(tokenA, tokenB, fee)); + } + + function pancakeV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external { + require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported + (address tokenIn, address tokenOut, uint24 fee, address payer) = + abi.decode(data, (address, address, uint24, address)); + V3SmartRouterHelper.verifyCallback(address(factory), tokenIn, tokenOut, fee); + + (bool isExactInput, uint256 amountToPay) = + amount0Delta > 0 ? (tokenIn < tokenOut, uint256(amount0Delta)) : (tokenOut < tokenIn, uint256(amount1Delta)); + if (isExactInput) { + IERC20(tokenIn).transferFrom(payer, msg.sender, amountToPay); + } + } + + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut) { + amountOut = exactInputInternal( + params.amountIn, + params.recipient, + params.sqrtPriceLimitX96, + abi.encode(params.tokenIn, params.tokenOut, params.fee, msg.sender) + ); + require(amountOut >= params.amountOutMinimum, "Too little received"); + } + + function exactInputInternal(uint256 amountIn, address recipient, uint160 sqrtPriceLimitX96, bytes memory data) + private + returns (uint256 amountOut) + { + // allow swapping to the router address with address 0 + if (recipient == address(0)) recipient = address(this); + + (address tokenIn, address tokenOut, uint24 fee,) = abi.decode(data, (address, address, uint24, address)); + + bool zeroForOne = tokenIn < tokenOut; + + (int256 amount0, int256 amount1) = getPool(tokenIn, tokenOut, fee).swap( + recipient, + zeroForOne, + int256(amountIn), + sqrtPriceLimitX96 == 0 + ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) + : sqrtPriceLimitX96, + data + ); + + return uint256(-(zeroForOne ? amount1 : amount0)); + } +} diff --git a/test/libraries/CalldataDecoder.t.sol b/test/libraries/CalldataDecoder.t.sol index c678416a..dbdcb82c 100644 --- a/test/libraries/CalldataDecoder.t.sol +++ b/test/libraries/CalldataDecoder.t.sol @@ -25,6 +25,16 @@ contract CalldataDecoderTest is Test { assertEq(_address, __address); } + function test_fuzz_decodeCurrencyAndAddress_outOfBounds(Currency _currency, address __address) public { + bytes memory params = abi.encode(_currency, __address); + bytes memory invalidParams = _removeFinalByte(params); + + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrencyAndAddress(invalidParams); + } + function test_fuzz_decodeCurrency(Currency _currency) public view { bytes memory params = abi.encode(_currency); (Currency currency) = decoder.decodeCurrency(params); @@ -32,6 +42,16 @@ contract CalldataDecoderTest is Test { assertEq(Currency.unwrap(currency), Currency.unwrap(_currency)); } + function test_fuzz_decodeCurrency_outOfBounds(Currency _currency) public { + bytes memory params = abi.encode(_currency); + bytes memory invalidParams = _removeFinalByte(params); + + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrency(invalidParams); + } + function test_fuzz_decodeActionsRouterParams(bytes memory _actions, bytes[] memory _actionParams) public view { bytes memory params = abi.encode(_actions, _actionParams); (bytes memory actions, bytes[] memory actionParams) = decoder.decodeActionsRouterParams(params); @@ -86,6 +106,16 @@ contract CalldataDecoderTest is Test { assertEq(Currency.unwrap(currency1), Currency.unwrap(_currency1)); } + function test_fuzz_decodeCurrencyPair_outOfBounds(Currency _currency0, Currency _currency1) public { + bytes memory params = abi.encode(_currency0, _currency1); + bytes memory invalidParams = _removeFinalByte(params); + + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrencyPair(invalidParams); + } + function test_fuzz_decodeCurrencyPairAndAddress(Currency _currency0, Currency _currency1, address __address) public view @@ -98,6 +128,20 @@ contract CalldataDecoderTest is Test { assertEq(_address, __address); } + function test_fuzz_decodeCurrencyPairAndAddress__outOfBounds( + Currency _currency0, + Currency _currency1, + address __address + ) public { + bytes memory params = abi.encode(_currency0, _currency1, __address); + bytes memory invalidParams = _removeFinalByte(params); + + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrencyPairAndAddress(invalidParams); + } + function test_fuzz_decodeCurrencyAddressAndUint256(Currency _currency, address _addr, uint256 _amount) public view @@ -110,6 +154,18 @@ contract CalldataDecoderTest is Test { assertEq(amount, _amount); } + function test_fuzz_decodeCurrencyAddressAndUint256_outOfBounds(Currency _currency, address _addr, uint256 _amount) + public + { + bytes memory params = abi.encode(_currency, _addr, _amount); + bytes memory invalidParams = _removeFinalByte(params); + + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrencyAddressAndUint256(invalidParams); + } + function test_fuzz_decodeCurrencyAndUint256(Currency _currency, uint256 _amount) public view { bytes memory params = abi.encode(_currency, _amount); (Currency currency, uint256 amount) = decoder.decodeCurrencyAndUint256(params); @@ -118,10 +174,38 @@ contract CalldataDecoderTest is Test { assertEq(amount, _amount); } + function test_fuzz_decodeCurrencyAndUint256_outOfBounds(Currency _currency, uint256 _amount) public { + bytes memory params = abi.encode(_currency, _amount); + bytes memory invalidParams = _removeFinalByte(params); + + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrencyAndUint256(invalidParams); + } + function test_fuzz_decodeUint256(uint256 _amount) public view { bytes memory params = abi.encode(_amount); uint256 amount = decoder.decodeUint256(params); assertEq(amount, _amount); } + + function test_fuzz_decodeUint256_outOfBounds(uint256 _amount) public { + bytes memory params = abi.encode(_amount); + bytes memory invalidParams = _removeFinalByte(params); + + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeUint256(invalidParams); + } + + function _removeFinalByte(bytes memory params) internal pure returns (bytes memory result) { + result = new bytes(params.length - 1); + // dont copy the final byte + for (uint256 i = 0; i < params.length - 2; i++) { + result[i] = params[i]; + } + } } diff --git a/test/pool-bin/libraries/BinCalldataDecoder.t.sol b/test/pool-bin/libraries/BinCalldataDecoder.t.sol index 26a04c25..ad5f36f9 100644 --- a/test/pool-bin/libraries/BinCalldataDecoder.t.sol +++ b/test/pool-bin/libraries/BinCalldataDecoder.t.sol @@ -6,8 +6,10 @@ import {Currency} from "pancake-v4-core/src/types/Currency.sol"; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; import {MockBinCalldataDecoder} from "../mocks/MockBinCalldataDecoder.sol"; +import {CalldataDecoder} from "../../../src/libraries/CalldataDecoder.sol"; import {IV4Router} from "../../../src/interfaces/IV4Router.sol"; import {IBinPositionManager} from "../../../src/pool-bin/interfaces/IBinPositionManager.sol"; +import {IBinRouterBase} from "../../../src/pool-bin/interfaces/IBinRouterBase.sol"; import {PathKey} from "../../../src/libraries/PathKey.sol"; contract BinCalldataDecoderTest is Test { @@ -81,6 +83,27 @@ contract BinCalldataDecoderTest is Test { assertEq(swapParams.amountOutMinimum, _swapParams.amountOutMinimum); } + function test_decodeBinSwapExactInParams_outOfBounds() public { + PathKey[] memory path = new PathKey[](0); + IV4Router.BinSwapExactInputParams memory _swapParams = IBinRouterBase.BinSwapExactInputParams({ + currencyIn: Currency.wrap(makeAddr("currencyIn")), + path: path, + amountIn: 1 ether, + amountOutMinimum: 1 ether + }); + + /// @dev params.length is 192 as abi.encode adds 32 bytes for dynamic field. However ether.js doesn't add 32 bytes + /// thus we need to remove 32 bytes from the end of the params + /// ref: https://ethereum.stackexchange.com/questions/152971/abi-encode-decode-mystery-additional-32-byte-field-uniswap-v2 + bytes memory params = abi.encode(_swapParams); + assertEq(params.length, 192); + bytes memory invalidParam = _removeBytes(params, 32 + 1); + assertEq(invalidParam.length, params.length - 33); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeBinSwapExactInParams(invalidParam); + } + function test_fuzz_decodeBinSwapExactInSingleParams(IV4Router.BinSwapExactInputSingleParams memory _swapParams) public view @@ -95,6 +118,27 @@ contract BinCalldataDecoderTest is Test { assertEq(swapParams.hookData, _swapParams.hookData); } + function test_fuzz_decodeBinSwapExactInSingleParams_outOfBounds(PoolKey memory key) public { + IV4Router.BinSwapExactInputSingleParams memory _swapParams = IBinRouterBase.BinSwapExactInputSingleParams({ + poolKey: key, + swapForY: true, + amountIn: 1 ether, + amountOutMinimum: 1 ether, + hookData: "" + }); + + /// @dev params.length is 384 as abi.encode adds 32 bytes for dynamic field. However ether.js doesn't add 32 bytes + /// thus we need to remove 32 bytes from the end of the params + /// ref: https://ethereum.stackexchange.com/questions/152971/abi-encode-decode-mystery-additional-32-byte-field-uniswap-v2 + bytes memory params = abi.encode(_swapParams); + assertEq(params.length, 0x180); + bytes memory invalidParam = _removeBytes(params, 32 + 1); + assertEq(invalidParam.length, 0x160 - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeBinSwapExactInSingleParams(invalidParam); + } + function test_fuzz_decodeBinSwapExactOutParams(IV4Router.BinSwapExactOutputParams memory _swapParams) public view { bytes memory params = abi.encode(_swapParams); IV4Router.BinSwapExactOutputParams memory swapParams = decoder.decodeBinSwapExactOutParams(params); @@ -105,6 +149,27 @@ contract BinCalldataDecoderTest is Test { assertEq(swapParams.amountInMaximum, _swapParams.amountInMaximum); } + function test_decodeBinSwapExactOutParams_outOfBounds() public { + PathKey[] memory path = new PathKey[](0); + IV4Router.BinSwapExactOutputParams memory _swapParams = IBinRouterBase.BinSwapExactOutputParams({ + currencyOut: Currency.wrap(makeAddr("currencyOut")), + path: path, + amountOut: 1 ether, + amountInMaximum: 1 ether + }); + + /// @dev params.length is 192 as abi.encode adds 32 bytes for dynamic field. However ether.js doesn't add 32 bytes + /// thus we need to remove 32 bytes from the end of the params + /// ref: https://ethereum.stackexchange.com/questions/152971/abi-encode-decode-mystery-additional-32-byte-field-uniswap-v2 + bytes memory params = abi.encode(_swapParams); + assertEq(params.length, 192); + bytes memory invalidParam = _removeBytes(params, 32 + 1); + assertEq(invalidParam.length, params.length - 33); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeBinSwapExactOutParams(invalidParam); + } + function test_fuzz_decodeBinSwapExactOutSingleParams(IV4Router.BinSwapExactOutputSingleParams memory _swapParams) public view @@ -119,6 +184,27 @@ contract BinCalldataDecoderTest is Test { assertEq(swapParams.hookData, _swapParams.hookData); } + function test_fuzz_decodeBinSwapExactOutSingleParams_outOfBounds(PoolKey memory key) public { + IV4Router.BinSwapExactOutputSingleParams memory _swapParams = IBinRouterBase.BinSwapExactOutputSingleParams({ + poolKey: key, + swapForY: true, + amountOut: 1 ether, + amountInMaximum: 1 ether, + hookData: "" + }); + + /// @dev params.length is 384 as abi.encode adds 32 bytes for dynamic field. However ether.js doesn't add 32 bytes + /// thus we need to remove 32 bytes from the end of the params + /// ref: https://ethereum.stackexchange.com/questions/152971/abi-encode-decode-mystery-additional-32-byte-field-uniswap-v2 + bytes memory params = abi.encode(_swapParams); + assertEq(params.length, 0x180); + bytes memory invalidParam = _removeBytes(params, 32 + 1); + assertEq(invalidParam.length, 0x160 - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeBinSwapExactOutSingleParams(invalidParam); + } + function _assertEq(PathKey[] memory path1, PathKey[] memory path2) internal pure { assertEq(path1.length, path2.length); for (uint256 i = 0; i < path1.length; i++) { @@ -153,4 +239,25 @@ contract BinCalldataDecoderTest is Test { assertEq(arr1[i], arr2[i]); } } + + function _removeFinalByte(bytes memory params) internal pure returns (bytes memory result) { + result = new bytes(params.length - 1); + // dont copy the final byte + for (uint256 i = 0; i < params.length - 2; i++) { + result[i] = params[i]; + } + } + + /// @param amountOfByteToRemove the number of bytes to remove from the end of params + function _removeBytes(bytes memory params, uint256 amountOfByteToRemove) + internal + pure + returns (bytes memory result) + { + result = new bytes(params.length - amountOfByteToRemove); + // dont copy the final byte + for (uint256 i = 0; i < result.length; i++) { + result[i] = params[i]; + } + } } diff --git a/test/pool-cl/libraries/CLCalldataDecoder.t.sol b/test/pool-cl/libraries/CLCalldataDecoder.t.sol index 9fcdb01e..ad05a6c3 100644 --- a/test/pool-cl/libraries/CLCalldataDecoder.t.sol +++ b/test/pool-cl/libraries/CLCalldataDecoder.t.sol @@ -7,7 +7,9 @@ import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; import {PoolId} from "pancake-v4-core/src/types/PoolId.sol"; import {MockCLCalldataDecoder} from "../mocks/MockCLCalldataDecoder.sol"; +import {CalldataDecoder} from "../../../src/libraries/CalldataDecoder.sol"; import {IV4Router} from "../../../src/interfaces/IV4Router.sol"; +import {ICLRouterBase} from "../../../src/pool-cl/interfaces/ICLRouterBase.sol"; import {PathKey} from "../../../src/libraries/PathKey.sol"; contract CLCalldataDecoderTest is Test { @@ -35,6 +37,20 @@ contract CLCalldataDecoderTest is Test { assertEq(hookData, _hookData); } + function test_fuzz_decodeCLModifyLiquidityParams_outOfBounds( + uint256 _tokenId, + uint256 _liquidity, + uint128 _amount0, + uint128 _amount1 + ) public { + bytes memory params = abi.encode(_tokenId, _liquidity, _amount0, _amount1, ""); + bytes memory invalidParam = _removeFinalByte(params); + assertEq(invalidParam.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCLModifyLiquidityParams(invalidParam); + } + function test_fuzz_decodeBurnParams( uint256 _tokenId, uint128 _amount0Min, @@ -116,6 +132,27 @@ contract CLCalldataDecoderTest is Test { _assertEq(swapParams.path, _swapParams.path); } + function test_decodeSwapExactInParams_outOfBounds() public { + PathKey[] memory path = new PathKey[](0); + IV4Router.CLSwapExactInputParams memory _swapParams = ICLRouterBase.CLSwapExactInputParams({ + currencyIn: Currency.wrap(makeAddr("currencyIn")), + path: path, + amountIn: 1 ether, + amountOutMinimum: 1 ether + }); + + /// @dev params.length is 192 as abi.encode adds 32 bytes for dynamic field. However ether.js doesn't add 32 bytes + /// thus we need to remove 32 bytes from the end of the params + /// ref: https://ethereum.stackexchange.com/questions/152971/abi-encode-decode-mystery-additional-32-byte-field-uniswap-v2 + bytes memory params = abi.encode(_swapParams); + assertEq(params.length, 192); + bytes memory invalidParam = _removeBytes(params, 32 + 1); + assertEq(invalidParam.length, params.length - 33); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCLSwapExactInParams(invalidParam); + } + function test_fuzz_decodeSwapExactInSingleParams(IV4Router.CLSwapExactInputSingleParams calldata _swapParams) public view @@ -130,6 +167,27 @@ contract CLCalldataDecoderTest is Test { _assertEq(swapParams.poolKey, _swapParams.poolKey); } + function test_fuzz_decodeSwapExactInSingleParams_outOfBounds(PoolKey memory key) public { + IV4Router.CLSwapExactInputSingleParams memory _swapParams = ICLRouterBase.CLSwapExactInputSingleParams({ + poolKey: key, + zeroForOne: true, + amountIn: 1 ether, + amountOutMinimum: 1 ether, + hookData: "" + }); + + /// @dev params.length is 384 as abi.encode adds 32 bytes for dynamic field. However ether.js doesn't add 32 bytes + /// thus we need to remove 32 bytes from the end of the params + /// ref: https://ethereum.stackexchange.com/questions/152971/abi-encode-decode-mystery-additional-32-byte-field-uniswap-v2 + bytes memory params = abi.encode(_swapParams); + assertEq(params.length, 0x180); + bytes memory invalidParam = _removeBytes(params, 32 + 1); + assertEq(invalidParam.length, 0x160 - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCLSwapExactInSingleParams(invalidParam); + } + function test_fuzz_decodeSwapExactOutParams(IV4Router.CLSwapExactOutputParams calldata _swapParams) public view { bytes memory params = abi.encode(_swapParams); IV4Router.CLSwapExactOutputParams memory swapParams = decoder.decodeCLSwapExactOutParams(params); @@ -140,6 +198,27 @@ contract CLCalldataDecoderTest is Test { _assertEq(swapParams.path, _swapParams.path); } + function test_decodeSwapExactOutParams_outOfBounds() public { + PathKey[] memory path = new PathKey[](0); + IV4Router.CLSwapExactOutputParams memory _swapParams = ICLRouterBase.CLSwapExactOutputParams({ + currencyOut: Currency.wrap(makeAddr("currencyOut")), + path: path, + amountOut: 1 ether, + amountInMaximum: 1 ether + }); + + /// @dev params.length is 192 as abi.encode adds 32 bytes for dynamic field. However ether.js doesn't add 32 bytes + /// thus we need to remove 32 bytes from the end of the params + /// ref: https://ethereum.stackexchange.com/questions/152971/abi-encode-decode-mystery-additional-32-byte-field-uniswap-v2 + bytes memory params = abi.encode(_swapParams); + assertEq(params.length, 192); + bytes memory invalidParam = _removeBytes(params, 32 + 1); + assertEq(invalidParam.length, params.length - 33); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCLSwapExactOutParams(invalidParam); + } + function test_fuzz_decodeSwapExactOutSingleParams(IV4Router.CLSwapExactOutputSingleParams calldata _swapParams) public view @@ -170,6 +249,27 @@ contract CLCalldataDecoderTest is Test { assertEq(_hookData, hookData); } + function test_fuzz_decodeSwapExactOutSingleParams_outOfBounds(PoolKey memory key) public { + IV4Router.CLSwapExactOutputSingleParams memory _swapParams = ICLRouterBase.CLSwapExactOutputSingleParams({ + poolKey: key, + zeroForOne: true, + amountOut: 1 ether, + amountInMaximum: 1 ether, + hookData: "" + }); + + /// @dev params.length is 384 as abi.encode adds 32 bytes for dynamic field. However ether.js doesn't add 32 bytes + /// thus we need to remove 32 bytes from the end of the params + /// ref: https://ethereum.stackexchange.com/questions/152971/abi-encode-decode-mystery-additional-32-byte-field-uniswap-v2 + bytes memory params = abi.encode(_swapParams); + assertEq(params.length, 0x180); + bytes memory invalidParam = _removeBytes(params, 32 + 1); + assertEq(invalidParam.length, 0x160 - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCLSwapExactOutSingleParams(invalidParam); + } + function _assertEq(PathKey[] memory path1, PathKey[] memory path2) internal pure { assertEq(path1.length, path2.length); for (uint256 i = 0; i < path1.length; i++) { @@ -190,4 +290,25 @@ contract CLCalldataDecoderTest is Test { assertEq(key1.fee, key2.fee); assertEq(key1.parameters, key2.parameters); } + + function _removeFinalByte(bytes memory params) internal pure returns (bytes memory result) { + result = new bytes(params.length - 1); + // dont copy the final byte + for (uint256 i = 0; i < params.length - 2; i++) { + result[i] = params[i]; + } + } + + /// @param amountOfByteToRemove the number of bytes to remove from the end of params + function _removeBytes(bytes memory params, uint256 amountOfByteToRemove) + internal + pure + returns (bytes memory result) + { + result = new bytes(params.length - amountOfByteToRemove); + // dont copy the final byte + for (uint256 i = 0; i < result.length; i++) { + result[i] = params[i]; + } + } } diff --git a/test/pool-cl/mocks/MockCLBadSubscribers.sol b/test/pool-cl/mocks/MockCLBadSubscribers.sol index 5e61fa40..22b5c2d4 100644 --- a/test/pool-cl/mocks/MockCLBadSubscribers.sol +++ b/test/pool-cl/mocks/MockCLBadSubscribers.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {ICLSubscriber} from "../../../src/pool-cl/interfaces/ICLSubscriber.sol"; import {CLPositionManager} from "../../../src/pool-cl/CLPositionManager.sol"; import {BalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol"; +import {CLPositionInfo} from "../../../src/pool-cl/libraries/CLPositionInfoLibrary.sol"; /// @notice A subscriber contract that returns values from the subscriber entrypoints contract MockCLReturnDataSubscriber is ICLSubscriber { @@ -12,6 +13,7 @@ contract MockCLReturnDataSubscriber is ICLSubscriber { uint256 public notifySubscribeCount; uint256 public notifyUnsubscribeCount; uint256 public notifyModifyLiquidityCount; + uint256 public notifyBurnCount; error NotAuthorizedNotifer(address sender); @@ -47,6 +49,10 @@ contract MockCLReturnDataSubscriber is ICLSubscriber { notifyModifyLiquidityCount++; } + function notifyBurn(uint256, address, CLPositionInfo, uint256, BalanceDelta) external { + notifyBurnCount++; + } + function setReturnDataSize(uint256 _value) external { memPtr = _value; } @@ -81,6 +87,10 @@ contract MockCLRevertSubscriber is ICLSubscriber { revert TestRevert("notifyUnsubscribe"); } + function notifyBurn(uint256, address, CLPositionInfo, uint256, BalanceDelta) external { + revert TestRevert("notifyBurn"); + } + function notifyModifyLiquidity(uint256, int256, BalanceDelta) external view onlyByPosm { revert TestRevert("notifyModifyLiquidity"); } diff --git a/test/pool-cl/mocks/MockCLSubscriber.sol b/test/pool-cl/mocks/MockCLSubscriber.sol index 95d55992..0812aba0 100644 --- a/test/pool-cl/mocks/MockCLSubscriber.sol +++ b/test/pool-cl/mocks/MockCLSubscriber.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {ICLSubscriber} from "../../../src/pool-cl/interfaces/ICLSubscriber.sol"; import {CLPositionManager} from "../../../src/pool-cl/CLPositionManager.sol"; import {BalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol"; +import {CLPositionInfo} from "../../../src/pool-cl/libraries/CLPositionInfoLibrary.sol"; /// @notice A subscriber contract that ingests updates from the v4 position manager contract MockCLSubscriber is ICLSubscriber { @@ -12,7 +13,9 @@ contract MockCLSubscriber is ICLSubscriber { uint256 public notifySubscribeCount; uint256 public notifyUnsubscribeCount; uint256 public notifyModifyLiquidityCount; + uint256 public notifyBurnCount; int256 public liquidityChange; + uint256 public liquidity; BalanceDelta public feesAccrued; bytes public subscribeData; @@ -44,4 +47,16 @@ contract MockCLSubscriber is ICLSubscriber { liquidityChange = _liquidityChange; feesAccrued = _feesAccrued; } + + function notifyBurn( + uint256 tokenId, + address owner, + CLPositionInfo info, + uint256 _liquidity, + BalanceDelta _feesAccrued + ) external onlyByPosm { + liquidity = _liquidity; + feesAccrued = _feesAccrued; + notifyBurnCount++; + } } diff --git a/test/pool-cl/position-managers/CLPositionManager.notifier.sol b/test/pool-cl/position-managers/CLPositionManager.notifier.sol index 4eb40bf4..474316e6 100644 --- a/test/pool-cl/position-managers/CLPositionManager.notifier.sol +++ b/test/pool-cl/position-managers/CLPositionManager.notifier.sol @@ -548,8 +548,32 @@ contract CLPositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { lpm.modifyLiquidities(calls, _deadline); } - /// @notice burning a position will automatically notify unsubscribe - function test_burn_unsubscribe() public { + function test_notifyBurn_wraps_revert() public { + uint256 tokenId = lpm.nextTokenId(); + mint(key, -300, 300, 100e18, alice, ZERO_BYTES); + + // approve this contract to operate on alices liq + vm.startPrank(alice); + lpm.approve(address(this), tokenId); + vm.stopPrank(); + + lpm.subscribe(tokenId, address(revertSubscriber), ZERO_BYTES); + + bytes memory calls = getBurnEncoded(tokenId, ZERO_BYTES); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(revertSubscriber), + ICLSubscriber.notifyBurn.selector, + abi.encodeWithSelector(MockCLRevertSubscriber.TestRevert.selector, "notifyBurn"), + abi.encodeWithSelector(ICLNotifier.BurnNotificationReverted.selector) + ) + ); + lpm.modifyLiquidities(calls, _deadline); + } + + /// @notice burning a position will automatically notify burn + function test_notifyBurn_succeeds() public { uint256 tokenId = lpm.nextTokenId(); mint(key, -300, 300, 100e18, alice, ZERO_BYTES); @@ -564,11 +588,12 @@ contract CLPositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { assertEq(sub.notifyUnsubscribeCount(), 0); - // burn the position, causing an unsubscribe + // burn the position, causing a notifyBurn burn(tokenId, ZERO_BYTES); // position is now unsubscribed - assertEq(sub.notifyUnsubscribeCount(), 1); + assertEq(sub.notifyUnsubscribeCount(), 0); + assertEq(sub.notifyBurnCount(), 1); } /// @notice Test that users cannot forcibly avoid unsubscribe logic via gas limits