From 294eee6f5bdc0eae245630fc1c9c700880430b00 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Tue, 3 Jun 2025 22:17:24 +0800 Subject: [PATCH 01/16] feat: update stable swap impl --- foundry.toml | 2 +- .../BinNativePancakeSwapInfinityTest.json | 6 +-- snapshots/BinPancakeSwapInfinityTest.json | 12 +++--- .../CLNativePancakeSwapInfinityTest.json | 4 +- snapshots/CLPancakeSwapInfinityTest.json | 12 +++--- snapshots/StableSwapBusdUsdcTest.json | 4 +- snapshots/UniversalRouterTest.json | 4 +- snapshots/V2BnbCake.json | 4 +- snapshots/V2MockBnb.json | 4 +- snapshots/V3BnbCake.json | 2 +- .../V3ToInfinityMigrationNativeTest.json | 4 +- snapshots/V3ToInfinityMigrationTest.json | 4 +- src/modules/pancakeswap/StableSwapRouter.sol | 21 +++++++--- test/stableSwap/StableSwap.t.sol | 38 +++++++++++++++++++ 14 files changed, 85 insertions(+), 36 deletions(-) diff --git a/foundry.toml b/foundry.toml index 120d514..1f9c23b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ src = "src" out = 'foundry-out' libs = ["lib"] via_ir = true -optimizer_runs = 20_000 +optimizer_runs = 10_000 ffi = true fs_permissions = [ { access = "read-write", path = ".forge-snapshots/" }, diff --git a/snapshots/BinNativePancakeSwapInfinityTest.json b/snapshots/BinNativePancakeSwapInfinityTest.json index d4f9548..01dca82 100644 --- a/snapshots/BinNativePancakeSwapInfinityTest.json +++ b/snapshots/BinNativePancakeSwapInfinityTest.json @@ -1,6 +1,6 @@ { - "test_infiBinSwap_ExactInSingle_NativeIn": "128929", - "test_infiBinSwap_ExactInSingle_NativeOut": "117627", - "test_infiBinSwap_ExactInSingle_NativeOut_RouterRecipient": "117916", + "test_infiBinSwap_ExactInSingle_NativeIn": "129700", + "test_infiBinSwap_ExactInSingle_NativeOut": "118428", + "test_infiBinSwap_ExactInSingle_NativeOut_RouterRecipient": "118717", "test_infiBinSwap_infiInitializeBinPool": "132846" } \ No newline at end of file diff --git a/snapshots/BinPancakeSwapInfinityTest.json b/snapshots/BinPancakeSwapInfinityTest.json index 198c77c..a2f7404 100644 --- a/snapshots/BinPancakeSwapInfinityTest.json +++ b/snapshots/BinPancakeSwapInfinityTest.json @@ -1,9 +1,9 @@ { - "test_infiBinSwap_ExactInSingle": "143529", - "test_infiBinSwap_ExactIn_MultiHop": "174923", - "test_infiBinSwap_ExactIn_SingleHop": "145327", - "test_infiBinSwap_ExactOutSingle": "147891", - "test_infiBinSwap_ExactOut_MultiHop": "178771", - "test_infiBinSwap_ExactOut_SingleHop": "149699", + "test_infiBinSwap_ExactInSingle": "144330", + "test_infiBinSwap_ExactIn_MultiHop": "176210", + "test_infiBinSwap_ExactIn_SingleHop": "146128", + "test_infiBinSwap_ExactOutSingle": "148584", + "test_infiBinSwap_ExactOut_MultiHop": "179842", + "test_infiBinSwap_ExactOut_SingleHop": "150392", "test_infiBinSwap_InitializeBinPool": "152986" } \ No newline at end of file diff --git a/snapshots/CLNativePancakeSwapInfinityTest.json b/snapshots/CLNativePancakeSwapInfinityTest.json index a30e2e8..d3e968b 100644 --- a/snapshots/CLNativePancakeSwapInfinityTest.json +++ b/snapshots/CLNativePancakeSwapInfinityTest.json @@ -1,5 +1,5 @@ { - "test_infiClSwap_ExactInSingle_NativeIn": "161970", - "test_infiClSwap_ExactInSingle_NativeOut": "144706", + "test_infiClSwap_ExactInSingle_NativeIn": "162412", + "test_infiClSwap_ExactInSingle_NativeOut": "145154", "test_infiClSwap_infiInitializeClPool": "133628" } \ No newline at end of file diff --git a/snapshots/CLPancakeSwapInfinityTest.json b/snapshots/CLPancakeSwapInfinityTest.json index 71b9b15..2610903 100644 --- a/snapshots/CLPancakeSwapInfinityTest.json +++ b/snapshots/CLPancakeSwapInfinityTest.json @@ -1,9 +1,9 @@ { - "test_infiClSwap_ExactInSingle": "176570", - "test_infiClSwap_ExactIn_MultiHop": "241016", - "test_infiClSwap_ExactIn_SingleHop": "178361", - "test_infiClSwap_ExactOutSingle": "180894", - "test_infiClSwap_ExactOut_MultiHop": "244792", - "test_infiClSwap_ExactOut_SingleHop": "182697", + "test_infiClSwap_ExactInSingle": "177042", + "test_infiClSwap_ExactIn_MultiHop": "241645", + "test_infiClSwap_ExactIn_SingleHop": "178833", + "test_infiClSwap_ExactOutSingle": "181362", + "test_infiClSwap_ExactOut_MultiHop": "245413", + "test_infiClSwap_ExactOut_SingleHop": "183165", "test_infiClSwap_infiInitializeClPool": "153756" } \ No newline at end of file diff --git a/snapshots/StableSwapBusdUsdcTest.json b/snapshots/StableSwapBusdUsdcTest.json index dd9c422..902d5e5 100644 --- a/snapshots/StableSwapBusdUsdcTest.json +++ b/snapshots/StableSwapBusdUsdcTest.json @@ -1,4 +1,4 @@ { - "test_stableSwap_ExactInput0For1": "193988", - "test_stableSwap_ExactInput1For0": "194056" + "test_stableSwap_ExactInput0For1": "193548", + "test_stableSwap_ExactInput1For0": "194840" } \ No newline at end of file diff --git a/snapshots/UniversalRouterTest.json b/snapshots/UniversalRouterTest.json index a1a4348..48c552a 100644 --- a/snapshots/UniversalRouterTest.json +++ b/snapshots/UniversalRouterTest.json @@ -1,4 +1,4 @@ { - "UniversalRouterBytecodeSize": "24350", - "test_sweep_token": "55429" + "UniversalRouterBytecodeSize": "24291", + "test_sweep_token": "55435" } \ No newline at end of file diff --git a/snapshots/V2BnbCake.json b/snapshots/V2BnbCake.json index 296597a..67f3dc5 100644 --- a/snapshots/V2BnbCake.json +++ b/snapshots/V2BnbCake.json @@ -1,4 +1,4 @@ { - "test_v2Swap_exactInput0For1": "116435", - "test_v2Swap_exactOutput0For1": "117038" + "test_v2Swap_exactInput0For1": "116453", + "test_v2Swap_exactOutput0For1": "117044" } \ No newline at end of file diff --git a/snapshots/V2MockBnb.json b/snapshots/V2MockBnb.json index 1bacec2..4d85cbc 100644 --- a/snapshots/V2MockBnb.json +++ b/snapshots/V2MockBnb.json @@ -1,4 +1,4 @@ { - "test_v2Swap_exactInput0For1": "100167", - "test_v2Swap_exactOutput0For1": "100791" + "test_v2Swap_exactInput0For1": "100185", + "test_v2Swap_exactOutput0For1": "100797" } \ No newline at end of file diff --git a/snapshots/V3BnbCake.json b/snapshots/V3BnbCake.json index f764c4d..55956e3 100644 --- a/snapshots/V3BnbCake.json +++ b/snapshots/V3BnbCake.json @@ -1,6 +1,6 @@ { "test_v3Swap_ExactInput0For1": "151904", - "test_v3Swap_ExactInput0For1_ContractBalance": "154811", + "test_v3Swap_ExactInput0For1_ContractBalance": "154817", "test_v3Swap_exactInput_MultiHop": "243354", "test_v3Swap_exactOutput0For1": "150606", "test_v3Swap_exactOutput_MultiHop": "254284" diff --git a/snapshots/V3ToInfinityMigrationNativeTest.json b/snapshots/V3ToInfinityMigrationNativeTest.json index a458c3f..5fd5e00 100644 --- a/snapshots/V3ToInfinityMigrationNativeTest.json +++ b/snapshots/V3ToInfinityMigrationNativeTest.json @@ -1,4 +1,4 @@ { - "test_infiBinPositionmanager_BinAddLiquidity_Native": "543857", - "test_infiCLPositionmanager_Mint_Native": "533474" + "test_infiBinPositionmanager_BinAddLiquidity_Native": "544766", + "test_infiCLPositionmanager_Mint_Native": "533949" } \ No newline at end of file diff --git a/snapshots/V3ToInfinityMigrationTest.json b/snapshots/V3ToInfinityMigrationTest.json index 6af1ec8..e54f24d 100644 --- a/snapshots/V3ToInfinityMigrationTest.json +++ b/snapshots/V3ToInfinityMigrationTest.json @@ -1,5 +1,5 @@ { - "test_infiBinPositionmanager_BinAddLiquidity": "591530", - "test_infiCLPositionmanager_Mint": "581171", + "test_infiBinPositionmanager_BinAddLiquidity": "592472", + "test_infiCLPositionmanager_Mint": "581676", "test_v3PositionManager_burn": "286990" } \ No newline at end of file diff --git a/src/modules/pancakeswap/StableSwapRouter.sol b/src/modules/pancakeswap/StableSwapRouter.sol index 846c3d2..6e04d03 100644 --- a/src/modules/pancakeswap/StableSwapRouter.sol +++ b/src/modules/pancakeswap/StableSwapRouter.sol @@ -45,16 +45,25 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable emit SetStableSwap(stableSwapFactory, stableSwapInfo); } - function _stableSwap(address[] calldata path, uint256[] calldata flag) private { + function _stableSwap(address[] calldata path, uint256[] calldata flag, uint256 amountIn) private { unchecked { if (path.length - 1 != flag.length) revert StableInvalidPath(); for (uint256 i; i < flag.length; i++) { (address input, address output) = (path[i], path[i + 1]); + + ERC20 tokenOut = ERC20(path[i+ 1]); + uint256 balanceBefore = tokenOut.balanceOf(address(this)); + (uint256 k, uint256 j, address swapContract) = stableSwapFactory.getStableInfo(input, output, flag[i]); - uint256 amountIn = ERC20(input).balanceOf(address(this)); ERC20(input).safeApprove(swapContract, amountIn); IStableSwap(swapContract).exchange(k, j, amountIn, 0); + + if (i != flag.length - 1) { + // not last swap, we need to update amountIn for the next hop + // we do this as swapContract do not return the output amount + amountIn = tokenOut.balanceOf(address(this)) - balanceBefore; + } } } } @@ -74,14 +83,16 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable uint256[] calldata flag, address payer ) internal { - if (amountIn != Constants.ALREADY_PAID && amountIn != ActionConstants.CONTRACT_BALANCE) { + if (amountIn != ActionConstants.CONTRACT_BALANCE) { payOrPermit2Transfer(path[0], payer, address(this), amountIn); + } else { + amountIn = ERC20(path[0]).balanceOf(address(this)); } ERC20 tokenOut = ERC20(path[path.length - 1]); uint256 balanceBefore = tokenOut.balanceOf(address(this)); - _stableSwap(path, flag); + _stableSwap(path, flag, amountIn); uint256 amountOut = tokenOut.balanceOf(address(this)) - balanceBefore; if (amountOut < amountOutMinimum) revert StableTooLittleReceived(); @@ -106,7 +117,7 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable ) internal { payOrPermit2Transfer(path[0], payer, address(this), amountIn); - _stableSwap(path, flag); + _stableSwap(path, flag, amountIn); if (recipient != address(this)) pay(path[path.length - 1], recipient, amountOut); } diff --git a/test/stableSwap/StableSwap.t.sol b/test/stableSwap/StableSwap.t.sol index 5db1f35..95eb8a3 100644 --- a/test/stableSwap/StableSwap.t.sol +++ b/test/stableSwap/StableSwap.t.sol @@ -142,6 +142,26 @@ abstract contract StableSwapTest is Test { assertGt(ERC20(token0()).balanceOf(FROM), BALANCE); } + function test_stableSwap_ExactInput0For1_Twice_FromRouter() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + deal(token0(), address(router), AMOUNT * 2); + + address[] memory path = new address[](2); + path[0] = token0(); + path[1] = token1(); + + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) + // recipient, amountIn, amountOutMin, path, flag, payerIsUser + bytes[] memory inputs = new bytes[](2); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag(), false); + inputs[1] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag(), false); + + router.execute(commands, inputs); + assertEq(ERC20(token0()).balanceOf(FROM), BALANCE); // no token0 taken from user, taken from router + assertGt(ERC20(token1()).balanceOf(FROM), BALANCE); // token1 received + } + + function test_stableSwap_exactInput0For1FromRouter() public { bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); deal(token0(), address(router), AMOUNT); @@ -251,6 +271,24 @@ abstract contract StableSwapTest is Test { assertEq(ERC20(token1()).balanceOf(FROM), BALANCE); // no token1 taken from user, taken from router } + function test_stableSwap_exactOutput1For0FromRouter_Twice_FromRouter() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT))); + deal(token1(), address(router), BALANCE * 2); + + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) + address[] memory path = new address[](2); + path[0] = token1(); + path[1] = token0(); + + bytes[] memory inputs = new bytes[](2); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, type(uint256).max, path, flag(), false); + inputs[1] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, type(uint256).max, path, flag(), false); + + router.execute(commands, inputs); + assertGe(ERC20(token0()).balanceOf(FROM), BALANCE + AMOUNT * 2); + assertEq(ERC20(token1()).balanceOf(FROM), BALANCE); // no token1 taken from user, taken from router + } + function token0() internal virtual returns (address); function token1() internal virtual returns (address); function flag() internal virtual returns (uint256[] memory); From dd7bddc00cc749ffebcf40552a1c3c2c7afbaa70 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Tue, 3 Jun 2025 22:23:32 +0800 Subject: [PATCH 02/16] lin and revert some change --- snapshots/BinNativePancakeSwapInfinityTest.json | 6 +++--- snapshots/BinPancakeSwapInfinityTest.json | 12 ++++++------ snapshots/CLNativePancakeSwapInfinityTest.json | 4 ++-- snapshots/CLPancakeSwapInfinityTest.json | 12 ++++++------ snapshots/StableSwapBusdUsdcTest.json | 4 ++-- snapshots/UniversalRouterTest.json | 2 +- snapshots/V3BnbCake.json | 10 +++++----- src/modules/pancakeswap/StableSwapRouter.sol | 8 +++++--- test/stableSwap/StableSwap.t.sol | 8 +++++--- 9 files changed, 35 insertions(+), 31 deletions(-) diff --git a/snapshots/BinNativePancakeSwapInfinityTest.json b/snapshots/BinNativePancakeSwapInfinityTest.json index 01dca82..fcf88a6 100644 --- a/snapshots/BinNativePancakeSwapInfinityTest.json +++ b/snapshots/BinNativePancakeSwapInfinityTest.json @@ -1,6 +1,6 @@ { - "test_infiBinSwap_ExactInSingle_NativeIn": "129700", - "test_infiBinSwap_ExactInSingle_NativeOut": "118428", - "test_infiBinSwap_ExactInSingle_NativeOut_RouterRecipient": "118717", + "test_infiBinSwap_ExactInSingle_NativeIn": "129706", + "test_infiBinSwap_ExactInSingle_NativeOut": "118434", + "test_infiBinSwap_ExactInSingle_NativeOut_RouterRecipient": "118729", "test_infiBinSwap_infiInitializeBinPool": "132846" } \ No newline at end of file diff --git a/snapshots/BinPancakeSwapInfinityTest.json b/snapshots/BinPancakeSwapInfinityTest.json index a2f7404..96db0c2 100644 --- a/snapshots/BinPancakeSwapInfinityTest.json +++ b/snapshots/BinPancakeSwapInfinityTest.json @@ -1,9 +1,9 @@ { - "test_infiBinSwap_ExactInSingle": "144330", - "test_infiBinSwap_ExactIn_MultiHop": "176210", - "test_infiBinSwap_ExactIn_SingleHop": "146128", - "test_infiBinSwap_ExactOutSingle": "148584", - "test_infiBinSwap_ExactOut_MultiHop": "179842", - "test_infiBinSwap_ExactOut_SingleHop": "150392", + "test_infiBinSwap_ExactInSingle": "144336", + "test_infiBinSwap_ExactIn_MultiHop": "176216", + "test_infiBinSwap_ExactIn_SingleHop": "146134", + "test_infiBinSwap_ExactOutSingle": "148590", + "test_infiBinSwap_ExactOut_MultiHop": "179848", + "test_infiBinSwap_ExactOut_SingleHop": "150398", "test_infiBinSwap_InitializeBinPool": "152986" } \ No newline at end of file diff --git a/snapshots/CLNativePancakeSwapInfinityTest.json b/snapshots/CLNativePancakeSwapInfinityTest.json index d3e968b..e613d8a 100644 --- a/snapshots/CLNativePancakeSwapInfinityTest.json +++ b/snapshots/CLNativePancakeSwapInfinityTest.json @@ -1,5 +1,5 @@ { - "test_infiClSwap_ExactInSingle_NativeIn": "162412", - "test_infiClSwap_ExactInSingle_NativeOut": "145154", + "test_infiClSwap_ExactInSingle_NativeIn": "162424", + "test_infiClSwap_ExactInSingle_NativeOut": "145166", "test_infiClSwap_infiInitializeClPool": "133628" } \ No newline at end of file diff --git a/snapshots/CLPancakeSwapInfinityTest.json b/snapshots/CLPancakeSwapInfinityTest.json index 2610903..f10c05b 100644 --- a/snapshots/CLPancakeSwapInfinityTest.json +++ b/snapshots/CLPancakeSwapInfinityTest.json @@ -1,9 +1,9 @@ { - "test_infiClSwap_ExactInSingle": "177042", - "test_infiClSwap_ExactIn_MultiHop": "241645", - "test_infiClSwap_ExactIn_SingleHop": "178833", - "test_infiClSwap_ExactOutSingle": "181362", - "test_infiClSwap_ExactOut_MultiHop": "245413", - "test_infiClSwap_ExactOut_SingleHop": "183165", + "test_infiClSwap_ExactInSingle": "177054", + "test_infiClSwap_ExactIn_MultiHop": "241651", + "test_infiClSwap_ExactIn_SingleHop": "178839", + "test_infiClSwap_ExactOutSingle": "181368", + "test_infiClSwap_ExactOut_MultiHop": "245419", + "test_infiClSwap_ExactOut_SingleHop": "183171", "test_infiClSwap_infiInitializeClPool": "153756" } \ No newline at end of file diff --git a/snapshots/StableSwapBusdUsdcTest.json b/snapshots/StableSwapBusdUsdcTest.json index 902d5e5..3968e26 100644 --- a/snapshots/StableSwapBusdUsdcTest.json +++ b/snapshots/StableSwapBusdUsdcTest.json @@ -1,4 +1,4 @@ { - "test_stableSwap_ExactInput0For1": "193548", - "test_stableSwap_ExactInput1For0": "194840" + "test_stableSwap_ExactInput0For1": "193636", + "test_stableSwap_ExactInput1For0": "194928" } \ No newline at end of file diff --git a/snapshots/UniversalRouterTest.json b/snapshots/UniversalRouterTest.json index 48c552a..3839301 100644 --- a/snapshots/UniversalRouterTest.json +++ b/snapshots/UniversalRouterTest.json @@ -1,4 +1,4 @@ { - "UniversalRouterBytecodeSize": "24291", + "UniversalRouterBytecodeSize": "24051", "test_sweep_token": "55435" } \ No newline at end of file diff --git a/snapshots/V3BnbCake.json b/snapshots/V3BnbCake.json index 55956e3..6e562ac 100644 --- a/snapshots/V3BnbCake.json +++ b/snapshots/V3BnbCake.json @@ -1,7 +1,7 @@ { - "test_v3Swap_ExactInput0For1": "151904", - "test_v3Swap_ExactInput0For1_ContractBalance": "154817", - "test_v3Swap_exactInput_MultiHop": "243354", - "test_v3Swap_exactOutput0For1": "150606", - "test_v3Swap_exactOutput_MultiHop": "254284" + "test_v3Swap_ExactInput0For1": "151922", + "test_v3Swap_ExactInput0For1_ContractBalance": "154835", + "test_v3Swap_exactInput_MultiHop": "243390", + "test_v3Swap_exactOutput0For1": "150624", + "test_v3Swap_exactOutput_MultiHop": "254314" } \ No newline at end of file diff --git a/src/modules/pancakeswap/StableSwapRouter.sol b/src/modules/pancakeswap/StableSwapRouter.sol index 6e04d03..2d703b3 100644 --- a/src/modules/pancakeswap/StableSwapRouter.sol +++ b/src/modules/pancakeswap/StableSwapRouter.sol @@ -52,7 +52,7 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable for (uint256 i; i < flag.length; i++) { (address input, address output) = (path[i], path[i + 1]); - ERC20 tokenOut = ERC20(path[i+ 1]); + ERC20 tokenOut = ERC20(path[i + 1]); uint256 balanceBefore = tokenOut.balanceOf(address(this)); (uint256 k, uint256 j, address swapContract) = stableSwapFactory.getStableInfo(input, output, flag[i]); @@ -83,9 +83,11 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable uint256[] calldata flag, address payer ) internal { - if (amountIn != ActionConstants.CONTRACT_BALANCE) { + if (amountIn != Constants.ALREADY_PAID && amountIn != ActionConstants.CONTRACT_BALANCE) { payOrPermit2Transfer(path[0], payer, address(this), amountIn); - } else { + } + + if (amountIn == ActionConstants.CONTRACT_BALANCE) { amountIn = ERC20(path[0]).balanceOf(address(this)); } diff --git a/test/stableSwap/StableSwap.t.sol b/test/stableSwap/StableSwap.t.sol index 95eb8a3..e06d8da 100644 --- a/test/stableSwap/StableSwap.t.sol +++ b/test/stableSwap/StableSwap.t.sol @@ -143,7 +143,8 @@ abstract contract StableSwapTest is Test { } function test_stableSwap_ExactInput0For1_Twice_FromRouter() public { - bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + bytes memory commands = + abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); deal(token0(), address(router), AMOUNT * 2); address[] memory path = new address[](2); @@ -161,7 +162,6 @@ abstract contract StableSwapTest is Test { assertGt(ERC20(token1()).balanceOf(FROM), BALANCE); // token1 received } - function test_stableSwap_exactInput0For1FromRouter() public { bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); deal(token0(), address(router), AMOUNT); @@ -272,7 +272,9 @@ abstract contract StableSwapTest is Test { } function test_stableSwap_exactOutput1For0FromRouter_Twice_FromRouter() public { - bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT))); + bytes memory commands = abi.encodePacked( + bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT)) + ); deal(token1(), address(router), BALANCE * 2); // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) From 85b9510394244952852c4906db455c06452fea04 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 4 Jun 2025 09:20:57 +0800 Subject: [PATCH 03/16] feat: remove unchecked checks --- snapshots/StableSwapBusdUsdcTest.json | 4 +-- snapshots/UniversalRouterTest.json | 2 +- src/modules/pancakeswap/StableSwapRouter.sol | 30 +++++++++++--------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/snapshots/StableSwapBusdUsdcTest.json b/snapshots/StableSwapBusdUsdcTest.json index 3968e26..db5fe3e 100644 --- a/snapshots/StableSwapBusdUsdcTest.json +++ b/snapshots/StableSwapBusdUsdcTest.json @@ -1,4 +1,4 @@ { - "test_stableSwap_ExactInput0For1": "193636", - "test_stableSwap_ExactInput1For0": "194928" + "test_stableSwap_ExactInput0For1": "192810", + "test_stableSwap_ExactInput1For0": "193490" } \ No newline at end of file diff --git a/snapshots/UniversalRouterTest.json b/snapshots/UniversalRouterTest.json index 3839301..956c1fc 100644 --- a/snapshots/UniversalRouterTest.json +++ b/snapshots/UniversalRouterTest.json @@ -1,4 +1,4 @@ { - "UniversalRouterBytecodeSize": "24051", + "UniversalRouterBytecodeSize": "24127", "test_sweep_token": "55435" } \ No newline at end of file diff --git a/src/modules/pancakeswap/StableSwapRouter.sol b/src/modules/pancakeswap/StableSwapRouter.sol index 2d703b3..634cd80 100644 --- a/src/modules/pancakeswap/StableSwapRouter.sol +++ b/src/modules/pancakeswap/StableSwapRouter.sol @@ -45,25 +45,27 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable emit SetStableSwap(stableSwapFactory, stableSwapInfo); } + /// @dev if a single hop, path would be of size 2, and flag would be of size 1 + /// if 2 hops, path would be of size 3, and flag would be of size 2 function _stableSwap(address[] calldata path, uint256[] calldata flag, uint256 amountIn) private { - unchecked { - if (path.length - 1 != flag.length) revert StableInvalidPath(); + if (path.length - 1 != flag.length) revert StableInvalidPath(); - for (uint256 i; i < flag.length; i++) { - (address input, address output) = (path[i], path[i + 1]); + uint256 balanceBefore; + for (uint256 i; i < flag.length; i++) { + (address input, address output) = (path[i], path[i + 1]); - ERC20 tokenOut = ERC20(path[i + 1]); - uint256 balanceBefore = tokenOut.balanceOf(address(this)); + bool isLastHop = i == flag.length - 1; + if (!isLastHop) { + balanceBefore = ERC20(path[i + 1]).balanceOf(address(this)); + } - (uint256 k, uint256 j, address swapContract) = stableSwapFactory.getStableInfo(input, output, flag[i]); - ERC20(input).safeApprove(swapContract, amountIn); - IStableSwap(swapContract).exchange(k, j, amountIn, 0); + (uint256 k, uint256 j, address swapContract) = stableSwapFactory.getStableInfo(input, output, flag[i]); + ERC20(input).safeApprove(swapContract, amountIn); + IStableSwap(swapContract).exchange(k, j, amountIn, 0); - if (i != flag.length - 1) { - // not last swap, we need to update amountIn for the next hop - // we do this as swapContract do not return the output amount - amountIn = tokenOut.balanceOf(address(this)) - balanceBefore; - } + if (!isLastHop) { + // if not last swap, update amountIn for the next hop. this is done as swapContract do not return the output amount + amountIn = ERC20(path[i + 1]).balanceOf(address(this)) - balanceBefore; } } } From e63efc7dd8f628f2414ea8a25dac10f5499343ea Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 4 Jun 2025 09:49:55 +0800 Subject: [PATCH 04/16] feat: add more multi-hop tests --- snapshots/StableSwapMultiHop.json | 8 + test/stableSwap/StableSwap.t.sol | 30 +-- test/stableSwap/StableSwap_MultiHop.t.sol | 224 ++++++++++++++++++++++ 3 files changed, 247 insertions(+), 15 deletions(-) create mode 100644 snapshots/StableSwapMultiHop.json create mode 100644 test/stableSwap/StableSwap_MultiHop.t.sol diff --git a/snapshots/StableSwapMultiHop.json b/snapshots/StableSwapMultiHop.json new file mode 100644 index 0000000..942e4e2 --- /dev/null +++ b/snapshots/StableSwapMultiHop.json @@ -0,0 +1,8 @@ +{ + "test_stableSwap_ExactInput0For1_DualAction_FromRouter": "284914", + "test_stableSwap_ExactInput0For1_DualAction_FromUser": "311144", + "test_stableSwap_ExactInput0For1_MultiHop_FromRouter": "281742", + "test_stableSwap_ExactInput0For1_MultiHop_FromUser": "307972", + "test_stableSwap_ExactInput0For1_SamePath_FromRouter": "246522", + "test_stableSwap_ExactInput0For1_SamePath_FromUser": "292183" +} \ No newline at end of file diff --git a/test/stableSwap/StableSwap.t.sol b/test/stableSwap/StableSwap.t.sol index e06d8da..4db50ac 100644 --- a/test/stableSwap/StableSwap.t.sol +++ b/test/stableSwap/StableSwap.t.sol @@ -142,35 +142,35 @@ abstract contract StableSwapTest is Test { assertGt(ERC20(token0()).balanceOf(FROM), BALANCE); } - function test_stableSwap_ExactInput0For1_Twice_FromRouter() public { - bytes memory commands = - abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); - deal(token0(), address(router), AMOUNT * 2); - + function test_stableSwap_exactInput0For1FromRouter() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + deal(token0(), address(router), AMOUNT); + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) address[] memory path = new address[](2); path[0] = token0(); path[1] = token1(); - - // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) - // recipient, amountIn, amountOutMin, path, flag, payerIsUser - bytes[] memory inputs = new bytes[](2); + bytes[] memory inputs = new bytes[](1); inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag(), false); - inputs[1] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag(), false); router.execute(commands, inputs); assertEq(ERC20(token0()).balanceOf(FROM), BALANCE); // no token0 taken from user, taken from router assertGt(ERC20(token1()).balanceOf(FROM), BALANCE); // token1 received } - function test_stableSwap_exactInput0For1FromRouter() public { - bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); - deal(token0(), address(router), AMOUNT); - // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) + function test_stableSwap_ExactInput0For1_Twice_FromRouter() public { + bytes memory commands = + abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + deal(token0(), address(router), AMOUNT * 2); + address[] memory path = new address[](2); path[0] = token0(); path[1] = token1(); - bytes[] memory inputs = new bytes[](1); + + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) + // recipient, amountIn, amountOutMin, path, flag, payerIsUser + bytes[] memory inputs = new bytes[](2); inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag(), false); + inputs[1] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag(), false); router.execute(commands, inputs); assertEq(ERC20(token0()).balanceOf(FROM), BALANCE); // no token0 taken from user, taken from router diff --git a/test/stableSwap/StableSwap_MultiHop.t.sol b/test/stableSwap/StableSwap_MultiHop.t.sol new file mode 100644 index 0000000..abe6d73 --- /dev/null +++ b/test/stableSwap/StableSwap_MultiHop.t.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ActionConstants} from "infinity-periphery/src/libraries/ActionConstants.sol"; + +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; +import {UniversalRouter} from "../../src/UniversalRouter.sol"; +import {Commands} from "../../src/libraries/Commands.sol"; +import {RouterParameters} from "../../src/base/RouterImmutables.sol"; +import {IStableSwapFactory} from "../../src/interfaces/IStableSwapFactory.sol"; +import {IStableSwapInfo} from "../../src/interfaces/IStableSwapInfo.sol"; +import {StableSwapRouter} from "../../src/modules/pancakeswap/StableSwapRouter.sol"; + +contract StableSwapMultiHop is Test { + ERC20 constant USDC = ERC20(0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d); + ERC20 constant BUSD = ERC20(0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56); + ERC20 constant USDT = ERC20(0x55d398326f99059fF775485246999027B3197955); + + uint256 constant AMOUNT = 1 ether; + uint256 constant BALANCE = 100000 ether; + ERC20 constant WETH9 = ERC20(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c); + IPermit2 constant PERMIT2 = IPermit2(0x31c2F6fcFf4F8759b3Bd5Bf0e1084A055615c768); + address constant FROM = address(1234); + + /// @dev Address found from smart router via https://bscscan.com/address/0x13f4EA83D0bd40E75C8222255bc855a974568Dd4#readContract + /// @dev StableInfo refers to PancakeStableSwapTwoPoolInfo, threePoolInfo is not present as its not used in PCS + IStableSwapFactory STABLE_FACTORY = IStableSwapFactory(0x25a55f9f2279A54951133D503490342b50E5cd15); + IStableSwapInfo STABLE_INFO = IStableSwapInfo(0x150c8AbEB487137acCC541925408e73b92F39A50); + + UniversalRouter public router; + + function setUp() public { + // BSC: Jun-04-2025 01:23:02 AM +UTC) + vm.createSelectFork(vm.envString("FORK_URL"), 50837520); + + RouterParameters memory params = RouterParameters({ + permit2: address(PERMIT2), + weth9: address(WETH9), + v2Factory: address(0), + v3Factory: address(0), + v3Deployer: address(0), + v2InitCodeHash: bytes32(0), + v3InitCodeHash: bytes32(0), + stableFactory: address(STABLE_FACTORY), + stableInfo: address(STABLE_INFO), + infiVault: address(0), + infiClPoolManager: address(0), + infiBinPoolManager: address(0), + v3NFTPositionManager: address(0), + infiClPositionManager: address(0), + infiBinPositionManager: address(0) + }); + router = new UniversalRouter(params); + + // verify USDC<>USDT pair and USDT<>BUSD pair exist + if (STABLE_FACTORY.getPairInfo(address(USDC), address(USDT)).swapContract == address(0)) { + revert("Pair doesn't exist"); + } + if (STABLE_FACTORY.getPairInfo(address(USDT), address(BUSD)).swapContract == address(0)) { + revert("Pair doesn't exist"); + } + + vm.startPrank(FROM); + deal(FROM, BALANCE); + deal(address(USDC), FROM, BALANCE); + deal(address(BUSD), FROM, BALANCE); + deal(address(USDT), FROM, BALANCE); + ERC20(address(USDC)).approve(address(PERMIT2), type(uint256).max); + ERC20(address(BUSD)).approve(address(PERMIT2), type(uint256).max); + ERC20(address(USDT)).approve(address(PERMIT2), type(uint256).max); + PERMIT2.approve(address(USDC), address(router), type(uint160).max, type(uint48).max); + PERMIT2.approve(address(BUSD), address(router), type(uint160).max, type(uint48).max); + PERMIT2.approve(address(USDT), address(router), type(uint160).max, type(uint48).max); + } + + /// do a multi-hop swap from USDC -> USDT -> BUSD and assume balance in contract + function test_stableSwap_ExactInput0For1_MultiHop_FromRouter() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + + deal(address(USDC), address(router), AMOUNT); + + uint256[] memory flag = new uint256[](2); + flag[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + flag[1] = 2; // 2 is the flag to indicate StableSwapTwoPool + + address[] memory path = new address[](3); + path[0] = address(USDC); + path[1] = address(USDT); + path[2] = address(BUSD); + + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag, false); + + router.execute(commands, inputs); + vm.snapshotGasLastCall("test_stableSwap_ExactInput0For1_MultiHop_FromRouter"); + assertEq(ERC20(address(USDC)).balanceOf(FROM), BALANCE); // no token0 taken from user, taken from router + assertGt(ERC20(address(BUSD)).balanceOf(FROM), BALANCE); // token1 received. + assertEq(ERC20(address(BUSD)).balanceOf(FROM), 100000999768181033551138); // roughly 0.999768181 recieved from swap + } + + function test_stableSwap_ExactInput0For1_MultiHop_FromUser() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + + uint256[] memory flag = new uint256[](2); + flag[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + flag[1] = 2; // 2 is the flag to indicate StableSwapTwoPool + + address[] memory path = new address[](3); + path[0] = address(USDC); + path[1] = address(USDT); + path[2] = address(BUSD); + + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag, true); + + router.execute(commands, inputs); + vm.snapshotGasLastCall("test_stableSwap_ExactInput0For1_MultiHop_FromUser"); + assertEq(ERC20(address(USDC)).balanceOf(FROM), BALANCE - AMOUNT); + assertEq(ERC20(address(BUSD)).balanceOf(FROM), 100000999768181033551138); // roughly 0.999768181 recieved from swap + } + + function test_stableSwap_ExactInput0For1_DualAction_FromRouter() public { + bytes memory commands = + abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + + deal(address(USDC), address(router), AMOUNT); + + uint256[] memory flag = new uint256[](1); + flag[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + + address[] memory path1 = new address[](2); + path1[0] = address(USDC); + path1[1] = address(USDT); + address[] memory path2 = new address[](2); + path2[0] = address(USDT); + path2[1] = address(BUSD); + + bytes[] memory inputs = new bytes[](2); + // first hop output to universal router + inputs[0] = abi.encode(ActionConstants.ADDRESS_THIS, AMOUNT, 0, path1, flag, false); + inputs[1] = abi.encode(ActionConstants.MSG_SENDER, ActionConstants.CONTRACT_BALANCE, 0, path2, flag, false); + + router.execute(commands, inputs); + vm.snapshotGasLastCall("test_stableSwap_ExactInput0For1_DualAction_FromRouter"); + assertEq(ERC20(address(USDC)).balanceOf(FROM), BALANCE); // no token0 taken from user, taken from router + assertGt(ERC20(address(BUSD)).balanceOf(FROM), BALANCE); // token1 received. roughly 0.999768181 + assertEq(ERC20(address(BUSD)).balanceOf(FROM), 100000999768181033551138); // roughly 0.999768181 recieved from swap + } + + function test_stableSwap_ExactInput0For1_DualAction_FromUser() public { + bytes memory commands = + abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + + uint256[] memory flag = new uint256[](1); + flag[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + + address[] memory path1 = new address[](2); + path1[0] = address(USDC); + path1[1] = address(USDT); + address[] memory path2 = new address[](2); + path2[0] = address(USDT); + path2[1] = address(BUSD); + + bytes[] memory inputs = new bytes[](2); + // first hop output to universal router + inputs[0] = abi.encode(ActionConstants.ADDRESS_THIS, AMOUNT, 0, path1, flag, true); + inputs[1] = abi.encode(ActionConstants.MSG_SENDER, ActionConstants.CONTRACT_BALANCE, 0, path2, flag, false); + + router.execute(commands, inputs); + vm.snapshotGasLastCall("test_stableSwap_ExactInput0For1_DualAction_FromUser"); + assertEq(ERC20(address(USDC)).balanceOf(FROM), BALANCE - AMOUNT); + assertEq(ERC20(address(BUSD)).balanceOf(FROM), 100000999768181033551138); // roughly 0.999768181 recieved from swap + } + + function test_stableSwap_ExactInput0For1_SamePath_FromRouter() public { + bytes memory commands = + abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + + deal(address(USDC), address(router), AMOUNT * 4); + + uint256[] memory flag = new uint256[](1); + flag[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + + address[] memory path = new address[](2); + path[0] = address(USDC); + path[1] = address(USDT); + + bytes[] memory inputs = new bytes[](2); + // first hop output to universal router + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag, false); + inputs[1] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT * 3, 0, path, flag, false); + + router.execute(commands, inputs); + vm.snapshotGasLastCall("test_stableSwap_ExactInput0For1_SamePath_FromRouter"); + assertEq(ERC20(address(USDC)).balanceOf(FROM), BALANCE); // no token0 taken from user, taken from router + assertEq(ERC20(address(USDT)).balanceOf(FROM), 100003996633124466009871); // roughly 3.9966331244 recieved from swap + } + + function test_stableSwap_ExactInput0For1_SamePath_FromUser() public { + bytes memory commands = + abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + + uint256[] memory flag = new uint256[](1); + flag[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + + address[] memory path = new address[](2); + path[0] = address(USDC); + path[1] = address(USDT); + + bytes[] memory inputs = new bytes[](2); + // first hop output to universal router + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag, true); + inputs[1] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT * 3, 0, path, flag, true); + + router.execute(commands, inputs); + vm.snapshotGasLastCall("test_stableSwap_ExactInput0For1_SamePath_FromUser"); + assertEq(ERC20(address(USDC)).balanceOf(FROM), BALANCE - (AMOUNT * 4)); + assertEq(ERC20(address(USDT)).balanceOf(FROM), 100003996633124466009871); // roughly 3.9966331244 recieved from swap + } +} From ea3215482d07dcbd1eb12e6acec0a7874e2fdb63 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:16:26 +0800 Subject: [PATCH 05/16] test: add exact output test --- snapshots/StableSwapMultiHop.json | 5 +- test/stableSwap/StableSwap_MultiHop.t.sol | 76 +++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/snapshots/StableSwapMultiHop.json b/snapshots/StableSwapMultiHop.json index 942e4e2..7a2af09 100644 --- a/snapshots/StableSwapMultiHop.json +++ b/snapshots/StableSwapMultiHop.json @@ -4,5 +4,8 @@ "test_stableSwap_ExactInput0For1_MultiHop_FromRouter": "281742", "test_stableSwap_ExactInput0For1_MultiHop_FromUser": "307972", "test_stableSwap_ExactInput0For1_SamePath_FromRouter": "246522", - "test_stableSwap_ExactInput0For1_SamePath_FromUser": "292183" + "test_stableSwap_ExactInput0For1_SamePath_FromUser": "292183", + "test_stableSwap_ExactOut0For1_MultiHop_FromUser": "354076", + "test_stableSwap_ExactOutput0For1_MultiHop_FromRouter": "327846", + "test_stableSwap_ExactOutput0For1_SamePath_FromRouter": "291659" } \ No newline at end of file diff --git a/test/stableSwap/StableSwap_MultiHop.t.sol b/test/stableSwap/StableSwap_MultiHop.t.sol index abe6d73..ed064f3 100644 --- a/test/stableSwap/StableSwap_MultiHop.t.sol +++ b/test/stableSwap/StableSwap_MultiHop.t.sol @@ -221,4 +221,80 @@ contract StableSwapMultiHop is Test { assertEq(ERC20(address(USDC)).balanceOf(FROM), BALANCE - (AMOUNT * 4)); assertEq(ERC20(address(USDT)).balanceOf(FROM), 100003996633124466009871); // roughly 3.9966331244 recieved from swap } + + function test_stableSwap_ExactOutput0For1_SamePath_FromRouter() public { + bytes memory commands = + abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT))); + + uint256 AMOUNT_IN = 1 ether; + deal(address(USDC), address(router), AMOUNT_IN * 4); + + uint256[] memory flag = new uint256[](1); + flag[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + + address[] memory path = new address[](2); + path[0] = address(USDC); + path[1] = address(USDT); + + bytes[] memory inputs = new bytes[](2); + // (recipient, amountOut, amountInMax, path, flag, payerIsUser) + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, 0.9 ether, AMOUNT_IN, path, flag, false); + inputs[1] = abi.encode(ActionConstants.MSG_SENDER, 0.9 ether * 3, AMOUNT_IN * 3, path, flag, false); + + router.execute(commands, inputs); + vm.snapshotGasLastCall("test_stableSwap_ExactOutput0For1_SamePath_FromRouter"); + assertEq(ERC20(address(USDT)).balanceOf(FROM), 100003600000000000000000); // exactly 3.6 usdt received + } + + function test_stableSwap_ExactOutput0For1_MultiHop_FromRouter() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT))); + + uint256 AMOUNT_IN = 1 ether; + deal(address(USDC), address(router), AMOUNT_IN); + + uint256[] memory flag = new uint256[](2); + flag[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + flag[1] = 2; // 2 is the flag to indicate StableSwapTwoPool + + address[] memory path = new address[](3); + path[0] = address(USDC); + path[1] = address(USDT); + path[2] = address(BUSD); + + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) + bytes[] memory inputs = new bytes[](1); + // (recipient, amountOut, amountInMax, path, flag, payerIsUser) + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, 0.9 ether, AMOUNT_IN, path, flag, false); + + router.execute(commands, inputs); + vm.snapshotGasLastCall("test_stableSwap_ExactOutput0For1_MultiHop_FromRouter"); + assertEq(ERC20(address(USDC)).balanceOf(FROM), BALANCE); // no token0 taken from user, taken from router + assertEq(ERC20(address(USDC)).balanceOf(address(router)), 99791314908871029); // roughly 0.9 usdc taken from router, leaving 0.1 left + assertEq(ERC20(address(BUSD)).balanceOf(FROM), 100000900000000000000000); // exactly 0.9 busd received + } + + function test_stableSwap_ExactOut0For1_MultiHop_FromUser() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT))); + + uint256 AMOUNT_IN = 1 ether; + + uint256[] memory flag = new uint256[](2); + flag[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + flag[1] = 2; // 2 is the flag to indicate StableSwapTwoPool + + address[] memory path = new address[](3); + path[0] = address(USDC); + path[1] = address(USDT); + path[2] = address(BUSD); + + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) + bytes[] memory inputs = new bytes[](1); + // (recipient, amountOut, amountInMax, path, flag, payerIsUser) + inputs[0] = abi.encode(ActionConstants.MSG_SENDER,0.9 ether, AMOUNT_IN, path, flag, true); + + router.execute(commands, inputs); + vm.snapshotGasLastCall("test_stableSwap_ExactOut0For1_MultiHop_FromUser"); + assertEq(ERC20(address(USDC)).balanceOf(FROM), 99999099791314908871029); // roughly 0.9 usdc taken from user + assertEq(ERC20(address(BUSD)).balanceOf(FROM), 100000900000000000000000); // exactly 0.9 busd received + } } From f40115e47dfa38daba0d5d3d1c744f5f451bdadc Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:17:15 +0800 Subject: [PATCH 06/16] forge fmt --- test/stableSwap/StableSwap_MultiHop.t.sol | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/stableSwap/StableSwap_MultiHop.t.sol b/test/stableSwap/StableSwap_MultiHop.t.sol index ed064f3..54052ff 100644 --- a/test/stableSwap/StableSwap_MultiHop.t.sol +++ b/test/stableSwap/StableSwap_MultiHop.t.sol @@ -223,8 +223,9 @@ contract StableSwapMultiHop is Test { } function test_stableSwap_ExactOutput0For1_SamePath_FromRouter() public { - bytes memory commands = - abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT))); + bytes memory commands = abi.encodePacked( + bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_OUT)) + ); uint256 AMOUNT_IN = 1 ether; deal(address(USDC), address(router), AMOUNT_IN * 4); @@ -243,7 +244,7 @@ contract StableSwapMultiHop is Test { router.execute(commands, inputs); vm.snapshotGasLastCall("test_stableSwap_ExactOutput0For1_SamePath_FromRouter"); - assertEq(ERC20(address(USDT)).balanceOf(FROM), 100003600000000000000000); // exactly 3.6 usdt received + assertEq(ERC20(address(USDT)).balanceOf(FROM), 100003600000000000000000); // exactly 3.6 usdt received } function test_stableSwap_ExactOutput0For1_MultiHop_FromRouter() public { @@ -290,11 +291,11 @@ contract StableSwapMultiHop is Test { // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) bytes[] memory inputs = new bytes[](1); // (recipient, amountOut, amountInMax, path, flag, payerIsUser) - inputs[0] = abi.encode(ActionConstants.MSG_SENDER,0.9 ether, AMOUNT_IN, path, flag, true); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, 0.9 ether, AMOUNT_IN, path, flag, true); router.execute(commands, inputs); vm.snapshotGasLastCall("test_stableSwap_ExactOut0For1_MultiHop_FromUser"); - assertEq(ERC20(address(USDC)).balanceOf(FROM), 99999099791314908871029); // roughly 0.9 usdc taken from user + assertEq(ERC20(address(USDC)).balanceOf(FROM), 99999099791314908871029); // roughly 0.9 usdc taken from user assertEq(ERC20(address(BUSD)).balanceOf(FROM), 100000900000000000000000); // exactly 0.9 busd received } } From 0c0e083bc07a1d3799b284a1de90e3bfe646fa30 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:46:39 +0800 Subject: [PATCH 07/16] refactor: refactor variable --- src/modules/pancakeswap/StableSwapRouter.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/pancakeswap/StableSwapRouter.sol b/src/modules/pancakeswap/StableSwapRouter.sol index 634cd80..93cd0ea 100644 --- a/src/modules/pancakeswap/StableSwapRouter.sol +++ b/src/modules/pancakeswap/StableSwapRouter.sol @@ -50,13 +50,13 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable function _stableSwap(address[] calldata path, uint256[] calldata flag, uint256 amountIn) private { if (path.length - 1 != flag.length) revert StableInvalidPath(); - uint256 balanceBefore; + uint256 outputTokenBal; for (uint256 i; i < flag.length; i++) { (address input, address output) = (path[i], path[i + 1]); bool isLastHop = i == flag.length - 1; if (!isLastHop) { - balanceBefore = ERC20(path[i + 1]).balanceOf(address(this)); + outputTokenBal = ERC20(path[i + 1]).balanceOf(address(this)); } (uint256 k, uint256 j, address swapContract) = stableSwapFactory.getStableInfo(input, output, flag[i]); @@ -65,7 +65,7 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable if (!isLastHop) { // if not last swap, update amountIn for the next hop. this is done as swapContract do not return the output amount - amountIn = ERC20(path[i + 1]).balanceOf(address(this)) - balanceBefore; + amountIn = ERC20(path[i + 1]).balanceOf(address(this)) - outputTokenBal; } } } From 29231508c6afe4a6b7ca7956acef5a159a9846a4 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:51:54 +0800 Subject: [PATCH 08/16] refactor: better variable name --- snapshots/StableSwapBusdUsdcTest.json | 4 ++-- snapshots/StableSwapMultiHop.json | 18 +++++++++--------- snapshots/UniversalRouterTest.json | 2 +- src/modules/pancakeswap/StableSwapRouter.sol | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/snapshots/StableSwapBusdUsdcTest.json b/snapshots/StableSwapBusdUsdcTest.json index db5fe3e..8bc0806 100644 --- a/snapshots/StableSwapBusdUsdcTest.json +++ b/snapshots/StableSwapBusdUsdcTest.json @@ -1,4 +1,4 @@ { - "test_stableSwap_ExactInput0For1": "192810", - "test_stableSwap_ExactInput1For0": "193490" + "test_stableSwap_ExactInput0For1": "192806", + "test_stableSwap_ExactInput1For0": "193486" } \ No newline at end of file diff --git a/snapshots/StableSwapMultiHop.json b/snapshots/StableSwapMultiHop.json index 7a2af09..226b23e 100644 --- a/snapshots/StableSwapMultiHop.json +++ b/snapshots/StableSwapMultiHop.json @@ -1,11 +1,11 @@ { - "test_stableSwap_ExactInput0For1_DualAction_FromRouter": "284914", - "test_stableSwap_ExactInput0For1_DualAction_FromUser": "311144", - "test_stableSwap_ExactInput0For1_MultiHop_FromRouter": "281742", - "test_stableSwap_ExactInput0For1_MultiHop_FromUser": "307972", - "test_stableSwap_ExactInput0For1_SamePath_FromRouter": "246522", - "test_stableSwap_ExactInput0For1_SamePath_FromUser": "292183", - "test_stableSwap_ExactOut0For1_MultiHop_FromUser": "354076", - "test_stableSwap_ExactOutput0For1_MultiHop_FromRouter": "327846", - "test_stableSwap_ExactOutput0For1_SamePath_FromRouter": "291659" + "test_stableSwap_ExactInput0For1_DualAction_FromRouter": "284906", + "test_stableSwap_ExactInput0For1_DualAction_FromUser": "311136", + "test_stableSwap_ExactInput0For1_MultiHop_FromRouter": "281493", + "test_stableSwap_ExactInput0For1_MultiHop_FromUser": "307724", + "test_stableSwap_ExactInput0For1_SamePath_FromRouter": "246514", + "test_stableSwap_ExactInput0For1_SamePath_FromUser": "292175", + "test_stableSwap_ExactOut0For1_MultiHop_FromUser": "353828", + "test_stableSwap_ExactOutput0For1_MultiHop_FromRouter": "327597", + "test_stableSwap_ExactOutput0For1_SamePath_FromRouter": "291651" } \ No newline at end of file diff --git a/snapshots/UniversalRouterTest.json b/snapshots/UniversalRouterTest.json index 956c1fc..d007bf0 100644 --- a/snapshots/UniversalRouterTest.json +++ b/snapshots/UniversalRouterTest.json @@ -1,4 +1,4 @@ { - "UniversalRouterBytecodeSize": "24127", + "UniversalRouterBytecodeSize": "24093", "test_sweep_token": "55435" } \ No newline at end of file diff --git a/src/modules/pancakeswap/StableSwapRouter.sol b/src/modules/pancakeswap/StableSwapRouter.sol index 93cd0ea..54391e0 100644 --- a/src/modules/pancakeswap/StableSwapRouter.sol +++ b/src/modules/pancakeswap/StableSwapRouter.sol @@ -56,7 +56,7 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable bool isLastHop = i == flag.length - 1; if (!isLastHop) { - outputTokenBal = ERC20(path[i + 1]).balanceOf(address(this)); + outputTokenBal = ERC20(output).balanceOf(address(this)); } (uint256 k, uint256 j, address swapContract) = stableSwapFactory.getStableInfo(input, output, flag[i]); @@ -65,7 +65,7 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable if (!isLastHop) { // if not last swap, update amountIn for the next hop. this is done as swapContract do not return the output amount - amountIn = ERC20(path[i + 1]).balanceOf(address(this)) - outputTokenBal; + amountIn = ERC20(output).balanceOf(address(this)) - outputTokenBal; } } } From 85c13b4930d4261a36cb014e31faae24a86114c9 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 4 Jun 2025 11:01:40 +0800 Subject: [PATCH 09/16] feat: update implementation --- foundry.toml | 2 +- .../BinNativePancakeSwapInfinityTest.json | 6 ++--- snapshots/BinPancakeSwapInfinityTest.json | 12 +++++----- .../CLNativePancakeSwapInfinityTest.json | 4 ++-- snapshots/CLPancakeSwapInfinityTest.json | 12 +++++----- snapshots/StableSwapBusdUsdcTest.json | 4 ++-- snapshots/StableSwapMultiHop.json | 16 ++++++------- snapshots/UniversalRouterTest.json | 4 ++-- snapshots/V2BnbCake.json | 4 ++-- snapshots/V2MockBnb.json | 4 ++-- snapshots/V3BnbCake.json | 10 ++++---- .../V3ToInfinityMigrationNativeTest.json | 4 ++-- snapshots/V3ToInfinityMigrationTest.json | 4 ++-- src/modules/pancakeswap/StableSwapRouter.sol | 24 ++++++++----------- 14 files changed, 53 insertions(+), 57 deletions(-) diff --git a/foundry.toml b/foundry.toml index 1f9c23b..120d514 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ src = "src" out = 'foundry-out' libs = ["lib"] via_ir = true -optimizer_runs = 10_000 +optimizer_runs = 20_000 ffi = true fs_permissions = [ { access = "read-write", path = ".forge-snapshots/" }, diff --git a/snapshots/BinNativePancakeSwapInfinityTest.json b/snapshots/BinNativePancakeSwapInfinityTest.json index fcf88a6..d4f9548 100644 --- a/snapshots/BinNativePancakeSwapInfinityTest.json +++ b/snapshots/BinNativePancakeSwapInfinityTest.json @@ -1,6 +1,6 @@ { - "test_infiBinSwap_ExactInSingle_NativeIn": "129706", - "test_infiBinSwap_ExactInSingle_NativeOut": "118434", - "test_infiBinSwap_ExactInSingle_NativeOut_RouterRecipient": "118729", + "test_infiBinSwap_ExactInSingle_NativeIn": "128929", + "test_infiBinSwap_ExactInSingle_NativeOut": "117627", + "test_infiBinSwap_ExactInSingle_NativeOut_RouterRecipient": "117916", "test_infiBinSwap_infiInitializeBinPool": "132846" } \ No newline at end of file diff --git a/snapshots/BinPancakeSwapInfinityTest.json b/snapshots/BinPancakeSwapInfinityTest.json index 96db0c2..198c77c 100644 --- a/snapshots/BinPancakeSwapInfinityTest.json +++ b/snapshots/BinPancakeSwapInfinityTest.json @@ -1,9 +1,9 @@ { - "test_infiBinSwap_ExactInSingle": "144336", - "test_infiBinSwap_ExactIn_MultiHop": "176216", - "test_infiBinSwap_ExactIn_SingleHop": "146134", - "test_infiBinSwap_ExactOutSingle": "148590", - "test_infiBinSwap_ExactOut_MultiHop": "179848", - "test_infiBinSwap_ExactOut_SingleHop": "150398", + "test_infiBinSwap_ExactInSingle": "143529", + "test_infiBinSwap_ExactIn_MultiHop": "174923", + "test_infiBinSwap_ExactIn_SingleHop": "145327", + "test_infiBinSwap_ExactOutSingle": "147891", + "test_infiBinSwap_ExactOut_MultiHop": "178771", + "test_infiBinSwap_ExactOut_SingleHop": "149699", "test_infiBinSwap_InitializeBinPool": "152986" } \ No newline at end of file diff --git a/snapshots/CLNativePancakeSwapInfinityTest.json b/snapshots/CLNativePancakeSwapInfinityTest.json index e613d8a..a30e2e8 100644 --- a/snapshots/CLNativePancakeSwapInfinityTest.json +++ b/snapshots/CLNativePancakeSwapInfinityTest.json @@ -1,5 +1,5 @@ { - "test_infiClSwap_ExactInSingle_NativeIn": "162424", - "test_infiClSwap_ExactInSingle_NativeOut": "145166", + "test_infiClSwap_ExactInSingle_NativeIn": "161970", + "test_infiClSwap_ExactInSingle_NativeOut": "144706", "test_infiClSwap_infiInitializeClPool": "133628" } \ No newline at end of file diff --git a/snapshots/CLPancakeSwapInfinityTest.json b/snapshots/CLPancakeSwapInfinityTest.json index f10c05b..71b9b15 100644 --- a/snapshots/CLPancakeSwapInfinityTest.json +++ b/snapshots/CLPancakeSwapInfinityTest.json @@ -1,9 +1,9 @@ { - "test_infiClSwap_ExactInSingle": "177054", - "test_infiClSwap_ExactIn_MultiHop": "241651", - "test_infiClSwap_ExactIn_SingleHop": "178839", - "test_infiClSwap_ExactOutSingle": "181368", - "test_infiClSwap_ExactOut_MultiHop": "245419", - "test_infiClSwap_ExactOut_SingleHop": "183171", + "test_infiClSwap_ExactInSingle": "176570", + "test_infiClSwap_ExactIn_MultiHop": "241016", + "test_infiClSwap_ExactIn_SingleHop": "178361", + "test_infiClSwap_ExactOutSingle": "180894", + "test_infiClSwap_ExactOut_MultiHop": "244792", + "test_infiClSwap_ExactOut_SingleHop": "182697", "test_infiClSwap_infiInitializeClPool": "153756" } \ No newline at end of file diff --git a/snapshots/StableSwapBusdUsdcTest.json b/snapshots/StableSwapBusdUsdcTest.json index 8bc0806..1468b85 100644 --- a/snapshots/StableSwapBusdUsdcTest.json +++ b/snapshots/StableSwapBusdUsdcTest.json @@ -1,4 +1,4 @@ { - "test_stableSwap_ExactInput0For1": "192806", - "test_stableSwap_ExactInput1For0": "193486" + "test_stableSwap_ExactInput0For1": "192811", + "test_stableSwap_ExactInput1For0": "193491" } \ No newline at end of file diff --git a/snapshots/StableSwapMultiHop.json b/snapshots/StableSwapMultiHop.json index 226b23e..806bc29 100644 --- a/snapshots/StableSwapMultiHop.json +++ b/snapshots/StableSwapMultiHop.json @@ -1,11 +1,11 @@ { - "test_stableSwap_ExactInput0For1_DualAction_FromRouter": "284906", - "test_stableSwap_ExactInput0For1_DualAction_FromUser": "311136", - "test_stableSwap_ExactInput0For1_MultiHop_FromRouter": "281493", - "test_stableSwap_ExactInput0For1_MultiHop_FromUser": "307724", + "test_stableSwap_ExactInput0For1_DualAction_FromRouter": "284916", + "test_stableSwap_ExactInput0For1_DualAction_FromUser": "311151", + "test_stableSwap_ExactInput0For1_MultiHop_FromRouter": "281403", + "test_stableSwap_ExactInput0For1_MultiHop_FromUser": "307638", "test_stableSwap_ExactInput0For1_SamePath_FromRouter": "246514", - "test_stableSwap_ExactInput0For1_SamePath_FromUser": "292175", - "test_stableSwap_ExactOut0For1_MultiHop_FromUser": "353828", - "test_stableSwap_ExactOutput0For1_MultiHop_FromRouter": "327597", - "test_stableSwap_ExactOutput0For1_SamePath_FromRouter": "291651" + "test_stableSwap_ExactInput0For1_SamePath_FromUser": "292184", + "test_stableSwap_ExactOut0For1_MultiHop_FromUser": "355264", + "test_stableSwap_ExactOutput0For1_MultiHop_FromRouter": "329029", + "test_stableSwap_ExactOutput0For1_SamePath_FromRouter": "294699" } \ No newline at end of file diff --git a/snapshots/UniversalRouterTest.json b/snapshots/UniversalRouterTest.json index d007bf0..ad438cd 100644 --- a/snapshots/UniversalRouterTest.json +++ b/snapshots/UniversalRouterTest.json @@ -1,4 +1,4 @@ { - "UniversalRouterBytecodeSize": "24093", - "test_sweep_token": "55435" + "UniversalRouterBytecodeSize": "24516", + "test_sweep_token": "55429" } \ No newline at end of file diff --git a/snapshots/V2BnbCake.json b/snapshots/V2BnbCake.json index 67f3dc5..296597a 100644 --- a/snapshots/V2BnbCake.json +++ b/snapshots/V2BnbCake.json @@ -1,4 +1,4 @@ { - "test_v2Swap_exactInput0For1": "116453", - "test_v2Swap_exactOutput0For1": "117044" + "test_v2Swap_exactInput0For1": "116435", + "test_v2Swap_exactOutput0For1": "117038" } \ No newline at end of file diff --git a/snapshots/V2MockBnb.json b/snapshots/V2MockBnb.json index 4d85cbc..1bacec2 100644 --- a/snapshots/V2MockBnb.json +++ b/snapshots/V2MockBnb.json @@ -1,4 +1,4 @@ { - "test_v2Swap_exactInput0For1": "100185", - "test_v2Swap_exactOutput0For1": "100797" + "test_v2Swap_exactInput0For1": "100167", + "test_v2Swap_exactOutput0For1": "100791" } \ No newline at end of file diff --git a/snapshots/V3BnbCake.json b/snapshots/V3BnbCake.json index 6e562ac..f764c4d 100644 --- a/snapshots/V3BnbCake.json +++ b/snapshots/V3BnbCake.json @@ -1,7 +1,7 @@ { - "test_v3Swap_ExactInput0For1": "151922", - "test_v3Swap_ExactInput0For1_ContractBalance": "154835", - "test_v3Swap_exactInput_MultiHop": "243390", - "test_v3Swap_exactOutput0For1": "150624", - "test_v3Swap_exactOutput_MultiHop": "254314" + "test_v3Swap_ExactInput0For1": "151904", + "test_v3Swap_ExactInput0For1_ContractBalance": "154811", + "test_v3Swap_exactInput_MultiHop": "243354", + "test_v3Swap_exactOutput0For1": "150606", + "test_v3Swap_exactOutput_MultiHop": "254284" } \ No newline at end of file diff --git a/snapshots/V3ToInfinityMigrationNativeTest.json b/snapshots/V3ToInfinityMigrationNativeTest.json index 5fd5e00..a458c3f 100644 --- a/snapshots/V3ToInfinityMigrationNativeTest.json +++ b/snapshots/V3ToInfinityMigrationNativeTest.json @@ -1,4 +1,4 @@ { - "test_infiBinPositionmanager_BinAddLiquidity_Native": "544766", - "test_infiCLPositionmanager_Mint_Native": "533949" + "test_infiBinPositionmanager_BinAddLiquidity_Native": "543857", + "test_infiCLPositionmanager_Mint_Native": "533474" } \ No newline at end of file diff --git a/snapshots/V3ToInfinityMigrationTest.json b/snapshots/V3ToInfinityMigrationTest.json index e54f24d..6af1ec8 100644 --- a/snapshots/V3ToInfinityMigrationTest.json +++ b/snapshots/V3ToInfinityMigrationTest.json @@ -1,5 +1,5 @@ { - "test_infiBinPositionmanager_BinAddLiquidity": "592472", - "test_infiCLPositionmanager_Mint": "581676", + "test_infiBinPositionmanager_BinAddLiquidity": "591530", + "test_infiCLPositionmanager_Mint": "581171", "test_v3PositionManager_burn": "286990" } \ No newline at end of file diff --git a/src/modules/pancakeswap/StableSwapRouter.sol b/src/modules/pancakeswap/StableSwapRouter.sol index 54391e0..b41a6ff 100644 --- a/src/modules/pancakeswap/StableSwapRouter.sol +++ b/src/modules/pancakeswap/StableSwapRouter.sol @@ -47,27 +47,27 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable /// @dev if a single hop, path would be of size 2, and flag would be of size 1 /// if 2 hops, path would be of size 3, and flag would be of size 2 - function _stableSwap(address[] calldata path, uint256[] calldata flag, uint256 amountIn) private { + /// @return amtOut The amount of output tokens received after the swap + function _stableSwap(address[] calldata path, uint256[] calldata flag, uint256 amountIn) private returns (uint256 amtOut) { if (path.length - 1 != flag.length) revert StableInvalidPath(); uint256 outputTokenBal; for (uint256 i; i < flag.length; i++) { (address input, address output) = (path[i], path[i + 1]); - bool isLastHop = i == flag.length - 1; - if (!isLastHop) { - outputTokenBal = ERC20(output).balanceOf(address(this)); - } + outputTokenBal = ERC20(output).balanceOf(address(this)); (uint256 k, uint256 j, address swapContract) = stableSwapFactory.getStableInfo(input, output, flag[i]); ERC20(input).safeApprove(swapContract, amountIn); IStableSwap(swapContract).exchange(k, j, amountIn, 0); - if (!isLastHop) { - // if not last swap, update amountIn for the next hop. this is done as swapContract do not return the output amount - amountIn = ERC20(output).balanceOf(address(this)) - outputTokenBal; - } + // Update amountIn for the next hop. this is done as swapContract do not return the output amount + // If this is the last hop, amountIn is the output amount + amountIn = ERC20(output).balanceOf(address(this)) - outputTokenBal; } + + // after the swap iterations, amountIn is the output amount + amtOut = amountIn; } /// @notice Performs a PancakeSwap stable exact input swap @@ -94,11 +94,7 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable } ERC20 tokenOut = ERC20(path[path.length - 1]); - uint256 balanceBefore = tokenOut.balanceOf(address(this)); - - _stableSwap(path, flag, amountIn); - - uint256 amountOut = tokenOut.balanceOf(address(this)) - balanceBefore; + uint256 amountOut = _stableSwap(path, flag, amountIn); if (amountOut < amountOutMinimum) revert StableTooLittleReceived(); if (recipient != address(this)) pay(address(tokenOut), recipient, amountOut); From d6ec203926ee8086abf6f775721759dc06e423e8 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 4 Jun 2025 11:13:38 +0800 Subject: [PATCH 10/16] forge fmt and add more test --- src/modules/pancakeswap/StableSwapRouter.sol | 5 ++++- test/stableSwap/StableSwap_MultiHop.t.sol | 21 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/modules/pancakeswap/StableSwapRouter.sol b/src/modules/pancakeswap/StableSwapRouter.sol index b41a6ff..3af2437 100644 --- a/src/modules/pancakeswap/StableSwapRouter.sol +++ b/src/modules/pancakeswap/StableSwapRouter.sol @@ -48,7 +48,10 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable /// @dev if a single hop, path would be of size 2, and flag would be of size 1 /// if 2 hops, path would be of size 3, and flag would be of size 2 /// @return amtOut The amount of output tokens received after the swap - function _stableSwap(address[] calldata path, uint256[] calldata flag, uint256 amountIn) private returns (uint256 amtOut) { + function _stableSwap(address[] calldata path, uint256[] calldata flag, uint256 amountIn) + private + returns (uint256 amtOut) + { if (path.length - 1 != flag.length) revert StableInvalidPath(); uint256 outputTokenBal; diff --git a/test/stableSwap/StableSwap_MultiHop.t.sol b/test/stableSwap/StableSwap_MultiHop.t.sol index 54052ff..94af612 100644 --- a/test/stableSwap/StableSwap_MultiHop.t.sol +++ b/test/stableSwap/StableSwap_MultiHop.t.sol @@ -123,6 +123,27 @@ contract StableSwapMultiHop is Test { assertEq(ERC20(address(BUSD)).balanceOf(FROM), 100000999768181033551138); // roughly 0.999768181 recieved from swap } + function test_stableSwap_ExactInput0For1_MultiHop_FromUser_StableTooLittleReceived() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + + uint256[] memory flag = new uint256[](2); + flag[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + flag[1] = 2; // 2 is the flag to indicate StableSwapTwoPool + + address[] memory path = new address[](3); + path[0] = address(USDC); + path[1] = address(USDT); + path[2] = address(BUSD); + + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) + bytes[] memory inputs = new bytes[](1); + // should receive 0.999768181 ether + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0.9998 ether, path, flag, true); + + vm.expectRevert(StableSwapRouter.StableTooLittleReceived.selector); + router.execute(commands, inputs); + } + function test_stableSwap_ExactInput0For1_DualAction_FromRouter() public { bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)), bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); From b378c8dad85591eaf3a94f4787eb1766747d7c45 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 4 Jun 2025 12:18:31 +0800 Subject: [PATCH 11/16] feat: add test to ensure multi-hop do not use contract balance --- snapshots/StableSwapMultiHop.json | 1 + test/stableSwap/StableSwap_MultiHop.t.sol | 46 +++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/snapshots/StableSwapMultiHop.json b/snapshots/StableSwapMultiHop.json index 806bc29..a9bc074 100644 --- a/snapshots/StableSwapMultiHop.json +++ b/snapshots/StableSwapMultiHop.json @@ -1,6 +1,7 @@ { "test_stableSwap_ExactInput0For1_DualAction_FromRouter": "284916", "test_stableSwap_ExactInput0For1_DualAction_FromUser": "311151", + "test_stableSwap_ExactInput0For1_MultiCommand_FromRouter": "417068", "test_stableSwap_ExactInput0For1_MultiHop_FromRouter": "281403", "test_stableSwap_ExactInput0For1_MultiHop_FromUser": "307638", "test_stableSwap_ExactInput0For1_SamePath_FromRouter": "246514", diff --git a/test/stableSwap/StableSwap_MultiHop.t.sol b/test/stableSwap/StableSwap_MultiHop.t.sol index 94af612..b37883c 100644 --- a/test/stableSwap/StableSwap_MultiHop.t.sol +++ b/test/stableSwap/StableSwap_MultiHop.t.sol @@ -319,4 +319,50 @@ contract StableSwapMultiHop is Test { assertEq(ERC20(address(USDC)).balanceOf(FROM), 99999099791314908871029); // roughly 0.9 usdc taken from user assertEq(ERC20(address(BUSD)).balanceOf(FROM), 100000900000000000000000); // exactly 0.9 busd received } + + // 3 commmands + // command 1: 1 USDC -> USDT + // command 2: 1 USDC -> USDT -> BUSD + // command 3: (contract balance from comamnd 1 output) USDT -> BUSD + /// @dev This test is to ensure that for multi-hop cases, the next hop input is the output of the previous hop + function test_stableSwap_ExactInput0For1_MultiCommand_FromRouter() public { + bytes memory commands = abi.encodePacked( + bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)), + bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)), + bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN)) + ); + + deal(address(USDC), address(router), AMOUNT * 2); + + uint256[] memory flag1 = new uint256[](1); + flag1[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + address[] memory path1 = new address[](2); + path1[0] = address(USDC); + path1[1] = address(USDT); + + uint256[] memory flag2 = new uint256[](2); + flag2[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + flag2[1] = 2; // 2 is the flag to indicate StableSwapTwoPool + address[] memory path2 = new address[](3); + path2[0] = address(USDC); + path2[1] = address(USDT); + path2[2] = address(BUSD); + + uint256[] memory flag3 = new uint256[](1); + flag3[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + address[] memory path3 = new address[](2); + path3[0] = address(USDT); + path3[1] = address(BUSD); + + bytes[] memory inputs = new bytes[](3); + // recipient, amountIn, amountOutMinimum, path, flag, payerIsUser + inputs[0] = abi.encode(ActionConstants.ADDRESS_THIS, AMOUNT, 0, path1, flag1, false); + inputs[1] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path2, flag2, false); + inputs[2] = abi.encode(ActionConstants.MSG_SENDER, ActionConstants.CONTRACT_BALANCE, 0, path3, flag3, false); + + router.execute(commands, inputs); + vm.snapshotGasLastCall("test_stableSwap_ExactInput0For1_MultiCommand_FromRouter"); + assertEq(ERC20(address(USDC)).balanceOf(FROM), BALANCE); // no token0 taken from user, taken from router + assertEq(ERC20(address(BUSD)).balanceOf(FROM), 100001999536354145097921); // roughly 1.9995 recieved from swap + } } From 49521455c57c25ac265cc6a2578767126761c00a Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 4 Jun 2025 12:28:08 +0800 Subject: [PATCH 12/16] test: update test --- snapshots/StableSwapMultiHop.json | 2 +- test/stableSwap/StableSwap_MultiHop.t.sol | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/snapshots/StableSwapMultiHop.json b/snapshots/StableSwapMultiHop.json index a9bc074..18d78a6 100644 --- a/snapshots/StableSwapMultiHop.json +++ b/snapshots/StableSwapMultiHop.json @@ -1,7 +1,7 @@ { "test_stableSwap_ExactInput0For1_DualAction_FromRouter": "284916", "test_stableSwap_ExactInput0For1_DualAction_FromUser": "311151", - "test_stableSwap_ExactInput0For1_MultiCommand_FromRouter": "417068", + "test_stableSwap_ExactInput0For1_MultiCommand_FromRouter": "417240", "test_stableSwap_ExactInput0For1_MultiHop_FromRouter": "281403", "test_stableSwap_ExactInput0For1_MultiHop_FromUser": "307638", "test_stableSwap_ExactInput0For1_SamePath_FromRouter": "246514", diff --git a/test/stableSwap/StableSwap_MultiHop.t.sol b/test/stableSwap/StableSwap_MultiHop.t.sol index b37883c..0d7d2b8 100644 --- a/test/stableSwap/StableSwap_MultiHop.t.sol +++ b/test/stableSwap/StableSwap_MultiHop.t.sol @@ -356,9 +356,10 @@ contract StableSwapMultiHop is Test { bytes[] memory inputs = new bytes[](3); // recipient, amountIn, amountOutMinimum, path, flag, payerIsUser - inputs[0] = abi.encode(ActionConstants.ADDRESS_THIS, AMOUNT, 0, path1, flag1, false); - inputs[1] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path2, flag2, false); - inputs[2] = abi.encode(ActionConstants.MSG_SENDER, ActionConstants.CONTRACT_BALANCE, 0, path3, flag3, false); + inputs[0] = abi.encode(ActionConstants.ADDRESS_THIS, AMOUNT, 0.9 ether, path1, flag1, false); + inputs[1] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0.9 ether, path2, flag2, false); + inputs[2] = + abi.encode(ActionConstants.MSG_SENDER, ActionConstants.CONTRACT_BALANCE, 0.9 ether, path3, flag3, false); router.execute(commands, inputs); vm.snapshotGasLastCall("test_stableSwap_ExactInput0For1_MultiCommand_FromRouter"); From 9fab7691cfd5cc6de9cb000e24e2e2e16565c697 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 4 Jun 2025 12:58:07 +0800 Subject: [PATCH 13/16] feat: slight refactor for gas saving --- snapshots/StableSwapBusdUsdcTest.json | 4 ++-- snapshots/StableSwapMultiHop.json | 14 +++++++------- snapshots/UniversalRouterTest.json | 2 +- src/modules/pancakeswap/StableSwapRouter.sol | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/snapshots/StableSwapBusdUsdcTest.json b/snapshots/StableSwapBusdUsdcTest.json index 1468b85..5bb3015 100644 --- a/snapshots/StableSwapBusdUsdcTest.json +++ b/snapshots/StableSwapBusdUsdcTest.json @@ -1,4 +1,4 @@ { - "test_stableSwap_ExactInput0For1": "192811", - "test_stableSwap_ExactInput1For0": "193491" + "test_stableSwap_ExactInput0For1": "192796", + "test_stableSwap_ExactInput1For0": "193476" } \ No newline at end of file diff --git a/snapshots/StableSwapMultiHop.json b/snapshots/StableSwapMultiHop.json index 18d78a6..d6cd930 100644 --- a/snapshots/StableSwapMultiHop.json +++ b/snapshots/StableSwapMultiHop.json @@ -1,11 +1,11 @@ { - "test_stableSwap_ExactInput0For1_DualAction_FromRouter": "284916", - "test_stableSwap_ExactInput0For1_DualAction_FromUser": "311151", - "test_stableSwap_ExactInput0For1_MultiCommand_FromRouter": "417240", - "test_stableSwap_ExactInput0For1_MultiHop_FromRouter": "281403", - "test_stableSwap_ExactInput0For1_MultiHop_FromUser": "307638", - "test_stableSwap_ExactInput0For1_SamePath_FromRouter": "246514", - "test_stableSwap_ExactInput0For1_SamePath_FromUser": "292184", + "test_stableSwap_ExactInput0For1_DualAction_FromRouter": "284892", + "test_stableSwap_ExactInput0For1_DualAction_FromUser": "311128", + "test_stableSwap_ExactInput0For1_MultiCommand_FromRouter": "417203", + "test_stableSwap_ExactInput0For1_MultiHop_FromRouter": "281388", + "test_stableSwap_ExactInput0For1_MultiHop_FromUser": "307624", + "test_stableSwap_ExactInput0For1_SamePath_FromRouter": "246485", + "test_stableSwap_ExactInput0For1_SamePath_FromUser": "292156", "test_stableSwap_ExactOut0For1_MultiHop_FromUser": "355264", "test_stableSwap_ExactOutput0For1_MultiHop_FromRouter": "329029", "test_stableSwap_ExactOutput0For1_SamePath_FromRouter": "294699" diff --git a/snapshots/UniversalRouterTest.json b/snapshots/UniversalRouterTest.json index ad438cd..984442c 100644 --- a/snapshots/UniversalRouterTest.json +++ b/snapshots/UniversalRouterTest.json @@ -1,4 +1,4 @@ { - "UniversalRouterBytecodeSize": "24516", + "UniversalRouterBytecodeSize": "24510", "test_sweep_token": "55429" } \ No newline at end of file diff --git a/src/modules/pancakeswap/StableSwapRouter.sol b/src/modules/pancakeswap/StableSwapRouter.sol index 3af2437..5fee536 100644 --- a/src/modules/pancakeswap/StableSwapRouter.sol +++ b/src/modules/pancakeswap/StableSwapRouter.sol @@ -96,11 +96,11 @@ abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable amountIn = ERC20(path[0]).balanceOf(address(this)); } - ERC20 tokenOut = ERC20(path[path.length - 1]); uint256 amountOut = _stableSwap(path, flag, amountIn); if (amountOut < amountOutMinimum) revert StableTooLittleReceived(); - if (recipient != address(this)) pay(address(tokenOut), recipient, amountOut); + address tokenOut = path[path.length - 1]; + if (recipient != address(this)) pay(tokenOut, recipient, amountOut); } /// @notice Performs a PancakeSwap stable exact output swap From c16b92fdcf4751dc4f10feb154d3d01752f2406c Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:07:32 +0800 Subject: [PATCH 14/16] feat: include arb stableswap test with arb stable address --- .../mainnet/DeployArbitrum.s.sol | 6 +- .../deployParameters/mainnet/DeployBase.s.sol | 2 +- .../deployParameters/mainnet/DeployBsc.s.sol | 2 +- snapshots/StableSwapArbTest.json | 4 + test/stableSwap/StableSwapArb.t.sol | 125 ++++++++++++++++++ 5 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 snapshots/StableSwapArbTest.json create mode 100644 test/stableSwap/StableSwapArb.t.sol diff --git a/script/deployParameters/mainnet/DeployArbitrum.s.sol b/script/deployParameters/mainnet/DeployArbitrum.s.sol index 700056d..92f5793 100644 --- a/script/deployParameters/mainnet/DeployArbitrum.s.sol +++ b/script/deployParameters/mainnet/DeployArbitrum.s.sol @@ -15,7 +15,7 @@ import {RouterParameters} from "../../../src/base/RouterImmutables.sol"; contract DeployArbitrum is DeployUniversalRouter { /// @notice contract address will be based on deployment salt function getDeploymentSalt() public pure override returns (bytes32) { - return keccak256("INFINITY-UNIVERSAL-ROUTER/UniversalRouter/0.0001"); + return keccak256("INFINITY-UNIVERSAL-ROUTER/UniversalRouter/1.0.1"); } function setUp() public override { @@ -27,8 +27,8 @@ contract DeployArbitrum is DeployUniversalRouter { v3Deployer: 0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9, v2InitCodeHash: 0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d, v3InitCodeHash: 0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2, - stableFactory: UNSUPPORTED_PROTOCOL, - stableInfo: UNSUPPORTED_PROTOCOL, + stableFactory: 0x5D5fBB19572c4A89846198c3DBEdB2B6eF58a77a, + stableInfo: 0x58B2F00f74a1877510Ec37b22f116Bf5D63Ab1b0, infiVault: UNSUPPORTED_PROTOCOL, infiClPoolManager: UNSUPPORTED_PROTOCOL, infiBinPoolManager: UNSUPPORTED_PROTOCOL, diff --git a/script/deployParameters/mainnet/DeployBase.s.sol b/script/deployParameters/mainnet/DeployBase.s.sol index 71aeaff..7101baa 100644 --- a/script/deployParameters/mainnet/DeployBase.s.sol +++ b/script/deployParameters/mainnet/DeployBase.s.sol @@ -15,7 +15,7 @@ import {RouterParameters} from "../../../src/base/RouterImmutables.sol"; contract DeployBase is DeployUniversalRouter { /// @notice contract address will be based on deployment salt function getDeploymentSalt() public pure override returns (bytes32) { - return keccak256("INFINITY-UNIVERSAL-ROUTER/UniversalRouter/1.0.0"); + return keccak256("INFINITY-UNIVERSAL-ROUTER/UniversalRouter/1.0.1"); } function setUp() public override { diff --git a/script/deployParameters/mainnet/DeployBsc.s.sol b/script/deployParameters/mainnet/DeployBsc.s.sol index 71fe052..a644e52 100644 --- a/script/deployParameters/mainnet/DeployBsc.s.sol +++ b/script/deployParameters/mainnet/DeployBsc.s.sol @@ -15,7 +15,7 @@ import {RouterParameters} from "../../../src/base/RouterImmutables.sol"; contract DeployBsc is DeployUniversalRouter { /// @notice contract address will be based on deployment salt function getDeploymentSalt() public pure override returns (bytes32) { - return keccak256("INFINITY-UNIVERSAL-ROUTER/UniversalRouter/1.0.0"); + return keccak256("INFINITY-UNIVERSAL-ROUTER/UniversalRouter/1.0.1"); } function setUp() public override { diff --git a/snapshots/StableSwapArbTest.json b/snapshots/StableSwapArbTest.json new file mode 100644 index 0000000..224c75e --- /dev/null +++ b/snapshots/StableSwapArbTest.json @@ -0,0 +1,4 @@ +{ + "test_stableSwap_ExactInput0For1": "199963", + "test_stableSwap_ExactInput1For0": "198844" +} \ No newline at end of file diff --git a/test/stableSwap/StableSwapArb.t.sol b/test/stableSwap/StableSwapArb.t.sol new file mode 100644 index 0000000..25ce53c --- /dev/null +++ b/test/stableSwap/StableSwapArb.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ActionConstants} from "infinity-periphery/src/libraries/ActionConstants.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; +import {UniversalRouter} from "../../src/UniversalRouter.sol"; +import {Constants} from "../../src/libraries/Constants.sol"; +import {Commands} from "../../src/libraries/Commands.sol"; +import {RouterParameters} from "../../src/base/RouterImmutables.sol"; +import {IStableSwapFactory} from "../../src/interfaces/IStableSwapFactory.sol"; +import {IStableSwapInfo} from "../../src/interfaces/IStableSwapInfo.sol"; + +/// @dev test stableSwap on arbitrum +contract StableSwapArbTest is Test { + address constant RECIPIENT = address(10); + uint256 constant AMOUNT = 1 ether; + uint256 constant BALANCE = 100000 ether; + ERC20 constant WETH9 = ERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1); + IPermit2 constant PERMIT2 = IPermit2(0x31c2F6fcFf4F8759b3Bd5Bf0e1084A055615c768); + address constant FROM = address(1234); + + /// @dev StableInfo refers to PancakeStableSwapTwoPoolInfo, threePoolInfo is not present as its not used in PCS + IStableSwapFactory STABLE_FACTORY = IStableSwapFactory(0x5D5fBB19572c4A89846198c3DBEdB2B6eF58a77a); + IStableSwapInfo STABLE_INFO = IStableSwapInfo(0x58B2F00f74a1877510Ec37b22f116Bf5D63Ab1b0); + + UniversalRouter public router; + + address public PENDLE = 0x0c880f6761F1af8d9Aa9C466984b80DAb9a8c9e8; + address public mPENDLE = 0xB688BA096b7Bb75d7841e47163Cd12D18B36A5bF; + + function setUp() public { + // BSC: May-09-2024 03:05:23 AM +UTC + vm.createSelectFork(vm.envString("ARB_FORK_URL"), 348588274); + + RouterParameters memory params = RouterParameters({ + permit2: address(PERMIT2), + weth9: address(WETH9), + v2Factory: address(0), + v3Factory: address(0), + v3Deployer: address(0), + v2InitCodeHash: bytes32(0), + v3InitCodeHash: bytes32(0), + stableFactory: address(STABLE_FACTORY), + stableInfo: address(STABLE_INFO), + infiVault: address(0), + infiClPoolManager: address(0), + infiBinPoolManager: address(0), + v3NFTPositionManager: address(0), + infiClPositionManager: address(0), + infiBinPositionManager: address(0) + }); + router = new UniversalRouter(params); + + // pair doesn't exist, revert to keep this test simple without adding to lp etc + // Pendle-mPendle: https://arbiscan.io/address/0x73ed25e04Aa673ddf7411441098fC5ae19976CE0 + if (STABLE_FACTORY.getPairInfo(PENDLE, mPENDLE).swapContract == address(0)) { + revert("Pair doesn't exist"); + } + + vm.startPrank(FROM); + deal(FROM, BALANCE); + deal(PENDLE, FROM, BALANCE); + deal(mPENDLE, FROM, BALANCE); + ERC20(PENDLE).approve(address(PERMIT2), type(uint256).max); + ERC20(mPENDLE).approve(address(PERMIT2), type(uint256).max); + PERMIT2.approve(PENDLE, address(router), type(uint160).max, type(uint48).max); + PERMIT2.approve(mPENDLE, address(router), type(uint160).max, type(uint48).max); + } + + function test_stableSwap_ExactInput0For1() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) + address[] memory path = new address[](2); + path[0] = PENDLE; + path[1] = mPENDLE; + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag(), true); + + router.execute(commands, inputs); + vm.snapshotGasLastCall("test_stableSwap_ExactInput0For1"); + assertEq(ERC20(PENDLE).balanceOf(FROM), BALANCE - AMOUNT); + assertGt(ERC20(mPENDLE).balanceOf(FROM), BALANCE); + } + + function test_stableSwap_ExactInput1For0() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) + address[] memory path = new address[](2); + path[0] = mPENDLE; + path[1] = PENDLE; + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag(), true); + + router.execute(commands, inputs); + vm.snapshotGasLastCall("test_stableSwap_ExactInput1For0"); + assertEq(ERC20(mPENDLE).balanceOf(FROM), BALANCE - AMOUNT); + assertGt(ERC20(PENDLE).balanceOf(FROM), BALANCE); + } + + function test_stableSwap_exactInput0For1FromRouter_Arb() public { + bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.STABLE_SWAP_EXACT_IN))); + deal(PENDLE, address(router), AMOUNT); + // equivalent: abi.decode(inputs, (address, uint256, uint256, address[], uint256[], bool) + address[] memory path = new address[](2); + path[0] = PENDLE; + path[1] = mPENDLE; + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(ActionConstants.MSG_SENDER, AMOUNT, 0, path, flag(), false); + + router.execute(commands, inputs); + assertEq(ERC20(PENDLE).balanceOf(FROM), BALANCE); // no token0 taken from user, taken from router + assertGt(ERC20(mPENDLE).balanceOf(FROM), BALANCE); // token1 received + } + + function flag() internal pure returns (uint256[] memory pairFlag) { + pairFlag = new uint256[](1); + pairFlag[0] = 2; // 2 is the flag to indicate StableSwapTwoPool + } +} From 45a3db042054cd93bc3a483d2205f07f9c80acef Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:13:00 +0800 Subject: [PATCH 15/16] test: update test comment --- test/stableSwap/StableSwapArb.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/stableSwap/StableSwapArb.t.sol b/test/stableSwap/StableSwapArb.t.sol index 25ce53c..9ec4f5f 100644 --- a/test/stableSwap/StableSwapArb.t.sol +++ b/test/stableSwap/StableSwapArb.t.sol @@ -33,7 +33,7 @@ contract StableSwapArbTest is Test { address public mPENDLE = 0xB688BA096b7Bb75d7841e47163Cd12D18B36A5bF; function setUp() public { - // BSC: May-09-2024 03:05:23 AM +UTC + // Arb: Jun-18-2025 08:50:07 AM +UTC vm.createSelectFork(vm.envString("ARB_FORK_URL"), 348588274); RouterParameters memory params = RouterParameters({ From f45b3d30b29eb57f1919fa3418fd6b1090e578a7 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:14:49 +0800 Subject: [PATCH 16/16] add ARB_FORK_URL in ci --- .github/workflows/test.yml | 1 + README.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ce199e..ab2238b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,5 +25,6 @@ jobs: run: forge test -vvv --isolate env: FOUNDRY_PROFILE: ci + ARB_FORK_URL: ${{ secrets.ARB_FORK_URL }} FORK_URL: ${{ secrets.FORK_URL }} TESTNET_FORK_URL: ${{ secrets.TESTNET_FORK_URL }} diff --git a/README.md b/README.md index b5282a5..96b1c36 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ // testnet fork test for infinity, mainnet fork test for v2/v3 export FORK_URL=https://bsc-mainnet.nodereal.io/v1/xxx export TESTNET_FORK_URL=https://bsc-testnet.nodereal.io/v1/xxx + +(If not set, only Arb test will fail) +export ARB_FORK_URL=https://xxx ``` 3. Run test with `forge test`