From 57acbd58c5146c88098e9bc274ec702ef25add32 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Mar 2025 19:10:45 +0530 Subject: [PATCH 1/8] fix: remove amountIn addition to amountOut in _swap for cyclic swaps, add testCyclicSwapWithTwoPools test to verify --- foundry/src/TychoRouter.sol | 4 +++- foundry/test/Constants.sol | 2 ++ foundry/test/TychoRouter.t.sol | 39 ++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 29ac360..59ca036 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -345,7 +345,9 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { currentAmountOut = _callExecutor( swapData.executor(), currentAmountIn, swapData.protocolData() ); - amounts[tokenOutIndex] += currentAmountOut; + amounts[tokenOutIndex] = tokenOutIndex == 0 + ? currentAmountOut + : amounts[tokenOutIndex] + currentAmountOut; remainingAmounts[tokenOutIndex] += currentAmountOut; remainingAmounts[tokenInIndex] -= currentAmountIn; } diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 6a0eb01..8677b87 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -50,6 +50,8 @@ contract Constants is Test, BaseConstants { address USV3_FACTORY_ETHEREUM = 0x1F98431c8aD98523631AE4a59f267346ea31F984; address USV2_FACTORY_ETHEREUM = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8; + address USDC_WETH_USV3 = 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640; // 0.05% fee + address USDC_WETH_USV3_2 = 0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8; // 0.3% fee // Uniswap universal router address UNIVERSAL_ROUTER = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af; diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 3676b59..b7573d3 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1192,6 +1192,45 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718); } + function testCyclicSwapWithTwoPools() public { + // This test has start and end tokens that are the same + // The flow is: + // USDC -> WETH -> USDC -> WETH -> USDC using two pools, and four swaps + uint256 amountIn = 100 * 10 ** 6; + deal(USDC_ADDR, tychoRouterAddr, amountIn); + + // First pool: USDC -> WETH, in uniswap v3 + bytes memory usdcWethData = encodeUniswapV3Swap( + USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3, true + ); + + // Second pool: WETH -> USDC, in uniswap v3 + bytes memory wethUsdcData = encodeUniswapV3Swap( + WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false + ); + + bytes[] memory swaps = new bytes[](4); + // USDC -> WETH + swaps[0] = encodeSwap( + uint8(0), uint8(1), uint24(0), address(usv3Executor), usdcWethData + ); + // WETH -> USDC + swaps[1] = encodeSwap( + uint8(1), uint8(0), uint24(0), address(usv3Executor), wethUsdcData + ); + // USDC -> WETH + swaps[2] = encodeSwap( + uint8(0), uint8(1), uint24(0), address(usv3Executor), usdcWethData + ); + // WETH -> USDC + swaps[3] = encodeSwap( + uint8(1), uint8(0), uint24(0), address(usv3Executor), wethUsdcData + ); + + tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99778590); + } + // Base Network Tests // Make sure to set the RPC_URL to base network function testSwapSingleBase() public { From 66c00df4f171b4c8b09d0b1840a2b6f9bfa8d8b2 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Mar 2025 19:57:47 +0530 Subject: [PATCH 2/8] test: add testCyclicSplitSwap test and update var naming in testCyclicSequentialSwap --- foundry/test/TychoRouter.t.sol | 100 ++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 9 deletions(-) diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index b7573d3..0eb5bae 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1192,45 +1192,127 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718); } - function testCyclicSwapWithTwoPools() public { + function testCyclicSequentialSwap() public { // This test has start and end tokens that are the same // The flow is: // USDC -> WETH -> USDC -> WETH -> USDC using two pools, and four swaps uint256 amountIn = 100 * 10 ** 6; deal(USDC_ADDR, tychoRouterAddr, amountIn); - // First pool: USDC -> WETH, in uniswap v3 - bytes memory usdcWethData = encodeUniswapV3Swap( + bytes memory usdcWethPoolOneZeroForOneData = encodeUniswapV3Swap( USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3, true ); - // Second pool: WETH -> USDC, in uniswap v3 - bytes memory wethUsdcData = encodeUniswapV3Swap( + bytes memory usdcWethPoolTwoOneForZeroData = encodeUniswapV3Swap( WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false ); bytes[] memory swaps = new bytes[](4); // USDC -> WETH swaps[0] = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv3Executor), usdcWethData + uint8(0), + uint8(1), + uint24(0), + address(usv3Executor), + usdcWethPoolOneZeroForOneData ); // WETH -> USDC swaps[1] = encodeSwap( - uint8(1), uint8(0), uint24(0), address(usv3Executor), wethUsdcData + uint8(1), + uint8(0), + uint24(0), + address(usv3Executor), + usdcWethPoolTwoOneForZeroData ); // USDC -> WETH swaps[2] = encodeSwap( - uint8(0), uint8(1), uint24(0), address(usv3Executor), usdcWethData + uint8(0), + uint8(1), + uint24(0), + address(usv3Executor), + usdcWethPoolOneZeroForOneData ); // WETH -> USDC swaps[3] = encodeSwap( - uint8(1), uint8(0), uint24(0), address(usv3Executor), wethUsdcData + uint8(1), + uint8(0), + uint24(0), + address(usv3Executor), + usdcWethPoolTwoOneForZeroData ); tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99778590); } + function testCyclicSplitSwap() public { + // This test has start and end tokens that are the same + // The flow is: + // ┌─── WETH (Pool 1) ───┐ + // │ │ + // USDC (60% split) ─┤ ├─> USDC + // │ │ + // └─── WETH (Pool 2) ───┘ + // + // 60% of USDC is swapped to WETH using Pool 1, then swapped back to USDC using Pool 2 + // 40% of USDC is swapped to WETH using Pool 2, then swapped back to USDC using Pool 1 + uint256 amountIn = 100 * 10 ** 6; + deal(USDC_ADDR, tychoRouterAddr, amountIn); + + bytes memory usdcWethPoolOneZeroForOneData = encodeUniswapV3Swap( + USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3, true + ); + bytes memory usdcWethPoolOneOneForZeroData = encodeUniswapV3Swap( + WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3, false + ); + + bytes memory usdcWethPoolTwoZeroForOneData = encodeUniswapV3Swap( + USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, true + ); + + bytes memory usdcWethPoolTwoOneForZeroData = encodeUniswapV3Swap( + WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false + ); + + bytes[] memory swaps = new bytes[](4); + // USDC -> WETH + swaps[0] = encodeSwap( + uint8(0), + uint8(1), + (0xffffff * 60) / 100, // 60% + address(usv3Executor), + usdcWethPoolOneZeroForOneData + ); + // WETH -> USDC + swaps[1] = encodeSwap( + uint8(1), + uint8(0), + uint24(0), + address(usv3Executor), + usdcWethPoolTwoOneForZeroData + ); + + // USDC -> WETH + swaps[2] = encodeSwap( + uint8(0), + uint8(1), + uint24(0), + address(usv3Executor), + usdcWethPoolTwoZeroForOneData + ); + + // WETH -> USDC + swaps[3] = encodeSwap( + uint8(1), + uint8(0), + uint24(0), + address(usv3Executor), + usdcWethPoolOneOneForZeroData + ); + tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99345512); + } + // Base Network Tests // Make sure to set the RPC_URL to base network function testSwapSingleBase() public { From 4d67df40965414caff94f8660c70f4acad51482f Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Mar 2025 22:00:25 +0530 Subject: [PATCH 3/8] feat: add cyclicSwapAmountOut tracker in _swap, add split cylic tests --- foundry/src/TychoRouter.sol | 11 ++-- foundry/test/Constants.sol | 1 + foundry/test/TychoRouter.t.sol | 99 +++++++++++++++++++++++----------- 3 files changed, 75 insertions(+), 36 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 59ca036..4e16c70 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -329,6 +329,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { uint256[] memory remainingAmounts = new uint256[](nTokens); uint256[] memory amounts = new uint256[](nTokens); + uint256 cyclicSwapAmountOut = 0; amounts[0] = amountIn; remainingAmounts[0] = amountIn; @@ -345,13 +346,15 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { currentAmountOut = _callExecutor( swapData.executor(), currentAmountIn, swapData.protocolData() ); - amounts[tokenOutIndex] = tokenOutIndex == 0 - ? currentAmountOut - : amounts[tokenOutIndex] + currentAmountOut; + if (tokenOutIndex == 0) { + cyclicSwapAmountOut += currentAmountOut; + } else { + amounts[tokenOutIndex] += currentAmountOut; + } remainingAmounts[tokenOutIndex] += currentAmountOut; remainingAmounts[tokenInIndex] -= currentAmountIn; } - return amounts[tokenOutIndex]; + return tokenOutIndex == 0 ? cyclicSwapAmountOut : amounts[tokenOutIndex]; } /** diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 8677b87..9149dcf 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -45,6 +45,7 @@ contract Constants is Test, BaseConstants { address DAI_USDC_POOL = 0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5; address WETH_WBTC_POOL = 0xBb2b8038a1640196FbE3e38816F3e67Cba72D940; address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416; + address USDC_WETH_USV2 = 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc; // Uniswap v3 address USV3_FACTORY_ETHEREUM = 0x1F98431c8aD98523631AE4a59f267346ea31F984; diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 0eb5bae..5fe98b1 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1195,26 +1195,26 @@ contract TychoRouterTest is TychoRouterTestSetup { function testCyclicSequentialSwap() public { // This test has start and end tokens that are the same // The flow is: - // USDC -> WETH -> USDC -> WETH -> USDC using two pools, and four swaps + // USDC -> WETH -> USDC using two pools uint256 amountIn = 100 * 10 ** 6; deal(USDC_ADDR, tychoRouterAddr, amountIn); - bytes memory usdcWethPoolOneZeroForOneData = encodeUniswapV3Swap( + bytes memory usdcWethV3Pool1ZeroOneData = encodeUniswapV3Swap( USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3, true ); - bytes memory usdcWethPoolTwoOneForZeroData = encodeUniswapV3Swap( + bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap( WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false ); - bytes[] memory swaps = new bytes[](4); + bytes[] memory swaps = new bytes[](2); // USDC -> WETH swaps[0] = encodeSwap( uint8(0), uint8(1), uint24(0), address(usv3Executor), - usdcWethPoolOneZeroForOneData + usdcWethV3Pool1ZeroOneData ); // WETH -> USDC swaps[1] = encodeSwap( @@ -1222,30 +1222,14 @@ contract TychoRouterTest is TychoRouterTestSetup { uint8(0), uint24(0), address(usv3Executor), - usdcWethPoolTwoOneForZeroData - ); - // USDC -> WETH - swaps[2] = encodeSwap( - uint8(0), - uint8(1), - uint24(0), - address(usv3Executor), - usdcWethPoolOneZeroForOneData - ); - // WETH -> USDC - swaps[3] = encodeSwap( - uint8(1), - uint8(0), - uint24(0), - address(usv3Executor), - usdcWethPoolTwoOneForZeroData + usdcWethV3Pool2OneZeroData ); tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); - assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99778590); + assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99889294); } - function testCyclicSplitSwap() public { + function testSplitInputCyclicSwap() public { // This test has start and end tokens that are the same // The flow is: // ┌─── WETH (Pool 1) ───┐ @@ -1259,18 +1243,18 @@ contract TychoRouterTest is TychoRouterTestSetup { uint256 amountIn = 100 * 10 ** 6; deal(USDC_ADDR, tychoRouterAddr, amountIn); - bytes memory usdcWethPoolOneZeroForOneData = encodeUniswapV3Swap( + bytes memory usdcWethV3Pool1ZeroOneData = encodeUniswapV3Swap( USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3, true ); - bytes memory usdcWethPoolOneOneForZeroData = encodeUniswapV3Swap( + bytes memory usdcWethV3Pool1OneZeroData = encodeUniswapV3Swap( WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3, false ); - bytes memory usdcWethPoolTwoZeroForOneData = encodeUniswapV3Swap( + bytes memory usdcWethV3Pool2ZeroOneData = encodeUniswapV3Swap( USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, true ); - bytes memory usdcWethPoolTwoOneForZeroData = encodeUniswapV3Swap( + bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap( WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false ); @@ -1281,7 +1265,7 @@ contract TychoRouterTest is TychoRouterTestSetup { uint8(1), (0xffffff * 60) / 100, // 60% address(usv3Executor), - usdcWethPoolOneZeroForOneData + usdcWethV3Pool1ZeroOneData ); // WETH -> USDC swaps[1] = encodeSwap( @@ -1289,7 +1273,7 @@ contract TychoRouterTest is TychoRouterTestSetup { uint8(0), uint24(0), address(usv3Executor), - usdcWethPoolTwoOneForZeroData + usdcWethV3Pool2OneZeroData ); // USDC -> WETH @@ -1298,7 +1282,7 @@ contract TychoRouterTest is TychoRouterTestSetup { uint8(1), uint24(0), address(usv3Executor), - usdcWethPoolTwoZeroForOneData + usdcWethV3Pool2ZeroOneData ); // WETH -> USDC @@ -1307,12 +1291,63 @@ contract TychoRouterTest is TychoRouterTestSetup { uint8(0), uint24(0), address(usv3Executor), - usdcWethPoolOneOneForZeroData + usdcWethV3Pool1OneZeroData ); tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99345512); } + function testSplitOutputCyclicSwap() public { + // This test has start and end tokens that are the same + // The flow is: + // ┌─── WETH (Pool 2, 60% split) ───┐ + // │ │ + // USDC ----─┤ ├─> USDC + // │ │ + // └─── WETH (Pool 2, the rest) ───┘ + // + uint256 amountIn = 100 * 10 ** 6; + deal(USDC_ADDR, tychoRouterAddr, amountIn); + + bytes memory usdcWethV2Data = encodeUniswapV2Swap( + USDC_ADDR, USDC_WETH_USV2, tychoRouterAddr, true + ); + + bytes memory usdcWethV3Pool1OneZeroData = encodeUniswapV3Swap( + WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3, false + ); + + bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap( + WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false + ); + + bytes[] memory swaps = new bytes[](3); + // USDC -> WETH + swaps[0] = encodeSwap( + uint8(0), uint8(1), uint24(0), address(usv2Executor), usdcWethV2Data + ); + // WETH -> USDC + swaps[1] = encodeSwap( + uint8(1), + uint8(0), + (0xffffff * 60) / 100, + address(usv3Executor), + usdcWethV3Pool1OneZeroData + ); + + // WETH -> USDC + swaps[2] = encodeSwap( + uint8(1), + uint8(0), + uint24(0), + address(usv3Executor), + usdcWethV3Pool2OneZeroData + ); + + tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99525908); + } + // Base Network Tests // Make sure to set the RPC_URL to base network function testSwapSingleBase() public { From bb10efc0ccc98db850868adab77254bca6ccbb02 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Mar 2025 23:00:55 +0530 Subject: [PATCH 4/8] docs: update docs at _swap and testSplitOutputCyclicSwap --- foundry/src/TychoRouter.sol | 6 +++++- foundry/test/TychoRouter.t.sol | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 4e16c70..6dd04f0 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -299,12 +299,15 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { * - The indices of the input and output tokens (via `tokenInIndex()` and `tokenOutIndex()`). * - The portion of the available amount to be used for the swap, indicated by the `split` value. * - * Two important notes: + * Three important notes: * - The contract assumes that token indexes follow a specific order: the sell token is at index 0, followed by any * intermediary tokens, and finally the buy token. * - A `split` value of 0 is interpreted as 100% of the available amount (i.e., the entire remaining balance). * This means that in scenarios without explicit splits the value should be 0, and when splits are present, * the last swap should also have a split value of 0. + * - In case of cyclic swaps, the output token is the same as the input token. + * `cyclicSwapAmountOut` is used to track the amount of the output token, and is updated when + * the `tokenOutIndex` is 0. * * @param amountIn The initial amount of the sell token to be swapped. * @param nTokens The total number of tokens involved in the swap path, used to initialize arrays for internal tracking. @@ -346,6 +349,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { currentAmountOut = _callExecutor( swapData.executor(), currentAmountIn, swapData.protocolData() ); + // Checks if the output token is the same as the input token if (tokenOutIndex == 0) { cyclicSwapAmountOut += currentAmountOut; } else { diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 5fe98b1..5b40291 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1232,11 +1232,11 @@ contract TychoRouterTest is TychoRouterTestSetup { function testSplitInputCyclicSwap() public { // This test has start and end tokens that are the same // The flow is: - // ┌─── WETH (Pool 1) ───┐ - // │ │ - // USDC (60% split) ─┤ ├─> USDC - // │ │ - // └─── WETH (Pool 2) ───┘ + // ┌─── WETH (USV3 Pool 1) ───┐ + // │ │ + // USDC (60% split) ─┤ ├─> USDC + // │ │ + // └─── WETH (USV3 Pool 2) ───┘ // // 60% of USDC is swapped to WETH using Pool 1, then swapped back to USDC using Pool 2 // 40% of USDC is swapped to WETH using Pool 2, then swapped back to USDC using Pool 1 @@ -1300,12 +1300,12 @@ contract TychoRouterTest is TychoRouterTestSetup { function testSplitOutputCyclicSwap() public { // This test has start and end tokens that are the same // The flow is: - // ┌─── WETH (Pool 2, 60% split) ───┐ - // │ │ - // USDC ----─┤ ├─> USDC - // │ │ - // └─── WETH (Pool 2, the rest) ───┘ - // + // ┌─── (USV3 Pool 1, 60% split) ───┐ + // │ │ + // USDC ──(USV2)── ┤WETH ├─> USDC + // │ │ + // └─── (USV3 Pool 2, 40% split) ───┘ + uint256 amountIn = 100 * 10 ** 6; deal(USDC_ADDR, tychoRouterAddr, amountIn); From bd6f74ca9ccb9d19ff6cfe6ecdb149254449ea37 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Mon, 10 Mar 2025 18:09:46 +0530 Subject: [PATCH 5/8] docs: update docs at testSplitInputCyclicSwap and testSplitOutputCyclicSwap --- foundry/test/TychoRouter.t.sol | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 5b40291..7d3d5c8 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1232,12 +1232,11 @@ contract TychoRouterTest is TychoRouterTestSetup { function testSplitInputCyclicSwap() public { // This test has start and end tokens that are the same // The flow is: - // ┌─── WETH (USV3 Pool 1) ───┐ - // │ │ - // USDC (60% split) ─┤ ├─> USDC - // │ │ - // └─── WETH (USV3 Pool 2) ───┘ - // + // ┌─── 60% ──> WETH ──> USDC ───┐ + // │ │ + // USDC ──────┤ ├──> USDC + // │ | + // └─── 40% ──> WETH ──> USDC ───┘ // 60% of USDC is swapped to WETH using Pool 1, then swapped back to USDC using Pool 2 // 40% of USDC is swapped to WETH using Pool 2, then swapped back to USDC using Pool 1 uint256 amountIn = 100 * 10 ** 6; @@ -1300,11 +1299,11 @@ contract TychoRouterTest is TychoRouterTestSetup { function testSplitOutputCyclicSwap() public { // This test has start and end tokens that are the same // The flow is: - // ┌─── (USV3 Pool 1, 60% split) ───┐ - // │ │ - // USDC ──(USV2)── ┤WETH ├─> USDC - // │ │ - // └─── (USV3 Pool 2, 40% split) ───┘ + // ┌─── (USV3, 60% split) ───┐ + // │ │ + // USDC ──(USV2) ── WETH──| ├─> USDC + // │ │ + // └─── (USV3, 40% split) ───┘ uint256 amountIn = 100 * 10 ** 6; deal(USDC_ADDR, tychoRouterAddr, amountIn); From 34d5ac6e1cc9b0c06c5ff4cb5f0f09525b9eee18 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Mon, 10 Mar 2025 22:20:13 +0530 Subject: [PATCH 6/8] test: update testSplitInputCyclicSwap to use all WETH for USDC --- foundry/test/TychoRouter.t.sol | 43 +++++++++++----------------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 7d3d5c8..b12bc14 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1232,33 +1232,28 @@ contract TychoRouterTest is TychoRouterTestSetup { function testSplitInputCyclicSwap() public { // This test has start and end tokens that are the same // The flow is: - // ┌─── 60% ──> WETH ──> USDC ───┐ - // │ │ - // USDC ──────┤ ├──> USDC - // │ | - // └─── 40% ──> WETH ──> USDC ───┘ - // 60% of USDC is swapped to WETH using Pool 1, then swapped back to USDC using Pool 2 - // 40% of USDC is swapped to WETH using Pool 2, then swapped back to USDC using Pool 1 + // ┌─── 60% ──> WETH ───┐ + // │ │ + // USDC ──────┤ ├──> USDC + // │ │ + // └─── 40% ──> WETH ───┘ uint256 amountIn = 100 * 10 ** 6; deal(USDC_ADDR, tychoRouterAddr, amountIn); bytes memory usdcWethV3Pool1ZeroOneData = encodeUniswapV3Swap( USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3, true ); - bytes memory usdcWethV3Pool1OneZeroData = encodeUniswapV3Swap( - WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3, false - ); bytes memory usdcWethV3Pool2ZeroOneData = encodeUniswapV3Swap( USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, true ); - bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap( - WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false + bytes memory wethUsdcV2OneZeroData = encodeUniswapV2Swap( + WETH_ADDR, USDC_WETH_USV2, tychoRouterAddr, false ); - bytes[] memory swaps = new bytes[](4); - // USDC -> WETH + bytes[] memory swaps = new bytes[](3); + // USDC -> WETH (60% split) swaps[0] = encodeSwap( uint8(0), uint8(1), @@ -1266,34 +1261,24 @@ contract TychoRouterTest is TychoRouterTestSetup { address(usv3Executor), usdcWethV3Pool1ZeroOneData ); - // WETH -> USDC + // USDC -> WETH (40% remainder) swaps[1] = encodeSwap( - uint8(1), - uint8(0), - uint24(0), - address(usv3Executor), - usdcWethV3Pool2OneZeroData - ); - - // USDC -> WETH - swaps[2] = encodeSwap( uint8(0), uint8(1), uint24(0), address(usv3Executor), usdcWethV3Pool2ZeroOneData ); - // WETH -> USDC - swaps[3] = encodeSwap( + swaps[2] = encodeSwap( uint8(1), uint8(0), uint24(0), - address(usv3Executor), - usdcWethV3Pool1OneZeroData + address(usv2Executor), + wethUsdcV2OneZeroData ); tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); - assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99345512); + assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99574171); } function testSplitOutputCyclicSwap() public { From 38cdf28e7ea93e394135a754e7d8626245964f04 Mon Sep 17 00:00:00 2001 From: Harsh Vardhan Roy <42067944+royvardhan@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:25:38 +0530 Subject: [PATCH 7/8] docs: update testSplitInputCyclicSwap Co-authored-by: Tamara --- foundry/test/TychoRouter.t.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index b12bc14..e86b622 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1232,11 +1232,11 @@ contract TychoRouterTest is TychoRouterTestSetup { function testSplitInputCyclicSwap() public { // This test has start and end tokens that are the same // The flow is: - // ┌─── 60% ──> WETH ───┐ - // │ │ - // USDC ──────┤ ├──> USDC - // │ │ - // └─── 40% ──> WETH ───┘ + // ┌─ (USV3, 60% split) ──> WETH ─┐ + // │ │ + // USDC ──────┤ ├──(USV2)──> USDC + // │ │ + // └─ (USV3, 40% split) ──> WETH ─┘ uint256 amountIn = 100 * 10 ** 6; deal(USDC_ADDR, tychoRouterAddr, amountIn); From 91f36fe3285ae4e3e010b9138e8226e257f8499c Mon Sep 17 00:00:00 2001 From: royvardhan Date: Mon, 10 Mar 2025 23:32:30 +0530 Subject: [PATCH 8/8] fix: amountConsumed check in _swapChecked for cyclic swap --- foundry/src/TychoRouter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 6dd04f0..32fb7f5 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -266,7 +266,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { uint256 amountConsumed = initialBalance - currentBalance; - if (amountConsumed != amountIn) { + if (tokenIn != tokenOut && amountConsumed != amountIn) { revert TychoRouter__AmountInDiffersFromConsumed( amountIn, amountConsumed );