From 7fc008a7180bcc6439ab2b8d3bc3d3af75ee92fd Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Fri, 14 Mar 2025 13:39:14 -0400 Subject: [PATCH 01/26] feat: Rename split swap interfaces - In preparation for adding single and sequential swap methods. --- foundry/src/Dispatcher.sol | 6 +- foundry/src/TychoRouter.sol | 21 +- foundry/test/TychoRouter.t.sol | 429 +++--------------- foundry/test/TychoRouterIntegration.t.sol | 289 ++++++++++++ foundry/test/TychoRouterTestSetup.sol | 8 +- .../evm/strategy_encoder/strategy_encoders.rs | 17 +- 6 files changed, 389 insertions(+), 381 deletions(-) create mode 100644 foundry/test/TychoRouterIntegration.t.sol diff --git a/foundry/src/Dispatcher.sol b/foundry/src/Dispatcher.sol index 828d0b1..a5354c7 100644 --- a/foundry/src/Dispatcher.sol +++ b/foundry/src/Dispatcher.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; import "@interfaces/ICallback.sol"; -error Dispatcher__UnapprovedExecutor(); +error Dispatcher__UnapprovedExecutor(address executor); error Dispatcher__NonContractExecutor(); error Dispatcher__InvalidDataLength(); @@ -59,7 +59,7 @@ contract Dispatcher { bytes calldata data ) internal returns (uint256 calculatedAmount) { if (!executors[executor]) { - revert Dispatcher__UnapprovedExecutor(); + revert Dispatcher__UnapprovedExecutor(executor); } // slither-disable-next-line controlled-delegatecall,low-level-calls @@ -84,7 +84,7 @@ contract Dispatcher { address executor = address(uint160(bytes20(data[data.length - 20:]))); if (!executors[executor]) { - revert Dispatcher__UnapprovedExecutor(); + revert Dispatcher__UnapprovedExecutor(executor); } // slither-disable-next-line controlled-delegatecall,low-level-calls diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 4b01e5b..4bfef7c 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -133,7 +133,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { * * @return amountOut The total amount of the output token received by the receiver, after deducting fees if applicable. */ - function swap( + function splitSwap( uint256 amountIn, address tokenIn, address tokenOut, @@ -145,7 +145,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { bytes calldata swaps ) public payable whenNotPaused nonReentrant returns (uint256 amountOut) { IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); - return _swapChecked( + return _splitSwapChecked( amountIn, tokenIn, tokenOut, @@ -185,7 +185,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { * * @return amountOut The total amount of the output token received by the receiver, after deducting fees if applicable. */ - function swapPermit2( + function splitSwapPermit2( uint256 amountIn, address tokenIn, address tokenOut, @@ -209,7 +209,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { ); } - return _swapChecked( + return _splitSwapChecked( amountIn, tokenIn, tokenOut, @@ -230,7 +230,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { * swap() and swapPermit2() functions. * */ - function _swapChecked( + function _splitSwapChecked( uint256 amountIn, address tokenIn, address tokenOut, @@ -258,7 +258,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { ? address(this).balance : IERC20(tokenIn).balanceOf(address(this)); - amountOut = _swap(amountIn, nTokens, swaps); + amountOut = _splitSwap(amountIn, nTokens, swaps); uint256 currentBalance = tokenIn == address(0) ? address(this).balance : IERC20(tokenIn).balanceOf(address(this)); @@ -314,10 +314,11 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { * * @return The total amount of the buy token obtained after all swaps have been executed. */ - function _swap(uint256 amountIn, uint256 nTokens, bytes calldata swaps_) - internal - returns (uint256) - { + function _splitSwap( + uint256 amountIn, + uint256 nTokens, + bytes calldata swaps_ + ) internal returns (uint256) { if (swaps_.length == 0) { revert TychoRouter__EmptySwaps(); } diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 3801a2e..ec0c5f2 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -206,7 +206,7 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSwapSimple() public { + function testSplitSwapSimple() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 // 1 WETH -> DAI // (USV2) @@ -217,20 +217,20 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr); assertEq(daiBalance, 2659881924818443699787); assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSwapSimplePermit2() public { + function testSplitSwapSimplePermit2() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 using Permit2 // 1 WETH -> DAI // (USV2) @@ -247,13 +247,13 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - tychoRouter.swapPermit2( + tychoRouter.splitSwapPermit2( amountIn, WETH_ADDR, DAI_ADDR, @@ -274,7 +274,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSwapMultipleHops() public { + function testSplitSwapMultipleHops() public { // Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2 // 1 WETH -> DAI -> USDC // (univ2) (univ2) @@ -283,7 +283,7 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes[] memory swaps = new bytes[](2); // WETH -> DAI - swaps[0] = encodeSwap( + swaps[0] = encodeSplitSwap( uint8(0), uint8(1), uint24(0), @@ -294,7 +294,7 @@ contract TychoRouterTest is TychoRouterTestSetup { ); // DAI -> USDC - swaps[1] = encodeSwap( + swaps[1] = encodeSplitSwap( uint8(1), uint8(2), uint24(0), @@ -302,14 +302,14 @@ contract TychoRouterTest is TychoRouterTestSetup { encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) ); - tychoRouter.exposedSwap(amountIn, 3, pleEncode(swaps)); + tychoRouter.exposedSplitSwap(amountIn, 3, pleEncode(swaps)); uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); assertEq(usdcBalance, 2644659787); assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSwapSplitHops() public { + function testSplitSwapSplitHops() public { // Trade 1 WETH for USDC through DAI and WBTC with 4 swaps on Uniswap V2 // -> DAI -> // 1 WETH USDC @@ -320,7 +320,7 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes[] memory swaps = new bytes[](4); // WETH -> WBTC (60%) - swaps[0] = encodeSwap( + swaps[0] = encodeSplitSwap( uint8(0), uint8(1), (0xffffff * 60) / 100, // 60% @@ -330,7 +330,7 @@ contract TychoRouterTest is TychoRouterTestSetup { ) ); // WBTC -> USDC - swaps[1] = encodeSwap( + swaps[1] = encodeSplitSwap( uint8(1), uint8(2), uint24(0), @@ -340,7 +340,7 @@ contract TychoRouterTest is TychoRouterTestSetup { ) ); // WETH -> DAI - swaps[2] = encodeSwap( + swaps[2] = encodeSplitSwap( uint8(0), uint8(3), uint24(0), @@ -351,7 +351,7 @@ contract TychoRouterTest is TychoRouterTestSetup { ); // DAI -> USDC - swaps[3] = encodeSwap( + swaps[3] = encodeSplitSwap( uint8(3), uint8(2), uint24(0), @@ -359,14 +359,14 @@ contract TychoRouterTest is TychoRouterTestSetup { encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) ); - tychoRouter.exposedSwap(amountIn, 4, pleEncode(swaps)); + tychoRouter.exposedSplitSwap(amountIn, 4, pleEncode(swaps)); uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); assertEq(usdcBalance, 2615491639); assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSwapChecked() public { + function testSplitSwapChecked() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 // Does permit2 token approval and transfer // Checks amount out at the end @@ -384,14 +384,14 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; uint256 minAmountOut = 2600 * 1e18; - uint256 amountOut = tychoRouter.swapPermit2( + uint256 amountOut = tychoRouter.splitSwapPermit2( amountIn, WETH_ADDR, DAI_ADDR, @@ -414,7 +414,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSwapCheckedUndefinedMinAmount() public { + function testSplitSwapCheckedUndefinedMinAmount() public { // Min amount should always be non-zero. If zero, swap attempt should revert. uint256 amountIn = 1 ether; @@ -430,7 +430,7 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); bytes[] memory swaps = new bytes[](1); @@ -438,7 +438,7 @@ contract TychoRouterTest is TychoRouterTestSetup { uint256 minAmountOut = 0; vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector); - tychoRouter.swapPermit2( + tychoRouter.splitSwapPermit2( amountIn, WETH_ADDR, DAI_ADDR, @@ -454,7 +454,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSwapCheckedNoPermit2() public { + function testSplitSwapCheckedNoPermit2() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 // Checks amount out at the end uint256 amountIn = 1 ether; @@ -468,14 +468,14 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; uint256 minAmountOut = 2600 * 1e18; - uint256 amountOut = tychoRouter.swap( + uint256 amountOut = tychoRouter.splitSwap( amountIn, WETH_ADDR, DAI_ADDR, @@ -496,7 +496,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSwapCheckedLessApprovalFailure() public { + function testSplitSwapCheckedLessApprovalFailure() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 // Fails while transferring the tokenIn to the router due to insufficient approval uint256 amountIn = 1 ether; @@ -510,7 +510,7 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); bytes[] memory swaps = new bytes[](1); @@ -518,7 +518,7 @@ contract TychoRouterTest is TychoRouterTestSetup { uint256 minAmountOut = 2600 * 1e18; vm.expectRevert(); - tychoRouter.swap( + tychoRouter.splitSwap( amountIn, WETH_ADDR, DAI_ADDR, @@ -533,7 +533,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSwapCheckedNegativeSlippageFailure() public { + function testSplitSwapCheckedNegativeSlippageFailure() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 // Does permit2 token approval and transfer // Checks amount out at the end and fails @@ -551,7 +551,7 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); bytes[] memory swaps = new bytes[](1); @@ -565,7 +565,7 @@ contract TychoRouterTest is TychoRouterTestSetup { minAmountOut ) ); - tychoRouter.swapPermit2( + tychoRouter.splitSwapPermit2( amountIn, WETH_ADDR, DAI_ADDR, @@ -581,7 +581,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSwapFee() public { + function testSplitSwapFee() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 // Does permit2 token approval and transfer // Takes fee at the end @@ -605,13 +605,13 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - uint256 amountOut = tychoRouter.swapPermit2( + uint256 amountOut = tychoRouter.splitSwapPermit2( amountIn, WETH_ADDR, DAI_ADDR, @@ -634,7 +634,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSwapWrapETH() public { + function testSplitSwapWrapETH() public { // Trade 1 ETH (and wrap it) for DAI with 1 swap on Uniswap V2 uint256 amountIn = 1 ether; @@ -657,13 +657,13 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - uint256 amountOut = tychoRouter.swapPermit2{value: amountIn}( + uint256 amountOut = tychoRouter.splitSwapPermit2{value: amountIn}( amountIn, address(0), DAI_ADDR, @@ -685,7 +685,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSwapUnwrapETH() public { + function testSplitSwapUnwrapETH() public { // Trade 3k DAI for WETH with 1 swap on Uniswap V2 and unwrap it at the end uint256 amountIn = 3_000 * 10 ** 18; @@ -701,13 +701,13 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes memory protocolData = encodeUniswapV2Swap(DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - uint256 amountOut = tychoRouter.swapPermit2( + uint256 amountOut = tychoRouter.splitSwapPermit2( amountIn, DAI_ADDR, address(0), @@ -728,7 +728,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSwapSingleUSV3() public { + function testSplitSwapSingleUSV3() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V3 // 1 WETH -> DAI // (USV3) @@ -740,20 +740,20 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes memory protocolData = encodeUniswapV3Swap( WETH_ADDR, DAI_ADDR, tychoRouterAddr, DAI_WETH_USV3, zeroForOne ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv3Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr); assertGe(finalBalance, expAmountOut); } - function testSwapSingleUSV3Permit2() public { + function testSplitSwapSingleUSV3Permit2() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V3 using Permit2 // 1 WETH -> DAI // (USV3) @@ -770,14 +770,14 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes memory protocolData = encodeUniswapV3Swap( WETH_ADDR, DAI_ADDR, tychoRouterAddr, DAI_WETH_USV3, zeroForOne ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv3Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - tychoRouter.swapPermit2( + tychoRouter.splitSwapPermit2( amountIn, WETH_ADDR, DAI_ADDR, @@ -801,236 +801,10 @@ contract TychoRouterTest is TychoRouterTestSetup { uint256 amountIn = 10 ** 18; bytes memory swaps = ""; vm.expectRevert(TychoRouter__EmptySwaps.selector); - tychoRouter.exposedSwap(amountIn, 2, swaps); + tychoRouter.exposedSplitSwap(amountIn, 2, swaps); } - function testSingleSwapIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - - // Tests swapping WETH -> DAI on a USV2 pool - deal(WETH_ADDR, ALICE, 1 ether); - uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); - - // Approve permit2 - vm.startPrank(ALICE); - IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using `test_split_swap_strategy_encoder_simple` - // but manually replacing the executor address - // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test - // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` - (bool success,) = tychoRouterAddr.call( - hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000903146e5f6c59c064b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067e4225a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067bc9c620000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000411fdbe0ac6bdafd51044f24b158235effa29797f468cd4684efa379053d3d15d47ed8b8206e3f6e7349f40aad231cc7e04ed25cbea1ac659b575be8cc168fc2361c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" - ); - - vm.stopPrank(); - - uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); - - assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 2659881924818443699787); - } - - function testSingleSwapWithoutPermit2Integration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - - // Tests swapping WETH -> DAI on a USV2 pool without permit2 - deal(WETH_ADDR, ALICE, 1 ether); - vm.startPrank(ALICE); - IERC20(WETH_ADDR).approve(address(tychoRouterAddr), 1 ether); - uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); - // Encoded solution generated using `test_split_swap_strategy_encoder_simple_route_no_permit2` - // but manually replacing the executor address - // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test - // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` - (bool success,) = tychoRouterAddr.call( - hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000008f1d5c1cae37400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" - ); - - vm.stopPrank(); - uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); - assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 2659881924818443699787); - } - - function testUSV4Integration() public { - // Test created with calldata from our router encoder. - - // Performs a sequential swap from USDC to PEPE though ETH using two - // consecutive USV4 pools - // - // USDC ──(USV4)──> ETH ───(USV4)──> PEPE - // - deal(USDC_ADDR, ALICE, 1 ether); - uint256 balancerBefore = IERC20(PEPE_ADDR).balanceOf(ALICE); - - // Approve permit2 - vm.startPrank(ALICE); - IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using `test_split_encoding_strategy_usv4` - // and ensuring that the encoded executor address is the one in this test - // `f62849f9a0b5bf2913b396098f7c7019b51a820a` - (bool success,) = tychoRouterAddr.call( - hex"d499aa88000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000005064ff624d54346285543f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000067e4237600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067bc9d7e00000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004166b5d3bb274c323e08eeba45d308cc9c11216f9aaafad2a22e94b94fec39293e5480f65f6238d7c8f1e8177f39118373e1041b0ab3a674d3041d119bdb6bc39c1b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c008a0001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d231193300f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000000000000000000000000bb800003c6982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000000000000000" - ); - - vm.stopPrank(); - - uint256 balancerAfter = IERC20(PEPE_ADDR).balanceOf(ALICE); - - assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 97191013220606467325121599); - } - - function testUSV4IntegrationInputETH() public { - // Test created with calldata from our router encoder. - - // Performs a single swap from ETH to PEPE without wrapping or unwrapping - // - // ETH ───(USV4)──> PEPE - // - deal(ALICE, 1 ether); - uint256 balancerBefore = IERC20(PEPE_ADDR).balanceOf(ALICE); - - // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_in` - // and ensuring that the encoded executor address is the one in this test - // `f62849f9a0b5bf2913b396098f7c7019b51a820a` - (bool success,) = tychoRouterAddr.call{value: 1 ether}( - hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d2311933000000000000000000000000000000000000000000c87c939ae635f92dc2379c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067f01a7800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c894800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000416642950b804a47a0abcb17d81cc2a7967d606e00e8de470e0e7827347658160a28b9892f147248b9bf31aad8faa06181aee0c4a612151e9ef4889991b9930b791b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007200700001000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d231193301f62849f9a0b5bf2913b396098f7c7019b51a820a6982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000" - ); - - vm.stopPrank(); - - uint256 balancerAfter = IERC20(PEPE_ADDR).balanceOf(ALICE); - - assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 242373460199848577067005852); - } - - function testUSV4IntegrationOutputETH() public { - // Test created with calldata from our router encoder. - - // Performs a single swap from USDC to ETH without wrapping or unwrapping - // - // USDC ───(USV4)──> ETH - // - deal(USDC_ADDR, ALICE, 3000_000000); - uint256 balancerBefore = ALICE.balance; - - // Approve permit2 - vm.startPrank(ALICE); - IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - - // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_out` - // and ensuring that the encoded executor address is the one in this test - // `f62849f9a0b5bf2913b396098f7c7019b51a820a` - (bool success,) = tychoRouterAddr.call( - hex"d499aa8800000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f81490b4f29aade000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000067f01af000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c894f80000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000417811cd10b02278128a9e4df9ef2e099cff6ad46ec6ead5ba0b70dd1db5749d573cf4a8821a524bd6cc5b61ce0faf69d1d4b1f9233b93a4b203e79668f250b1a71c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007200700001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000000000000000000000000bb800003c0000000000000000000000000000" - ); - - vm.stopPrank(); - - uint256 balancerAfter = ALICE.balance; - - assertTrue(success, "Call Failed"); - console.logUint(balancerAfter - balancerBefore); - assertEq(balancerAfter - balancerBefore, 1117254495486192350); - } - - function testSingleSwapWithWrapIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - - // Tests swapping WETH -> DAI on a USV2 pool, but ETH is received from the user - // and wrapped before the swap - deal(ALICE, 1 ether); - uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); - - // Approve permit2 - vm.startPrank(ALICE); - // Encoded solution generated using - // `test_split_swap_strategy_encoder_simple_route_wrap` - // but manually replacing the executor address - // `f6c5be66fff9dc69962d73da0a617a827c382329` with the one in this test - // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` - (bool success,) = tychoRouterAddr.call{value: 1 ether}( - hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000903146e5f6c59c064b000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067f0192a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c893320000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000419849ede1f59ad3092a3d8f6b5d7a4d3d854c8013d0a728b8556dc9744ddeed6c7edc4987c7724c280d493ca8dd55dd5aa5f5a66a66d85683f8a5b744908752a21b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600020000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" - ); - - vm.stopPrank(); - - uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); - - assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 2659881924818443699787); - } - - function testSingleSwapWithUnwrapIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - - // Tests swapping DAI -> WETH on a USV2 pool, and WETH is unwrapped to ETH - // before sending back to the user - deal(DAI_ADDR, ALICE, 3000 ether); - uint256 balancerBefore = ALICE.balance; - - // Approve permit2 - vm.startPrank(ALICE); - IERC20(DAI_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using - // `test_split_swap_strategy_encoder_simple_route_unwrap` - // but manually replacing the executor address - // `f6c5be66fff9dc69962d73da0a617a827c382329` with the one in this test - // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` - (bool success,) = tychoRouterAddr.call( - hex"d499aa880000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbd2fc137a30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000000000000000000000000000000000000067f017d700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c891df00000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004181b0d9c7bbf7bd3270e22a7ff337b019b006ea60d9e357035b622bfc8e48126343fa9c1342383d3d072c2ddea2072fd5e447e7b6a4b56f5e7973963d18664e5d1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395010000000000000000" - ); - - vm.stopPrank(); - - uint256 balancerAfter = ALICE.balance; - - assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 1120007305574805922); - } - - function testSplitSwapIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - - // Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools - // - // ┌──(USV2)──> WBTC ───(USV2)──> USDC - // WETH ─┤ - // └──(USV2)──> DAI ───(USV2)──> USDC - deal(WETH_ADDR, ALICE, 1 ether); - uint256 balancerBefore = IERC20(USDC_ADDR).balanceOf(ALICE); - - // Approve permit2 - vm.startPrank(ALICE); - IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using `test_split_swap_strategy_encoder_complex` - // but manually replacing the executor address - // `f6c5be66FFf9DC69962d73da0A617a827c382329` with the one in this test - // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` - (bool success,) = tychoRouterAddr.call( - hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067f0198700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c8938f000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041308a3ba881e23ac794deca324cfd959b808c86bb239b81c9db8873c8392382411f87902e6ceb8e59636d8d6fab4ead1863727f9a2168246c93b678f3ae4ae37b1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160005600028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d0139500005602030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d53ede3eca2a72b3aecc820e955b36f38437d0139501005601030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d0139501" - ); - - vm.stopPrank(); - - uint256 balancerAfter = IERC20(USDC_ADDR).balanceOf(ALICE); - - assertTrue(success, "Call Failed"); - assertGe(balancerAfter - balancerBefore, 26173932); - - // All input tokens are transferred to the router at first. Make sure we used - // all of it (and thus our splits are correct). - assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); - } - - function testSwapAmountInNotFullySpent() public { + function testSplitSwapAmountInNotFullySpent() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 // Has invalid data as input! There is only one swap with 60% of the input amount uint256 amountIn = 1 ether; @@ -1047,7 +821,7 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), (0xffffff * 60) / 100, // 60% @@ -1066,7 +840,7 @@ contract TychoRouterTest is TychoRouterTestSetup { ) ); - tychoRouter.swapPermit2( + tychoRouter.splitSwapPermit2( amountIn, WETH_ADDR, DAI_ADDR, @@ -1083,7 +857,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSwapSingleUSV4Callback() public { + function testSplitSwapSingleUSV4Callback() public { uint256 amountIn = 100 ether; deal(USDE_ADDR, tychoRouterAddr, amountIn); @@ -1099,19 +873,19 @@ contract TychoRouterTest is TychoRouterTestSetup { USDE_ADDR, USDT_ADDR, true, address(usv4Executor), pools ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); assertEq(IERC20(USDT_ADDR).balanceOf(tychoRouterAddr), 99943852); } - function testSwapSingleUSV4CallbackPermit2() public { + function testSplitSwapSingleUSV4CallbackPermit2() public { vm.startPrank(ALICE); uint256 amountIn = 100 ether; deal(USDE_ADDR, ALICE, amountIn); @@ -1132,14 +906,14 @@ contract TychoRouterTest is TychoRouterTestSetup { USDE_ADDR, USDT_ADDR, true, address(usv4Executor), pools ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - tychoRouter.swapPermit2( + tychoRouter.splitSwapPermit2( amountIn, USDE_ADDR, USDT_ADDR, @@ -1157,7 +931,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSwapMultipleUSV4Callback() public { + function testSplitSwapMultipleUSV4Callback() public { // This test has two uniswap v4 hops that will be executed inside of the V4 pool manager // USDE -> USDT -> WBTC uint256 amountIn = 100 ether; @@ -1180,14 +954,14 @@ contract TychoRouterTest is TychoRouterTestSetup { USDE_ADDR, WBTC_ADDR, true, address(usv4Executor), pools ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718); } @@ -1209,7 +983,7 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes[] memory swaps = new bytes[](2); // USDC -> WETH - swaps[0] = encodeSwap( + swaps[0] = encodeSplitSwap( uint8(0), uint8(1), uint24(0), @@ -1217,7 +991,7 @@ contract TychoRouterTest is TychoRouterTestSetup { usdcWethV3Pool1ZeroOneData ); // WETH -> USDC - swaps[1] = encodeSwap( + swaps[1] = encodeSplitSwap( uint8(1), uint8(0), uint24(0), @@ -1225,7 +999,7 @@ contract TychoRouterTest is TychoRouterTestSetup { usdcWethV3Pool2OneZeroData ); - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99889294); } @@ -1254,7 +1028,7 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes[] memory swaps = new bytes[](3); // USDC -> WETH (60% split) - swaps[0] = encodeSwap( + swaps[0] = encodeSplitSwap( uint8(0), uint8(1), (0xffffff * 60) / 100, // 60% @@ -1262,7 +1036,7 @@ contract TychoRouterTest is TychoRouterTestSetup { usdcWethV3Pool1ZeroOneData ); // USDC -> WETH (40% remainder) - swaps[1] = encodeSwap( + swaps[1] = encodeSplitSwap( uint8(0), uint8(1), uint24(0), @@ -1270,14 +1044,14 @@ contract TychoRouterTest is TychoRouterTestSetup { usdcWethV3Pool2ZeroOneData ); // WETH -> USDC - swaps[2] = encodeSwap( + swaps[2] = encodeSplitSwap( uint8(1), uint8(0), uint24(0), address(usv2Executor), wethUsdcV2OneZeroData ); - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99574171); } @@ -1307,11 +1081,11 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes[] memory swaps = new bytes[](3); // USDC -> WETH - swaps[0] = encodeSwap( + swaps[0] = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), usdcWethV2Data ); // WETH -> USDC - swaps[1] = encodeSwap( + swaps[1] = encodeSplitSwap( uint8(1), uint8(0), (0xffffff * 60) / 100, @@ -1320,7 +1094,7 @@ contract TychoRouterTest is TychoRouterTestSetup { ); // WETH -> USDC - swaps[2] = encodeSwap( + swaps[2] = encodeSplitSwap( uint8(1), uint8(0), uint24(0), @@ -1328,70 +1102,13 @@ contract TychoRouterTest is TychoRouterTestSetup { usdcWethV3Pool2OneZeroData ); - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99525908); } - function testCyclicSequentialSwapIntegration() public { - deal(USDC_ADDR, ALICE, 100 * 10 ** 6); - - // Approve permit2 - vm.startPrank(ALICE); - IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using `test_cyclic_sequential_swap` - // but manually replacing the executor address - // `dd8559c917393fc8dd2b4dd289c52ff445fde1b0` with the one in this test - // `2e234dae75c793f67a35089c9d99245e1c58470b` - (bool success,) = tychoRouterAddr.call( - hex"d499aa880000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f4308e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000067f67a8b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067cef493000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041c07077fc73bb0f5129006061288fa0583c101631307377281d6b8f3feb50aa2d564f9948c92e0e4abc3771d592bd2f22ebb18ccf21b270459b05f272251ce1c71b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d8000000" - ); - - assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99889294); - - vm.stopPrank(); - } - - function testSplitInputCyclicSwapIntegration() public { - deal(USDC_ADDR, ALICE, 100 * 10 ** 6); - - // Approve permit2 - vm.startPrank(ALICE); - IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using `test_split_input_cyclic_swap` - // but manually replacing the executor addresses with the ones in this test - // `dd8559c917393fc8dd2b4dd289c52ff445fde1b0` to `2e234dae75c793f67a35089c9d99245e1c58470b` - // `f6c5be66fff9dc69962d73da0a617a827c382329` to `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` - (bool success,) = tychoRouterAddr.call( - hex"d499aa880000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ef619b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000067f6c08700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067cf3a8f000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041f248bfa39e6801b4173cd4d61e5e5d0c31942eb3c194785f964a82b2c3e05b4b302bccc0924fa4c4ef90854e42865db11f458d3b6a62afddee833f3eb069cd521b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000136006d00019999992e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d801005601000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000" - ); - - assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99574171); - - vm.stopPrank(); - } - - function testSplitOutputCyclicSwapIntegration() public { - deal(USDC_ADDR, ALICE, 100 * 10 ** 6); - - // Approve permit2 - vm.startPrank(ALICE); - IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using `test_split_output_cyclic_swap` - // but manually replacing the executor addresses with the ones in this test - // `dd8559c917393fc8dd2b4dd289c52ff445fde1b0` to `2e234dae75c793f67a35089c9d99245e1c58470b` - // `f6c5be66fff9dc69962d73da0a617a827c382329` to `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` - (bool success,) = tychoRouterAddr.call( - hex"d499aa880000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005eea514000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000067f6be9400000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067cf389c000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041c02ad8eceede50085f35ce8e8313ebbac9b379396c6e72a35bb4df0970cbdaaa1a91e6f787641af55b13b926199c844df42fdd2ae7bb287db7e5cc2a8bc1d7f51b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000136005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d0139501006d01009999992e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564000006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d80000000000000000000000" - ); - - assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99525908); - - vm.stopPrank(); - } - // Base Network Tests // Make sure to set the RPC_URL to base network - function testSwapSingleBase() public { + function testSplitSwapSingleBase() public { vm.skip(true); vm.rollFork(26857267); uint256 amountIn = 10 * 10 ** 6; @@ -1401,13 +1118,13 @@ contract TychoRouterTest is TychoRouterTestSetup { BASE_USDC, USDC_MAG7_POOL, tychoRouterAddr, true ); - bytes memory swap = encodeSwap( + bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); assertGt(IERC20(BASE_MAG7).balanceOf(tychoRouterAddr), 1379830606); } } diff --git a/foundry/test/TychoRouterIntegration.t.sol b/foundry/test/TychoRouterIntegration.t.sol new file mode 100644 index 0000000..4ed4431 --- /dev/null +++ b/foundry/test/TychoRouterIntegration.t.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "./TychoRouterTestSetup.sol"; + +contract TychoRouterTestIntegration is TychoRouterTestSetup { + function testSplitSwapSingleIntegration() public { + // Test created with calldata from our router encoder, replacing the executor + // address with the USV2 executor address. + + // Tests swapping WETH -> DAI on a USV2 pool + deal(WETH_ADDR, ALICE, 1 ether); + uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + // Encoded solution generated using `test_split_swap_strategy_encoder_simple` + // but manually replacing the executor address + // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test + // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` + (bool success,) = tychoRouterAddr.call( + hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000903146e5f6c59c064b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067e4225a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067bc9c620000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000411fdbe0ac6bdafd51044f24b158235effa29797f468cd4684efa379053d3d15d47ed8b8206e3f6e7349f40aad231cc7e04ed25cbea1ac659b575be8cc168fc2361c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertEq(balancerAfter - balancerBefore, 2659881924818443699787); + } + + function testSplitSwapSingleWithoutPermit2Integration() public { + // Test created with calldata from our router encoder, replacing the executor + // address with the USV2 executor address. + + // Tests swapping WETH -> DAI on a USV2 pool without permit2 + deal(WETH_ADDR, ALICE, 1 ether); + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(address(tychoRouterAddr), 1 ether); + uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); + // Encoded solution generated using `test_split_swap_strategy_encoder_simple_route_no_permit2` + // but manually replacing the executor address + // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test + // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` + (bool success,) = tychoRouterAddr.call( + hex"79b9b93b0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000008f1d5c1cae37400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" + ); + + vm.stopPrank(); + uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); + assertTrue(success, "Call Failed"); + assertEq(balancerAfter - balancerBefore, 2659881924818443699787); + } + + function testSplitUSV4Integration() public { + // Test created with calldata from our router encoder. + + // Performs a sequential swap from USDC to PEPE though ETH using two + // consecutive USV4 pools + // + // USDC ──(USV4)──> ETH ───(USV4)──> PEPE + // + deal(USDC_ADDR, ALICE, 1 ether); + uint256 balancerBefore = IERC20(PEPE_ADDR).balanceOf(ALICE); + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + // Encoded solution generated using `test_split_encoding_strategy_usv4` + // and ensuring that the encoded executor address is the one in this test + // `f62849f9a0b5bf2913b396098f7c7019b51a820a` + (bool success,) = tychoRouterAddr.call( + hex"7c553846000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000005064ff624d54346285543f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000067e4237600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067bc9d7e00000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004166b5d3bb274c323e08eeba45d308cc9c11216f9aaafad2a22e94b94fec39293e5480f65f6238d7c8f1e8177f39118373e1041b0ab3a674d3041d119bdb6bc39c1b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c008a0001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d231193300f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000000000000000000000000bb800003c6982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = IERC20(PEPE_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertEq(balancerAfter - balancerBefore, 97191013220606467325121599); + } + + function testSplitUSV4IntegrationInputETH() public { + // Test created with calldata from our router encoder. + + // Performs a single swap from ETH to PEPE without wrapping or unwrapping + // + // ETH ───(USV4)──> PEPE + // + deal(ALICE, 1 ether); + uint256 balancerBefore = IERC20(PEPE_ADDR).balanceOf(ALICE); + + // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_in` + // and ensuring that the encoded executor address is the one in this test + // `f62849f9a0b5bf2913b396098f7c7019b51a820a` + (bool success,) = tychoRouterAddr.call{value: 1 ether}( + hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d2311933000000000000000000000000000000000000000000c87c939ae635f92dc2379c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067f01a7800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c894800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000416642950b804a47a0abcb17d81cc2a7967d606e00e8de470e0e7827347658160a28b9892f147248b9bf31aad8faa06181aee0c4a612151e9ef4889991b9930b791b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007200700001000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d231193301f62849f9a0b5bf2913b396098f7c7019b51a820a6982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = IERC20(PEPE_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertEq(balancerAfter - balancerBefore, 242373460199848577067005852); + } + + function testSplitUSV4IntegrationOutputETH() public { + // Test created with calldata from our router encoder. + + // Performs a single swap from USDC to ETH without wrapping or unwrapping + // + // USDC ───(USV4)──> ETH + // + deal(USDC_ADDR, ALICE, 3000_000000); + uint256 balancerBefore = ALICE.balance; + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + + // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_out` + // and ensuring that the encoded executor address is the one in this test + // `f62849f9a0b5bf2913b396098f7c7019b51a820a` + (bool success,) = tychoRouterAddr.call( + hex"7c55384600000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f81490b4f29aade000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000067f01af000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c894f80000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000417811cd10b02278128a9e4df9ef2e099cff6ad46ec6ead5ba0b70dd1db5749d573cf4a8821a524bd6cc5b61ce0faf69d1d4b1f9233b93a4b203e79668f250b1a71c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007200700001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000000000000000000000000bb800003c0000000000000000000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = ALICE.balance; + + assertTrue(success, "Call Failed"); + console.logUint(balancerAfter - balancerBefore); + assertEq(balancerAfter - balancerBefore, 1117254495486192350); + } + + function testSplitSwapSingleWithWrapIntegration() public { + // Test created with calldata from our router encoder, replacing the executor + // address with the USV2 executor address. + + // Tests swapping WETH -> DAI on a USV2 pool, but ETH is received from the user + // and wrapped before the swap + deal(ALICE, 1 ether); + uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); + + // Approve permit2 + vm.startPrank(ALICE); + // Encoded solution generated using + // `test_split_swap_strategy_encoder_simple_route_wrap` + // but manually replacing the executor address + // `f6c5be66fff9dc69962d73da0a617a827c382329` with the one in this test + // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` + (bool success,) = tychoRouterAddr.call{value: 1 ether}( + hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000903146e5f6c59c064b000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067f0192a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c893320000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000419849ede1f59ad3092a3d8f6b5d7a4d3d854c8013d0a728b8556dc9744ddeed6c7edc4987c7724c280d493ca8dd55dd5aa5f5a66a66d85683f8a5b744908752a21b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600020000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertEq(balancerAfter - balancerBefore, 2659881924818443699787); + } + + function testSplitSwapSingleWithUnwrapIntegration() public { + // Test created with calldata from our router encoder, replacing the executor + // address with the USV2 executor address. + + // Tests swapping DAI -> WETH on a USV2 pool, and WETH is unwrapped to ETH + // before sending back to the user + deal(DAI_ADDR, ALICE, 3000 ether); + uint256 balancerBefore = ALICE.balance; + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(DAI_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + // Encoded solution generated using + // `test_split_swap_strategy_encoder_simple_route_unwrap` + // but manually replacing the executor address + // `f6c5be66fff9dc69962d73da0a617a827c382329` with the one in this test + // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` + (bool success,) = tychoRouterAddr.call( + hex"7c5538460000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbd2fc137a30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000000000000000000000000000000000000067f017d700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c891df00000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004181b0d9c7bbf7bd3270e22a7ff337b019b006ea60d9e357035b622bfc8e48126343fa9c1342383d3d072c2ddea2072fd5e447e7b6a4b56f5e7973963d18664e5d1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395010000000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = ALICE.balance; + + assertTrue(success, "Call Failed"); + assertEq(balancerAfter - balancerBefore, 1120007305574805922); + } + + function testSplitSwapIntegration() public { + // Test created with calldata from our router encoder, replacing the executor + // address with the USV2 executor address. + + // Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools + // + // ┌──(USV2)──> WBTC ───(USV2)──> USDC + // WETH ─┤ + // └──(USV2)──> DAI ───(USV2)──> USDC + deal(WETH_ADDR, ALICE, 1 ether); + uint256 balancerBefore = IERC20(USDC_ADDR).balanceOf(ALICE); + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + // Encoded solution generated using `test_split_swap_strategy_encoder_complex` + // but manually replacing the executor address + // `f6c5be66FFf9DC69962d73da0A617a827c382329` with the one in this test + // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` + (bool success,) = tychoRouterAddr.call( + hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067f0198700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c8938f000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041308a3ba881e23ac794deca324cfd959b808c86bb239b81c9db8873c8392382411f87902e6ceb8e59636d8d6fab4ead1863727f9a2168246c93b678f3ae4ae37b1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160005600028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d0139500005602030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d53ede3eca2a72b3aecc820e955b36f38437d0139501005601030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d0139501" + ); + + vm.stopPrank(); + + uint256 balancerAfter = IERC20(USDC_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertGe(balancerAfter - balancerBefore, 26173932); + + // All input tokens are transferred to the router at first. Make sure we used + // all of it (and thus our splits are correct). + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } + + function testCyclicSequentialSwapIntegration() public { + deal(USDC_ADDR, ALICE, 100 * 10 ** 6); + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + // Encoded solution generated using `test_cyclic_sequential_swap` + // but manually replacing the executor address + // `dd8559c917393fc8dd2b4dd289c52ff445fde1b0` with the one in this test + // `2e234dae75c793f67a35089c9d99245e1c58470b` + (bool success,) = tychoRouterAddr.call( + hex"7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f4308e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000067f67a8b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067cef493000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041c07077fc73bb0f5129006061288fa0583c101631307377281d6b8f3feb50aa2d564f9948c92e0e4abc3771d592bd2f22ebb18ccf21b270459b05f272251ce1c71b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d8000000" + ); + + assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99889294); + + vm.stopPrank(); + } + + function testSplitInputCyclicSwapIntegration() public { + deal(USDC_ADDR, ALICE, 100 * 10 ** 6); + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + // Encoded solution generated using `test_split_input_cyclic_swap` + // but manually replacing the executor addresses with the ones in this test + // `dd8559c917393fc8dd2b4dd289c52ff445fde1b0` to `2e234dae75c793f67a35089c9d99245e1c58470b` + // `f6c5be66fff9dc69962d73da0a617a827c382329` to `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` + (bool success,) = tychoRouterAddr.call( + hex"7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ef619b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000067f6c08700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067cf3a8f000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041f248bfa39e6801b4173cd4d61e5e5d0c31942eb3c194785f964a82b2c3e05b4b302bccc0924fa4c4ef90854e42865db11f458d3b6a62afddee833f3eb069cd521b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000136006d00019999992e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d801005601000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000" + ); + + assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99574171); + + vm.stopPrank(); + } + + function testSplitOutputCyclicSwapIntegration() public { + deal(USDC_ADDR, ALICE, 100 * 10 ** 6); + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + // Encoded solution generated using `test_split_output_cyclic_swap` + // but manually replacing the executor addresses with the ones in this test + // `dd8559c917393fc8dd2b4dd289c52ff445fde1b0` to `2e234dae75c793f67a35089c9d99245e1c58470b` + // `f6c5be66fff9dc69962d73da0a617a827c382329` to `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` + (bool success,) = tychoRouterAddr.call( + hex"7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005eea514000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000067f6be9400000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067cf389c000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041c02ad8eceede50085f35ce8e8313ebbac9b379396c6e72a35bb4df0970cbdaaa1a91e6f787641af55b13b926199c844df42fdd2ae7bb287db7e5cc2a8bc1d7f51b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000136005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d0139501006d01009999992e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564000006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d80000000000000000000000" + ); + + assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99525908); + + vm.stopPrank(); + } +} diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 634f814..a92f44a 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -22,16 +22,16 @@ contract TychoRouterExposed is TychoRouter { return _unwrapETH(amount); } - function exposedSwap( + function exposedSplitSwap( uint256 amountIn, uint256 nTokens, bytes calldata swaps ) external returns (uint256) { - return _swap(amountIn, nTokens, swaps); + return _splitSwap(amountIn, nTokens, swaps); } } -contract TychoRouterTestSetup is Test, Constants { +contract TychoRouterTestSetup is Constants { TychoRouterExposed tychoRouter; address tychoRouterAddr; UniswapV2Executor public usv2Executor; @@ -181,7 +181,7 @@ contract TychoRouterTestSetup is Test, Constants { } } - function encodeSwap( + function encodeSplitSwap( uint8 tokenInIndex, uint8 tokenOutIndex, uint24 split, diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 92d1af8..a3e48d2 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -86,11 +86,12 @@ impl SplitSwapStrategyEncoder { ) -> Result { let chain = Chain::from(blockchain); let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { - (Some(Permit2::new(swapper_pk, chain.clone())?), "swapPermit2(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) + (Some(Permit2::new(swapper_pk, chain.clone())?), "splitSwapPermit2(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) } else { ( None, - "swap(uint256,address,address,uint256,bool,bool,uint256,address,bytes)".to_string(), + "splitSwap(uint256,address,address,uint256,bool,bool,uint256,address,bytes)" + .to_string(), ) }; Ok(Self { @@ -629,7 +630,7 @@ mod tests { .unwrap(); let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); let expected_input = [ - "d499aa88", // Function selector + "7c553846", // Function selector "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out @@ -950,7 +951,7 @@ mod tests { .unwrap(); let expected_input = [ - "d499aa88", // Function selector + "7c553846", // Function selector "000000000000000000000000000000000000000000000000000000003b9aca00", // amount in "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // token in "0000000000000000000000006982508145454ce325ddbe47a25d4ec3d2311933", // token out @@ -1060,7 +1061,7 @@ mod tests { .unwrap(); let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); let expected_input = [ - "0a83cb08", // Function selector + "79b9b93b", // Function selector "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out @@ -1300,7 +1301,7 @@ mod tests { .unwrap(); let hex_calldata = hex::encode(&calldata); let expected_input = [ - "d499aa88", // selector + "7c553846", // selector "0000000000000000000000000000000000000000000000000000000005f5e100", // given amount "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // given token "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // checked token @@ -1452,7 +1453,7 @@ mod tests { let hex_calldata = hex::encode(&calldata); let expected_input = [ - "d499aa88", // selector + "7c553846", // selector "0000000000000000000000000000000000000000000000000000000005f5e100", // given amount "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // given token "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // checked token @@ -1609,7 +1610,7 @@ mod tests { let hex_calldata = hex::encode(&calldata); let expected_input = [ - "d499aa88", // selector + "7c553846", // selector "0000000000000000000000000000000000000000000000000000000005f5e100", // given amount "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // given token "000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // checked token From 65178681e0606fc41bbeaa15aef738388f0d23f8 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Sun, 16 Mar 2025 00:22:16 -0400 Subject: [PATCH 02/26] docs: Rename to splitSwap in helper method docs too Took 4 minutes --- foundry/src/TychoRouter.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 4bfef7c..19e6b9c 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -223,11 +223,11 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { } /** - * @notice Internal implementation of the core swap logic shared between swap() and swapPermit2(). + * @notice Internal implementation of the core swap logic shared between splitSwap() and splitSwapPermit2(). * * @notice This function centralizes the swap execution logic. * @notice For detailed documentation on parameters and behavior, see the documentation for - * swap() and swapPermit2() functions. + * splitSwap() and splitSwapPermit2() functions. * */ function _splitSwapChecked( From 3ae9d3ad670e760e2f7d0cbe81e5b19b7e7a3dae Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Mon, 17 Mar 2025 23:50:39 -0400 Subject: [PATCH 03/26] feat: Add sequential swap methods - Add some basic tests (will add more in next commits) - Adapt sequential tests (which originally tested split swap) - Adapt a forgotten single swap test TODO: - Fix encoding of single and sequential swaps to not expect the split sawp format every time (the split and the token indices are not necessary and consume unnecessary gas). --- foundry/src/TychoRouter.sol | 196 ++++++++++++++++++++++++++ foundry/test/TychoRouter.t.sol | 11 +- foundry/test/TychoRouterTestSetup.sol | 19 +++ 3 files changed, 221 insertions(+), 5 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index a899469..e65a6b7 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -222,6 +222,112 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { ); } + /** + * @notice Executes a swap operation based on a predefined swap graph with no split routes. + * This function enables multi-step swaps, optional ETH wrapping/unwrapping, and validates the output amount + * against a user-specified minimum. This function performs a transferFrom to retrieve tokens from the caller. + * + * @dev + * - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token. + * - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver. + * - Swaps are executed sequentially using the `_swap` function. + * - A fee is deducted from the output token if `fee > 0`, and the remaining amount is sent to the receiver. + * - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater than 0. + * + * @param amountIn The input token amount to be swapped. + * @param tokenIn The address of the input token. Use `address(0)` for native ETH + * @param tokenOut The address of the output token. Use `address(0)` for native ETH + * @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage. + * @param wrapEth If true, wraps the input token (native ETH) into WETH. + * @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver. + * @param receiver The address to receive the output tokens. + * @param swaps Encoded swap graph data containing details of each swap. + * + * @return amountOut The total amount of the output token received by the receiver, after deducting fees if applicable. + */ + function sequentialSwap( + uint256 amountIn, + address tokenIn, + address tokenOut, + uint256 minAmountOut, + bool wrapEth, + bool unwrapEth, + address receiver, + bytes calldata swaps + ) public payable whenNotPaused nonReentrant returns (uint256 amountOut) { + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + return _sequentialSwapChecked( + amountIn, + tokenIn, + tokenOut, + minAmountOut, + wrapEth, + unwrapEth, + receiver, + swaps + ); + } + + /** + * @notice Executes a swap operation based on a predefined swap graph with no split routes. + * This function enables multi-step swaps, optional ETH wrapping/unwrapping, and validates the output amount + * against a user-specified minimum. + * + * @dev + * - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token. + * - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver. + * - For ERC20 tokens, Permit2 is used to approve and transfer tokens from the caller to the router. + * - A fee is deducted from the output token if `fee > 0`, and the remaining amount is sent to the receiver. + * - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater than 0. + * + * @param amountIn The input token amount to be swapped. + * @param tokenIn The address of the input token. Use `address(0)` for native ETH + * @param tokenOut The address of the output token. Use `address(0)` for native ETH + * @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage. + * @param wrapEth If true, wraps the input token (native ETH) into WETH. + * @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver. + * @param receiver The address to receive the output tokens. + * @param permitSingle A Permit2 structure containing token approval details for the input token. Ignored if `wrapEth` is true. + * @param signature A valid signature authorizing the Permit2 approval. Ignored if `wrapEth` is true. + * @param swaps Encoded swap graph data containing details of each swap. + * + * @return amountOut The total amount of the output token received by the receiver, after deducting fees if applicable. + */ + function sequentialSwapPermit2( + uint256 amountIn, + address tokenIn, + address tokenOut, + uint256 minAmountOut, + bool wrapEth, + bool unwrapEth, + address receiver, + IAllowanceTransfer.PermitSingle calldata permitSingle, + bytes calldata signature, + bytes calldata swaps + ) external payable whenNotPaused nonReentrant returns (uint256 amountOut) { + // For native ETH, assume funds already in our router. Else, transfer and handle approval. + if (tokenIn != address(0)) { + permit2.permit(msg.sender, permitSingle, signature); + permit2.transferFrom( + msg.sender, + address(this), + uint160(amountIn), + permitSingle.details.token + ); + } + + return _sequentialSwapChecked( + amountIn, + tokenIn, + tokenOut, + minAmountOut, + wrapEth, + unwrapEth, + receiver, + swaps + ); + } + /** * @notice Executes a single swap operation. * This function enables optional ETH wrapping/unwrapping, and validates the output amount against a user-specified minimum. @@ -472,6 +578,75 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { } } + /** + * @notice Internal implementation of the core swap logic shared between sequentialSwap() and sequentialSwapPermit2(). + * + * @notice This function centralizes the swap execution logic. + * @notice For detailed documentation on parameters and behavior, see the documentation for + * sequentialSwap() and sequentialSwapPermit2() functions. + * + */ + function _sequentialSwapChecked( + uint256 amountIn, + address tokenIn, + address tokenOut, + uint256 minAmountOut, + bool wrapEth, + bool unwrapEth, + address receiver, + bytes calldata swaps + ) internal returns (uint256 amountOut) { + if (receiver == address(0)) { + revert TychoRouter__AddressZero(); + } + if (minAmountOut == 0) { + revert TychoRouter__UndefinedMinAmountOut(); + } + + // Assume funds are already in the router. + if (wrapEth) { + _wrapETH(amountIn); + tokenIn = address(_weth); + } + + uint256 initialBalance = tokenIn == address(0) + ? address(this).balance + : IERC20(tokenIn).balanceOf(address(this)); + + amountOut = _sequentialSwap(amountIn, swaps); + + uint256 currentBalance = tokenIn == address(0) + ? address(this).balance + : IERC20(tokenIn).balanceOf(address(this)); + + uint256 amountConsumed = initialBalance - currentBalance; + + if (tokenIn != tokenOut && amountConsumed != amountIn) { + revert TychoRouter__AmountInDiffersFromConsumed( + amountIn, amountConsumed + ); + } + + if (fee > 0) { + uint256 feeAmount = (amountOut * fee) / 10000; + amountOut -= feeAmount; + IERC20(tokenOut).safeTransfer(feeReceiver, feeAmount); + } + + if (amountOut < minAmountOut) { + revert TychoRouter__NegativeSlippage(amountOut, minAmountOut); + } + + if (unwrapEth) { + _unwrapETH(amountOut); + } + if (tokenOut == address(0)) { + Address.sendValue(payable(receiver), amountOut); + } else { + IERC20(tokenOut).safeTransfer(receiver, amountOut); + } + } + /** * @dev Executes sequential swaps as defined by the provided swap graph. * @@ -542,6 +717,27 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { return tokenOutIndex == 0 ? cyclicSwapAmountOut : amounts[tokenOutIndex]; } + /** + * @dev Executes sequential swaps as defined by the provided swap graph. + * + * @param amountIn The initial amount of the sell token to be swapped. + * @param swaps_ Encoded swap graph data containing the details of each swap operation. + * + * @return calculatedAmount The total amount of the buy token obtained after all swaps have been executed. + */ + function _sequentialSwap( + uint256 amountIn, + bytes calldata swaps_ + ) internal returns (uint256 calculatedAmount) { + bytes calldata swap; + calculatedAmount = amountIn; + while (swaps_.length > 0) { + (swap, swaps_) = swaps_.next(); + calculatedAmount = + _callExecutor(swap.executor(), calculatedAmount, swap.protocolData()); + } + } + /** * @dev We use the fallback function to allow flexibility on callback. */ diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 2314bb2..d68e924 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -230,7 +230,7 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSplitSwapSimplePermit2() public { + function testSingleSwapSimplePermit2() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 using Permit2 // 1 WETH -> DAI // (USV2) @@ -274,7 +274,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSplitSwapMultipleHops() public { + function testSequentialSwapMultipleHops() public { // Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2 // 1 WETH -> DAI -> USDC // (univ2) (univ2) @@ -302,7 +302,7 @@ contract TychoRouterTest is TychoRouterTestSetup { encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) ); - tychoRouter.exposedSplitSwap(amountIn, 3, pleEncode(swaps)); + tychoRouter.exposedSequentialSwap(amountIn, pleEncode(swaps)); uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); assertEq(usdcBalance, 2644659787); @@ -965,7 +965,7 @@ contract TychoRouterTest is TychoRouterTestSetup { function testCyclicSequentialSwap() public { // This test has start and end tokens that are the same // The flow is: - // USDC -> WETH -> USDC using two pools + // USDC --(USV3)--> WETH --(USV3)--> USDC uint256 amountIn = 100 * 10 ** 6; deal(USDC_ADDR, tychoRouterAddr, amountIn); @@ -995,10 +995,11 @@ contract TychoRouterTest is TychoRouterTestSetup { usdcWethV3Pool2OneZeroData ); - tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); + tychoRouter.exposedSequentialSwap(amountIn, pleEncode(swaps)); assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99889294); } + function testSplitInputCyclicSwap() public { // This test has start and end tokens that are the same // The flow is: diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index a92f44a..54b86d6 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -29,6 +29,13 @@ contract TychoRouterExposed is TychoRouter { ) external returns (uint256) { return _splitSwap(amountIn, nTokens, swaps); } + + function exposedSequentialSwap( + uint256 amountIn, + bytes calldata swaps + ) external returns (uint256) { + return _sequentialSwap(amountIn, swaps); + } } contract TychoRouterTestSetup is Constants { @@ -193,6 +200,18 @@ contract TychoRouterTestSetup is Constants { ); } + function encodeSequentialSwap( + uint8 tokenInIndex, + uint8 tokenOutIndex, + uint24 split, + address executor, + bytes memory protocolData + ) internal pure returns (bytes memory) { + return abi.encodePacked( + tokenInIndex, tokenOutIndex, split, executor, protocolData + ); + } + function encodeUniswapV2Swap( address tokenIn, address target, From 64345663d085114310e71d3d1bd316858626bf83 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Sun, 16 Mar 2025 00:15:55 -0400 Subject: [PATCH 04/26] feat: Single swap methods. - Remove duplicate test case for single swp with permit2. This test came from the days when we could still have a 0 minimum amount. --- foundry/src/TychoRouter.sol | 181 +++++++++++++++++++++++++++++++++ foundry/test/TychoRouter.t.sol | 16 ++- 2 files changed, 187 insertions(+), 10 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 19e6b9c..3fceeb1 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -222,6 +222,117 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { ); } + /** + * @notice Executes a single swap operation. + * This function enables optional ETH wrapping/unwrapping, and validates the output amount against a user-specified minimum. + * This function performs a transferFrom to retrieve tokens from the caller. + * + * @dev + * - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token. + * - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver. + * - A fee is deducted from the output token if `fee > 0`, and the remaining amount is sent to the receiver. + * - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater than 0. + * + * @param amountIn The input token amount to be swapped. + * @param tokenIn The address of the input token. Use `address(0)` for native ETH + * @param tokenOut The address of the output token. Use `address(0)` for native ETH + * @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage. + * @param wrapEth If true, wraps the input token (native ETH) into WETH. + * @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver. + * @param nTokens The total number of tokens involved in the swap graph (used to initialize arrays for internal calculations). + * @param receiver The address to receive the output tokens. + * @param swapData Encoded swap details. + * + * @return amountOut The total amount of the output token received by the receiver, after deducting fees if applicable. + */ + function singleSwap( + uint256 amountIn, + address tokenIn, + address tokenOut, + uint256 minAmountOut, + bool wrapEth, + bool unwrapEth, + uint256 nTokens, + address receiver, + bytes calldata swapData + ) public payable whenNotPaused nonReentrant returns (uint256 amountOut) { + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + return _singleSwap( + amountIn, + tokenIn, + tokenOut, + minAmountOut, + wrapEth, + unwrapEth, + nTokens, + receiver, + swapData + ); + } + + /** + * @notice Executes a single swap operation. + * This function enables optional ETH wrapping/unwrapping, and validates the output amount + * against a user-specified minimum. + * + * @dev + * - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token. + * - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver. + * - For ERC20 tokens, Permit2 is used to approve and transfer tokens from the caller to the router. + * - A fee is deducted from the output token if `fee > 0`, and the remaining amount is sent to the receiver. + * - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater than 0. + * + * @param amountIn The input token amount to be swapped. + * @param tokenIn The address of the input token. Use `address(0)` for native ETH + * @param tokenOut The address of the output token. Use `address(0)` for native ETH + * @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage. + * @param wrapEth If true, wraps the input token (native ETH) into WETH. + * @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver. + * @param nTokens The total number of tokens involved in the swap graph (used to initialize arrays for internal calculations). + * @param receiver The address to receive the output tokens. + * @param permitSingle A Permit2 structure containing token approval details for the input token. Ignored if `wrapEth` is true. + * @param signature A valid signature authorizing the Permit2 approval. Ignored if `wrapEth` is true. + * @param swapData Encoded swap details. + * + * @return amountOut The total amount of the output token received by the receiver, after deducting fees if applicable. + */ + function singleSwapPermit2( + uint256 amountIn, + address tokenIn, + address tokenOut, + uint256 minAmountOut, + bool wrapEth, + bool unwrapEth, + uint256 nTokens, + address receiver, + IAllowanceTransfer.PermitSingle calldata permitSingle, + bytes calldata signature, + bytes calldata swapData + ) external payable whenNotPaused nonReentrant returns (uint256 amountOut) { + // For native ETH, assume funds already in our router. Else, transfer and handle approval. + if (tokenIn != address(0)) { + permit2.permit(msg.sender, permitSingle, signature); + permit2.transferFrom( + msg.sender, + address(this), + uint160(amountIn), + permitSingle.details.token + ); + } + + return _singleSwap( + amountIn, + tokenIn, + tokenOut, + minAmountOut, + wrapEth, + unwrapEth, + nTokens, + receiver, + swapData + ); + } + /** * @notice Internal implementation of the core swap logic shared between splitSwap() and splitSwapPermit2(). * @@ -291,6 +402,76 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { } } + /** + * @notice Internal implementation of the core swap logic shared between singleSwap() and singleSwapPermit2(). + * + * @notice This function centralizes the swap execution logic. + * @notice For detailed documentation on parameters and behavior, see the documentation for + * singleSwap() and singleSwapPermit2() functions. + * + */ + function _singleSwap( + uint256 amountIn, + address tokenIn, + address tokenOut, + uint256 minAmountOut, + bool wrapEth, + bool unwrapEth, + uint256 nTokens, + address receiver, + bytes calldata swap_ + ) internal returns (uint256 amountOut) { + if (receiver == address(0)) { + revert TychoRouter__AddressZero(); + } + if (minAmountOut == 0) { + revert TychoRouter__UndefinedMinAmountOut(); + } + + // Assume funds are already in the router. + if (wrapEth) { + _wrapETH(amountIn); + tokenIn = address(_weth); + } + + uint256 initialBalance = tokenIn == address(0) + ? address(this).balance + : IERC20(tokenIn).balanceOf(address(this)); + + amountOut = + _callExecutor(swap_.executor(), amountIn, swap_.protocolData()); + uint256 currentBalance = tokenIn == address(0) + ? address(this).balance + : IERC20(tokenIn).balanceOf(address(this)); + + uint256 amountConsumed = initialBalance - currentBalance; + + if (tokenIn != tokenOut && amountConsumed != amountIn) { + revert TychoRouter__AmountInDiffersFromConsumed( + amountIn, amountConsumed + ); + } + + if (fee > 0) { + uint256 feeAmount = (amountOut * fee) / 10000; + amountOut -= feeAmount; + IERC20(tokenOut).safeTransfer(feeReceiver, feeAmount); + } + + if (amountOut < minAmountOut) { + revert TychoRouter__NegativeSlippage(amountOut, minAmountOut); + } + + if (unwrapEth) { + _unwrapETH(amountOut); + } + if (tokenOut == address(0)) { + Address.sendValue(payable(receiver), amountOut); + } else { + IERC20(tokenOut).safeTransfer(receiver, amountOut); + } + } + /** * @dev Executes sequential swaps as defined by the provided swap graph. * diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index ec0c5f2..2314bb2 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -366,7 +366,7 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSplitSwapChecked() public { + function testSingleSwapChecked() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 // Does permit2 token approval and transfer // Checks amount out at the end @@ -387,11 +387,9 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; uint256 minAmountOut = 2600 * 1e18; - uint256 amountOut = tychoRouter.splitSwapPermit2( + uint256 amountOut = tychoRouter.singleSwapPermit2( amountIn, WETH_ADDR, DAI_ADDR, @@ -402,7 +400,7 @@ contract TychoRouterTest is TychoRouterTestSetup { ALICE, permitSingle, signature, - pleEncode(swaps) + swap ); uint256 expectedAmount = 2659881924818443699787; @@ -454,7 +452,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSplitSwapCheckedNoPermit2() public { + function testSingleSwapCheckedNoPermit2() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 // Checks amount out at the end uint256 amountIn = 1 ether; @@ -471,11 +469,9 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes memory swap = encodeSplitSwap( uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; uint256 minAmountOut = 2600 * 1e18; - uint256 amountOut = tychoRouter.splitSwap( + uint256 amountOut = tychoRouter.singleSwap( amountIn, WETH_ADDR, DAI_ADDR, @@ -484,7 +480,7 @@ contract TychoRouterTest is TychoRouterTestSetup { false, 2, ALICE, - pleEncode(swaps) + swap ); uint256 expectedAmount = 2659881924818443699787; From 1dad4afb6b91cf86ea000afeaeba8882af73b713 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 18 Mar 2025 01:37:14 -0400 Subject: [PATCH 05/26] feat: Decode single and sequential swaps in LibSwap - The old way was useful when we just had split swaps. Unfortunately, we now have split, sequential, and single swaps, which don't always require token indices or split percentages, so we need to decode differently for each case. --- foundry/lib/LibSwap.sol | 60 ++++++++++++----------- foundry/src/TychoRouter.sol | 42 ++++++++--------- foundry/test/LibSwap.t.sol | 68 +++++++++++++++++++++++---- foundry/test/TychoRouter.t.sol | 55 +++++++--------------- foundry/test/TychoRouterTestSetup.sol | 34 ++++++++------ 5 files changed, 148 insertions(+), 111 deletions(-) diff --git a/foundry/lib/LibSwap.sol b/foundry/lib/LibSwap.sol index f73a759..a892594 100644 --- a/foundry/lib/LibSwap.sol +++ b/foundry/lib/LibSwap.sol @@ -2,36 +2,42 @@ pragma solidity ^0.8.26; library LibSwap { - /// Returns the InToken index into an array of tokens - function tokenInIndex( - bytes calldata swap - ) internal pure returns (uint8 res) { - res = uint8(swap[0]); + /** + * @dev Returns arguments required to perform a single swap + */ + function decodeSingleSwap(bytes calldata swap) + internal + pure + returns (address executor, bytes calldata protocolData) + { + executor = address(uint160(bytes20(swap[0:20]))); + protocolData = swap[20:]; } - /// The OutToken index into an array of tokens - function tokenOutIndex( - bytes calldata swap - ) internal pure returns (uint8 res) { - res = uint8(swap[1]); + /** + * @dev Returns arguments required to perform a sequential swap + */ + function decodeSequentialSwap(bytes calldata swap) + internal + pure + returns (address executor, bytes calldata protocolData) + { + executor = address(uint160(bytes20(swap[0:20]))); + protocolData = swap[20:]; } - /// The relative amount of token quantity routed into this swap - function splitPercentage( - bytes calldata swap - ) internal pure returns (uint24 res) { - res = uint24(bytes3(swap[2:5])); - } - - /// The address of the executor contract - function executor(bytes calldata swap) internal pure returns (address res) { - res = address(uint160(bytes20(swap[5:25]))); - } - - /// Remaining bytes are interpreted as protocol data - function protocolData( - bytes calldata swap - ) internal pure returns (bytes calldata res) { - res = swap[25:]; + /** + * @dev Returns arguments required to perform a split swap + */ + function decodeSplitSwap(bytes calldata swap) + internal + pure + returns (uint8 tokenInIndex, uint8 tokenOutIndex, uint24 split, address executor, bytes calldata protocolData) + { + tokenInIndex = uint8(swap[0]); + tokenOutIndex = uint8(swap[1]); + split = uint24(bytes3(swap[2:5])); + executor = address(uint160(bytes20(swap[5:25]))); + protocolData = swap[25:]; } } diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index e65a6b7..0bce8dc 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -222,7 +222,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { ); } - /** + /** * @notice Executes a swap operation based on a predefined swap graph with no split routes. * This function enables multi-step swaps, optional ETH wrapping/unwrapping, and validates the output amount * against a user-specified minimum. This function performs a transferFrom to retrieve tokens from the caller. @@ -345,7 +345,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { * @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage. * @param wrapEth If true, wraps the input token (native ETH) into WETH. * @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver. - * @param nTokens The total number of tokens involved in the swap graph (used to initialize arrays for internal calculations). * @param receiver The address to receive the output tokens. * @param swapData Encoded swap details. * @@ -358,7 +357,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { uint256 minAmountOut, bool wrapEth, bool unwrapEth, - uint256 nTokens, address receiver, bytes calldata swapData ) public payable whenNotPaused nonReentrant returns (uint256 amountOut) { @@ -370,7 +368,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { minAmountOut, wrapEth, unwrapEth, - nTokens, receiver, swapData ); @@ -394,7 +391,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { * @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage. * @param wrapEth If true, wraps the input token (native ETH) into WETH. * @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver. - * @param nTokens The total number of tokens involved in the swap graph (used to initialize arrays for internal calculations). * @param receiver The address to receive the output tokens. * @param permitSingle A Permit2 structure containing token approval details for the input token. Ignored if `wrapEth` is true. * @param signature A valid signature authorizing the Permit2 approval. Ignored if `wrapEth` is true. @@ -409,7 +405,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { uint256 minAmountOut, bool wrapEth, bool unwrapEth, - uint256 nTokens, address receiver, IAllowanceTransfer.PermitSingle calldata permitSingle, bytes calldata signature, @@ -433,7 +428,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { minAmountOut, wrapEth, unwrapEth, - nTokens, receiver, swapData ); @@ -523,7 +517,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { uint256 minAmountOut, bool wrapEth, bool unwrapEth, - uint256 nTokens, address receiver, bytes calldata swap_ ) internal returns (uint256 amountOut) { @@ -544,8 +537,10 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { ? address(this).balance : IERC20(tokenIn).balanceOf(address(this)); - amountOut = - _callExecutor(swap_.executor(), amountIn, swap_.protocolData()); + (address executor, bytes calldata protocolData) = + swap_.decodeSingleSwap(); + + amountOut = _callExecutor(executor, amountIn, protocolData); uint256 currentBalance = tokenIn == address(0) ? address(this).balance : IERC20(tokenIn).balanceOf(address(this)); @@ -684,6 +679,8 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { uint8 tokenInIndex = 0; uint8 tokenOutIndex = 0; uint24 split; + address executor; + bytes calldata protocolData; bytes calldata swapData; uint256[] memory remainingAmounts = new uint256[](nTokens); @@ -694,17 +691,16 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { while (swaps_.length > 0) { (swapData, swaps_) = swaps_.next(); - tokenInIndex = swapData.tokenInIndex(); - tokenOutIndex = swapData.tokenOutIndex(); - split = swapData.splitPercentage(); + + (tokenInIndex, tokenOutIndex, split, executor, protocolData) = + swapData.decodeSplitSwap(); currentAmountIn = split > 0 ? (amounts[tokenInIndex] * split) / 0xffffff : remainingAmounts[tokenInIndex]; - currentAmountOut = _callExecutor( - swapData.executor(), currentAmountIn, swapData.protocolData() - ); + currentAmountOut = + _callExecutor(executor, currentAmountIn, protocolData); // Checks if the output token is the same as the input token if (tokenOutIndex == 0) { cyclicSwapAmountOut += currentAmountOut; @@ -725,16 +721,20 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { * * @return calculatedAmount The total amount of the buy token obtained after all swaps have been executed. */ - function _sequentialSwap( - uint256 amountIn, - bytes calldata swaps_ - ) internal returns (uint256 calculatedAmount) { + function _sequentialSwap(uint256 amountIn, bytes calldata swaps_) + internal + returns (uint256 calculatedAmount) + { bytes calldata swap; calculatedAmount = amountIn; while (swaps_.length > 0) { (swap, swaps_) = swaps_.next(); + + (address executor, bytes calldata protocolData) = + swap.decodeSingleSwap(); + calculatedAmount = - _callExecutor(swap.executor(), calculatedAmount, swap.protocolData()); + _callExecutor(executor, calculatedAmount, protocolData); } } diff --git a/foundry/test/LibSwap.t.sol b/foundry/test/LibSwap.t.sol index 75c94f7..326df54 100644 --- a/foundry/test/LibSwap.t.sol +++ b/foundry/test/LibSwap.t.sol @@ -7,7 +7,45 @@ import "../lib/LibSwap.sol"; contract LibSwapTest is Test { using LibSwap for bytes; - function testSwap() public view { + function testSingleSwap() public view { + address executor = 0x1234567890123456789012345678901234567890; + bytes memory protocolData = abi.encodePacked(uint256(123)); + + bytes memory swap = abi.encodePacked(executor, protocolData); + this.assertSingleSwap(swap, executor, protocolData); + } + + function assertSingleSwap( + bytes calldata swap, + address executor, + bytes calldata protocolData + ) public pure { + (address decodedExecutor, bytes memory decodedProtocolData) = + swap.decodeSingleSwap(); + assertEq(decodedExecutor, executor); + assertEq(decodedProtocolData, protocolData); + } + + function testSequentialSwap() public view { + address executor = 0x1234567890123456789012345678901234567890; + bytes memory protocolData = abi.encodePacked(uint256(234)); + + bytes memory swap = abi.encodePacked(executor, protocolData); + this.assertSequentialSwap(swap, executor, protocolData); + } + + function assertSequentialSwap( + bytes calldata swap, + address executor, + bytes calldata protocolData + ) public pure { + (address decodedExecutor, bytes memory decodedProtocolData) = + swap.decodeSequentialSwap(); + assertEq(decodedExecutor, executor); + assertEq(decodedProtocolData, protocolData); + } + + function testSplitSwap() public view { uint8 tokenInIndex = 1; uint8 tokenOutIndex = 2; uint24 split = 3; @@ -17,20 +55,32 @@ contract LibSwapTest is Test { bytes memory swap = abi.encodePacked( tokenInIndex, tokenOutIndex, split, executor, protocolData ); - this.assertSwap(swap, tokenInIndex, tokenOutIndex, split, executor); + this.assertSplitSwap( + swap, tokenInIndex, tokenOutIndex, split, executor, protocolData + ); } - // This is necessary so that the compiler accepts bytes as a LibSwap.sol - function assertSwap( + // This is necessary so that the compiler accepts bytes as a LibSwap.sol for testing + // This is because this function takes calldata as input + function assertSplitSwap( bytes calldata swap, uint8 tokenInIndex, uint8 tokenOutIndex, uint24 split, - address executor + address executor, + bytes calldata protocolData ) public pure { - assert(swap.tokenInIndex() == tokenInIndex); - assert(swap.tokenOutIndex() == tokenOutIndex); - assert(swap.splitPercentage() == split); - assert(swap.executor() == executor); + ( + uint8 decodedTokenInIndex, + uint8 decodedTokenOutIndex, + uint24 decodedSplit, + address decodedExecutor, + bytes memory decodedProtocolData + ) = swap.decodeSplitSwap(); + assertEq(decodedTokenInIndex, tokenInIndex); + assertEq(decodedTokenOutIndex, tokenOutIndex); + assertEq(decodedSplit, split); + assertEq(decodedExecutor, executor); + assertEq(decodedProtocolData, protocolData); } } diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index d68e924..6ee6582 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -230,7 +230,7 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSingleSwapSimplePermit2() public { + function testSingleSwapPermit2() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 using Permit2 // 1 WETH -> DAI // (USV2) @@ -247,24 +247,20 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); - tychoRouter.splitSwapPermit2( + tychoRouter.singleSwapPermit2( amountIn, WETH_ADDR, DAI_ADDR, 2659881924818443699786, false, false, - 2, ALICE, permitSingle, signature, - pleEncode(swaps) + swap ); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); @@ -274,7 +270,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testSequentialSwapMultipleHops() public { + function testSequentialSwap() public { // Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2 // 1 WETH -> DAI -> USDC // (univ2) (univ2) @@ -283,10 +279,7 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes[] memory swaps = new bytes[](2); // WETH -> DAI - swaps[0] = encodeSplitSwap( - uint8(0), - uint8(1), - uint24(0), + swaps[0] = encodeSequentialSwap( address(usv2Executor), encodeUniswapV2Swap( WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false @@ -294,10 +287,7 @@ contract TychoRouterTest is TychoRouterTestSetup { ); // DAI -> USDC - swaps[1] = encodeSplitSwap( - uint8(1), - uint8(2), - uint24(0), + swaps[1] = encodeSequentialSwap( address(usv2Executor), encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) ); @@ -384,9 +374,8 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); uint256 minAmountOut = 2600 * 1e18; uint256 amountOut = tychoRouter.singleSwapPermit2( @@ -396,7 +385,6 @@ contract TychoRouterTest is TychoRouterTestSetup { minAmountOut, false, false, - 2, ALICE, permitSingle, signature, @@ -466,9 +454,8 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); uint256 minAmountOut = 2600 * 1e18; uint256 amountOut = tychoRouter.singleSwap( @@ -478,7 +465,6 @@ contract TychoRouterTest is TychoRouterTestSetup { minAmountOut, false, false, - 2, ALICE, swap ); @@ -979,27 +965,18 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes[] memory swaps = new bytes[](2); // USDC -> WETH - swaps[0] = encodeSplitSwap( - uint8(0), - uint8(1), - uint24(0), - address(usv3Executor), - usdcWethV3Pool1ZeroOneData + swaps[0] = encodeSequentialSwap( + address(usv3Executor), usdcWethV3Pool1ZeroOneData ); // WETH -> USDC - swaps[1] = encodeSplitSwap( - uint8(1), - uint8(0), - uint24(0), - address(usv3Executor), - usdcWethV3Pool2OneZeroData + swaps[1] = encodeSequentialSwap( + address(usv3Executor), usdcWethV3Pool2OneZeroData ); tychoRouter.exposedSequentialSwap(amountIn, pleEncode(swaps)); assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99889294); } - function testSplitInputCyclicSwap() public { // This test has start and end tokens that are the same // The flow is: diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 54b86d6..edbbe54 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -30,10 +30,10 @@ contract TychoRouterExposed is TychoRouter { return _splitSwap(amountIn, nTokens, swaps); } - function exposedSequentialSwap( - uint256 amountIn, - bytes calldata swaps - ) external returns (uint256) { + function exposedSequentialSwap(uint256 amountIn, bytes calldata swaps) + external + returns (uint256) + { return _sequentialSwap(amountIn, swaps); } } @@ -188,19 +188,23 @@ contract TychoRouterTestSetup is Constants { } } - function encodeSplitSwap( - uint8 tokenInIndex, - uint8 tokenOutIndex, - uint24 split, - address executor, - bytes memory protocolData - ) internal pure returns (bytes memory) { - return abi.encodePacked( - tokenInIndex, tokenOutIndex, split, executor, protocolData - ); + function encodeSingleSwap(address executor, bytes memory protocolData) + internal + pure + returns (bytes memory) + { + return abi.encodePacked(executor, protocolData); } - function encodeSequentialSwap( + function encodeSequentialSwap(address executor, bytes memory protocolData) + internal + pure + returns (bytes memory) + { + return abi.encodePacked(executor, protocolData); + } + + function encodeSplitSwap( uint8 tokenInIndex, uint8 tokenOutIndex, uint24 split, From 49e0c498284e73f14a70900052fd4d845c3a732c Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Mon, 17 Mar 2025 13:18:02 -0400 Subject: [PATCH 06/26] chore: (single swap) remove unnecessary cycle check - It's not possible to have a single cyclical 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 3fceeb1..a899469 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -446,7 +446,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { uint256 amountConsumed = initialBalance - currentBalance; - if (tokenIn != tokenOut && amountConsumed != amountIn) { + if (amountConsumed != amountIn) { revert TychoRouter__AmountInDiffersFromConsumed( amountIn, amountConsumed ); From c4455dcabb499ce16979058b59878f2ab0a4a344 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 18 Mar 2025 21:13:27 -0400 Subject: [PATCH 07/26] refactor: Separate test files for split/sequential/single - Add more test cases for sequential swap and single swap to match those of split swap (fee, slippage), to catch errors more easily if someone makes a small code change to either the single or sequential methods - Excluded USV3 and USV4 tests on sequential and single swap, since these tests were more to test USV3 and USV4 executor functionality than the high level sswap methods - they should already be sufficiently tested. - Remove `testSplitSwapSimple` and `testSplitSwapSingleUSV3` since this is already tested by several other high-level methods (see single USV3 and single USV4 tests). We should prioritize integration-testing public methods over private methods. --- foundry/test/TychoRouter.t.sol | 889 ------------------- foundry/test/TychoRouterSequentialSwap.t.sol | 344 +++++++ foundry/test/TychoRouterSingleSwap.t.sol | 307 +++++++ foundry/test/TychoRouterSplitSwap.t.sol | 683 ++++++++++++++ 4 files changed, 1334 insertions(+), 889 deletions(-) create mode 100644 foundry/test/TychoRouterSequentialSwap.t.sol create mode 100644 foundry/test/TychoRouterSingleSwap.t.sol create mode 100644 foundry/test/TychoRouterSplitSwap.t.sol diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 6ee6582..4a79e5c 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -206,899 +206,10 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSplitSwapSimple() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // 1 WETH -> DAI - // (USV2) - uint256 amountIn = 1 ether; - deal(WETH_ADDR, tychoRouterAddr, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); - - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr); - assertEq(daiBalance, 2659881924818443699787); - assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); - } - - function testSingleSwapPermit2() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 using Permit2 - // 1 WETH -> DAI - // (USV2) - vm.startPrank(ALICE); - - uint256 amountIn = 1 ether; - deal(WETH_ADDR, ALICE, amountIn); - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = - encodeSingleSwap(address(usv2Executor), protocolData); - - tychoRouter.singleSwapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - 2659881924818443699786, - false, - false, - ALICE, - permitSingle, - signature, - swap - ); - - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, 2659881924818443699787); - assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); - - vm.stopPrank(); - } - - function testSequentialSwap() public { - // Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2 - // 1 WETH -> DAI -> USDC - // (univ2) (univ2) - uint256 amountIn = 1 ether; - deal(WETH_ADDR, tychoRouterAddr, amountIn); - - bytes[] memory swaps = new bytes[](2); - // WETH -> DAI - swaps[0] = encodeSequentialSwap( - address(usv2Executor), - encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ) - ); - - // DAI -> USDC - swaps[1] = encodeSequentialSwap( - address(usv2Executor), - encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) - ); - - tychoRouter.exposedSequentialSwap(amountIn, pleEncode(swaps)); - - uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); - assertEq(usdcBalance, 2644659787); - assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); - } - - function testSplitSwapSplitHops() public { - // Trade 1 WETH for USDC through DAI and WBTC with 4 swaps on Uniswap V2 - // -> DAI -> - // 1 WETH USDC - // -> WBTC -> - // (univ2) (univ2) - uint256 amountIn = 1 ether; - deal(WETH_ADDR, tychoRouterAddr, amountIn); - - bytes[] memory swaps = new bytes[](4); - // WETH -> WBTC (60%) - swaps[0] = encodeSplitSwap( - uint8(0), - uint8(1), - (0xffffff * 60) / 100, // 60% - address(usv2Executor), - encodeUniswapV2Swap( - WETH_ADDR, WETH_WBTC_POOL, tychoRouterAddr, false - ) - ); - // WBTC -> USDC - swaps[1] = encodeSplitSwap( - uint8(1), - uint8(2), - uint24(0), - address(usv2Executor), - encodeUniswapV2Swap( - WBTC_ADDR, USDC_WBTC_POOL, tychoRouterAddr, true - ) - ); - // WETH -> DAI - swaps[2] = encodeSplitSwap( - uint8(0), - uint8(3), - uint24(0), - address(usv2Executor), - encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ) - ); - - // DAI -> USDC - swaps[3] = encodeSplitSwap( - uint8(3), - uint8(2), - uint24(0), - address(usv2Executor), - encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) - ); - - tychoRouter.exposedSplitSwap(amountIn, 4, pleEncode(swaps)); - - uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); - assertEq(usdcBalance, 2615491639); - assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); - } - - function testSingleSwapChecked() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // Does permit2 token approval and transfer - // Checks amount out at the end - uint256 amountIn = 1 ether; - deal(WETH_ADDR, ALICE, amountIn); - - vm.startPrank(ALICE); - - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = - encodeSingleSwap(address(usv2Executor), protocolData); - - uint256 minAmountOut = 2600 * 1e18; - uint256 amountOut = tychoRouter.singleSwapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - minAmountOut, - false, - false, - ALICE, - permitSingle, - signature, - swap - ); - - uint256 expectedAmount = 2659881924818443699787; - assertEq(amountOut, expectedAmount); - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, expectedAmount); - assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); - - vm.stopPrank(); - } - - function testSplitSwapCheckedUndefinedMinAmount() public { - // Min amount should always be non-zero. If zero, swap attempt should revert. - - uint256 amountIn = 1 ether; - deal(WETH_ADDR, ALICE, amountIn); - - vm.startPrank(ALICE); - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - uint256 minAmountOut = 0; - - vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector); - tychoRouter.splitSwapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - minAmountOut, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - vm.stopPrank(); - } - - function testSingleSwapCheckedNoPermit2() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // Checks amount out at the end - uint256 amountIn = 1 ether; - - deal(WETH_ADDR, ALICE, amountIn); - vm.startPrank(ALICE); - // Approve the tokenIn to be transferred to the router - IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = - encodeSingleSwap(address(usv2Executor), protocolData); - - uint256 minAmountOut = 2600 * 1e18; - uint256 amountOut = tychoRouter.singleSwap( - amountIn, - WETH_ADDR, - DAI_ADDR, - minAmountOut, - false, - false, - ALICE, - swap - ); - - uint256 expectedAmount = 2659881924818443699787; - assertEq(amountOut, expectedAmount); - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, expectedAmount); - assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); - - vm.stopPrank(); - } - - function testSplitSwapCheckedLessApprovalFailure() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // Fails while transferring the tokenIn to the router due to insufficient approval - uint256 amountIn = 1 ether; - - deal(WETH_ADDR, ALICE, amountIn); - vm.startPrank(ALICE); - // Approve less than the amountIn - IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn - 1); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - uint256 minAmountOut = 2600 * 1e18; - vm.expectRevert(); - tychoRouter.splitSwap( - amountIn, - WETH_ADDR, - DAI_ADDR, - minAmountOut, - false, - false, - 2, - ALICE, - pleEncode(swaps) - ); - - vm.stopPrank(); - } - - function testSplitSwapCheckedNegativeSlippageFailure() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // Does permit2 token approval and transfer - // Checks amount out at the end and fails - uint256 amountIn = 1 ether; - deal(WETH_ADDR, ALICE, amountIn); - - vm.startPrank(ALICE); - - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - uint256 minAmountOut = 3000 * 1e18; - vm.expectRevert( - abi.encodeWithSelector( - TychoRouter__NegativeSlippage.selector, - 2659881924818443699787, // actual amountOut - minAmountOut - ) - ); - tychoRouter.splitSwapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - minAmountOut, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - vm.stopPrank(); - } - - function testSplitSwapFee() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // Does permit2 token approval and transfer - // Takes fee at the end - - vm.startPrank(FEE_SETTER); - tychoRouter.setFee(100); - tychoRouter.setFeeReceiver(FEE_RECEIVER); - vm.stopPrank(); - - uint256 amountIn = 1 ether; - deal(WETH_ADDR, ALICE, amountIn); - - vm.startPrank(ALICE); - - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - uint256 amountOut = tychoRouter.splitSwapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - 2633283105570259262780, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - - uint256 expectedAmount = 2633283105570259262790; - assertEq(amountOut, expectedAmount); - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, expectedAmount); - assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26598819248184436997); - - vm.stopPrank(); - } - - function testSplitSwapWrapETH() public { - // Trade 1 ETH (and wrap it) for DAI with 1 swap on Uniswap V2 - - uint256 amountIn = 1 ether; - deal(ALICE, amountIn); - - vm.startPrank(ALICE); - - IAllowanceTransfer.PermitSingle memory emptyPermitSingle = - IAllowanceTransfer.PermitSingle({ - details: IAllowanceTransfer.PermitDetails({ - token: address(0), - amount: 0, - expiration: 0, - nonce: 0 - }), - spender: address(0), - sigDeadline: 0 - }); - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - uint256 amountOut = tychoRouter.splitSwapPermit2{value: amountIn}( - amountIn, - address(0), - DAI_ADDR, - 2659881924818443699780, - true, - false, - 2, - ALICE, - emptyPermitSingle, - "", - pleEncode(swaps) - ); - uint256 expectedAmount = 2659881924818443699787; - assertEq(amountOut, expectedAmount); - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, expectedAmount); - assertEq(ALICE.balance, 0); - - vm.stopPrank(); - } - - function testSplitSwapUnwrapETH() public { - // Trade 3k DAI for WETH with 1 swap on Uniswap V2 and unwrap it at the end - - uint256 amountIn = 3_000 * 10 ** 18; - deal(DAI_ADDR, ALICE, amountIn); - - vm.startPrank(ALICE); - - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(DAI_ADDR, amountIn); - - bytes memory protocolData = - encodeUniswapV2Swap(DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true); - - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - uint256 amountOut = tychoRouter.splitSwapPermit2( - amountIn, - DAI_ADDR, - address(0), - 1120007305574805920, - false, - true, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - - uint256 expectedAmount = 1120007305574805922; // 1.12 ETH - assertEq(amountOut, expectedAmount); - assertEq(ALICE.balance, expectedAmount); - - vm.stopPrank(); - } - - function testSplitSwapSingleUSV3() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V3 - // 1 WETH -> DAI - // (USV3) - uint256 amountIn = 10 ** 18; - deal(WETH_ADDR, tychoRouterAddr, amountIn); - - uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI - bool zeroForOne = false; - bytes memory protocolData = encodeUniswapV3Swap( - WETH_ADDR, DAI_ADDR, tychoRouterAddr, DAI_WETH_USV3, zeroForOne - ); - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv3Executor), protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); - - uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr); - assertGe(finalBalance, expAmountOut); - } - - function testSplitSwapSingleUSV3Permit2() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V3 using Permit2 - // 1 WETH -> DAI - // (USV3) - vm.startPrank(ALICE); - uint256 amountIn = 10 ** 18; - deal(WETH_ADDR, ALICE, amountIn); - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amountIn); - - uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI - bool zeroForOne = false; - bytes memory protocolData = encodeUniswapV3Swap( - WETH_ADDR, DAI_ADDR, tychoRouterAddr, DAI_WETH_USV3, zeroForOne - ); - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv3Executor), protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.splitSwapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - expAmountOut - 1, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - - uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertGe(finalBalance, expAmountOut); - - vm.stopPrank(); - } - function testEmptySwapsRevert() public { uint256 amountIn = 10 ** 18; bytes memory swaps = ""; vm.expectRevert(TychoRouter__EmptySwaps.selector); tychoRouter.exposedSplitSwap(amountIn, 2, swaps); } - - function testSplitSwapAmountInNotFullySpent() public { - // Trade 1 WETH for DAI with 1 swap on Uniswap V2 - // Has invalid data as input! There is only one swap with 60% of the input amount - uint256 amountIn = 1 ether; - deal(WETH_ADDR, ALICE, amountIn); - - vm.startPrank(ALICE); - - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false - ); - - bytes memory swap = encodeSplitSwap( - uint8(0), - uint8(1), - (0xffffff * 60) / 100, // 60% - address(usv2Executor), - protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - vm.expectRevert( - abi.encodeWithSelector( - TychoRouter__AmountInDiffersFromConsumed.selector, - 1000000000000000000, - 600000000000000000 - ) - ); - - tychoRouter.splitSwapPermit2( - amountIn, - WETH_ADDR, - DAI_ADDR, - 1, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - - vm.stopPrank(); - } - - function testSplitSwapSingleUSV4Callback() public { - uint256 amountIn = 100 ether; - deal(USDE_ADDR, tychoRouterAddr, amountIn); - - UniswapV4Executor.UniswapV4Pool[] memory pools = - new UniswapV4Executor.UniswapV4Pool[](1); - pools[0] = UniswapV4Executor.UniswapV4Pool({ - intermediaryToken: USDT_ADDR, - fee: uint24(100), - tickSpacing: int24(1) - }); - - bytes memory protocolData = UniswapV4Utils.encodeExactInput( - USDE_ADDR, USDT_ADDR, true, address(usv4Executor), pools - ); - - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); - - assertEq(IERC20(USDT_ADDR).balanceOf(tychoRouterAddr), 99943852); - } - - function testSplitSwapSingleUSV4CallbackPermit2() public { - vm.startPrank(ALICE); - uint256 amountIn = 100 ether; - deal(USDE_ADDR, ALICE, amountIn); - ( - IAllowanceTransfer.PermitSingle memory permitSingle, - bytes memory signature - ) = handlePermit2Approval(USDE_ADDR, amountIn); - - UniswapV4Executor.UniswapV4Pool[] memory pools = - new UniswapV4Executor.UniswapV4Pool[](1); - pools[0] = UniswapV4Executor.UniswapV4Pool({ - intermediaryToken: USDT_ADDR, - fee: uint24(100), - tickSpacing: int24(1) - }); - - bytes memory protocolData = UniswapV4Utils.encodeExactInput( - USDE_ADDR, USDT_ADDR, true, address(usv4Executor), pools - ); - - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.splitSwapPermit2( - amountIn, - USDE_ADDR, - USDT_ADDR, - 99943850, - false, - false, - 2, - ALICE, - permitSingle, - signature, - pleEncode(swaps) - ); - - assertEq(IERC20(USDT_ADDR).balanceOf(ALICE), 99943852); - vm.stopPrank(); - } - - function testSplitSwapMultipleUSV4Callback() public { - // This test has two uniswap v4 hops that will be executed inside of the V4 pool manager - // USDE -> USDT -> WBTC - uint256 amountIn = 100 ether; - deal(USDE_ADDR, tychoRouterAddr, amountIn); - - UniswapV4Executor.UniswapV4Pool[] memory pools = - new UniswapV4Executor.UniswapV4Pool[](2); - pools[0] = UniswapV4Executor.UniswapV4Pool({ - intermediaryToken: USDT_ADDR, - fee: uint24(100), - tickSpacing: int24(1) - }); - pools[1] = UniswapV4Executor.UniswapV4Pool({ - intermediaryToken: WBTC_ADDR, - fee: uint24(3000), - tickSpacing: int24(60) - }); - - bytes memory protocolData = UniswapV4Utils.encodeExactInput( - USDE_ADDR, WBTC_ADDR, true, address(usv4Executor), pools - ); - - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData - ); - - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); - - assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718); - } - - function testCyclicSequentialSwap() public { - // This test has start and end tokens that are the same - // The flow is: - // USDC --(USV3)--> WETH --(USV3)--> USDC - 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 usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap( - WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false - ); - - bytes[] memory swaps = new bytes[](2); - // USDC -> WETH - swaps[0] = encodeSequentialSwap( - address(usv3Executor), usdcWethV3Pool1ZeroOneData - ); - // WETH -> USDC - swaps[1] = encodeSequentialSwap( - address(usv3Executor), usdcWethV3Pool2OneZeroData - ); - - tychoRouter.exposedSequentialSwap(amountIn, pleEncode(swaps)); - assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99889294); - } - - function testSplitInputCyclicSwap() public { - // This test has start and end tokens that are the same - // The flow is: - // ┌─ (USV3, 60% split) ──> WETH ─┐ - // │ │ - // USDC ──────┤ ├──(USV2)──> USDC - // │ │ - // └─ (USV3, 40% split) ──> 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 usdcWethV3Pool2ZeroOneData = encodeUniswapV3Swap( - USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, true - ); - - bytes memory wethUsdcV2OneZeroData = encodeUniswapV2Swap( - WETH_ADDR, USDC_WETH_USV2, tychoRouterAddr, false - ); - - bytes[] memory swaps = new bytes[](3); - // USDC -> WETH (60% split) - swaps[0] = encodeSplitSwap( - uint8(0), - uint8(1), - (0xffffff * 60) / 100, // 60% - address(usv3Executor), - usdcWethV3Pool1ZeroOneData - ); - // USDC -> WETH (40% remainder) - swaps[1] = encodeSplitSwap( - uint8(0), - uint8(1), - uint24(0), - address(usv3Executor), - usdcWethV3Pool2ZeroOneData - ); - // WETH -> USDC - swaps[2] = encodeSplitSwap( - uint8(1), - uint8(0), - uint24(0), - address(usv2Executor), - wethUsdcV2OneZeroData - ); - tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); - assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99574171); - } - - function testSplitOutputCyclicSwap() public { - // This test has start and end tokens that are the same - // The flow is: - // ┌─── (USV3, 60% split) ───┐ - // │ │ - // USDC ──(USV2) ── WETH──| ├─> USDC - // │ │ - // └─── (USV3, 40% split) ───┘ - - 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] = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), usdcWethV2Data - ); - // WETH -> USDC - swaps[1] = encodeSplitSwap( - uint8(1), - uint8(0), - (0xffffff * 60) / 100, - address(usv3Executor), - usdcWethV3Pool1OneZeroData - ); - - // WETH -> USDC - swaps[2] = encodeSplitSwap( - uint8(1), - uint8(0), - uint24(0), - address(usv3Executor), - usdcWethV3Pool2OneZeroData - ); - - tychoRouter.exposedSplitSwap(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 testSplitSwapSingleBase() public { - vm.skip(true); - vm.rollFork(26857267); - uint256 amountIn = 10 * 10 ** 6; - deal(BASE_USDC, tychoRouterAddr, amountIn); - - bytes memory protocolData = encodeUniswapV2Swap( - BASE_USDC, USDC_MAG7_POOL, tychoRouterAddr, true - ); - - bytes memory swap = encodeSplitSwap( - uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData - ); - bytes[] memory swaps = new bytes[](1); - swaps[0] = swap; - - tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); - assertGt(IERC20(BASE_MAG7).balanceOf(tychoRouterAddr), 1379830606); - } } diff --git a/foundry/test/TychoRouterSequentialSwap.t.sol b/foundry/test/TychoRouterSequentialSwap.t.sol new file mode 100644 index 0000000..75b179f --- /dev/null +++ b/foundry/test/TychoRouterSequentialSwap.t.sol @@ -0,0 +1,344 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@src/executors/UniswapV4Executor.sol"; +import {TychoRouter} from "@src/TychoRouter.sol"; +import "./TychoRouterTestSetup.sol"; +import "./executors/UniswapV4Utils.sol"; +import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol"; + +contract TychoRouterSequentialSwapTest is TychoRouterTestSetup { + bytes32 public constant FEE_SETTER_ROLE = + 0xe6ad9a47fbda1dc18de1eb5eeb7d935e5e81b4748f3cfc61e233e64f88182060; + + function _getSequentialSwaps() internal view returns (bytes[] memory) { + // Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2 + // 1 WETH -> DAI -> USDC + // (univ2) (univ2) + bytes[] memory swaps = new bytes[](2); + // WETH -> DAI + swaps[0] = encodeSequentialSwap( + address(usv2Executor), + encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ) + ); + + // DAI -> USDC + swaps[1] = encodeSequentialSwap( + address(usv2Executor), + encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) + ); + return swaps; + } + + function testSequentialSwapInternalMethod() public { + // Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info + uint256 amountIn = 1 ether; + deal(WETH_ADDR, tychoRouterAddr, amountIn); + + bytes[] memory swaps = _getSequentialSwaps(); + tychoRouter.exposedSequentialSwap(amountIn, pleEncode(swaps)); + + uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); + assertEq(usdcBalance, 2644659787); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } + + function testSequentialSwapPermit2() public { + // Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + bytes[] memory swaps = _getSequentialSwaps(); + tychoRouter.sequentialSwapPermit2( + amountIn, + WETH_ADDR, + USDC_ADDR, + 1000_000000, // min amount + false, + false, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + + uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE); + assertEq(usdcBalance, 2644659787); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } + + function testSequentialSwapNoPermit2() public { + // Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn); + + bytes[] memory swaps = _getSequentialSwaps(); + tychoRouter.sequentialSwap( + amountIn, + WETH_ADDR, + USDC_ADDR, + 1000_000000, // min amount + false, + false, + ALICE, + pleEncode(swaps) + ); + + uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE); + assertEq(usdcBalance, 2644659787); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } + + function testSequentialSwapUndefinedMinAmount() public { + // Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn); + + bytes[] memory swaps = _getSequentialSwaps(); + vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector); + tychoRouter.sequentialSwap( + amountIn, + WETH_ADDR, + USDC_ADDR, + 0, // min amount + false, + false, + ALICE, + pleEncode(swaps) + ); + } + + function testSequentialSwapInsufficientApproval() public { + // Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn - 1); + + bytes[] memory swaps = _getSequentialSwaps(); + vm.expectRevert(); + tychoRouter.sequentialSwap( + amountIn, + WETH_ADDR, + USDC_ADDR, + 0, // min amount + false, + false, + ALICE, + pleEncode(swaps) + ); + } + + function testSequentialSwapNegativeSlippageFailure() public { + // Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info + + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + vm.startPrank(ALICE); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + bytes[] memory swaps = _getSequentialSwaps(); + + uint256 minAmountOut = 3000 * 1e18; + + vm.expectRevert( + abi.encodeWithSelector( + TychoRouter__NegativeSlippage.selector, + 2644659787, // actual amountOut + minAmountOut + ) + ); + tychoRouter.sequentialSwapPermit2( + amountIn, + WETH_ADDR, + DAI_ADDR, + minAmountOut, + false, + false, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + vm.stopPrank(); + } + + function testSequentialSwapFee() public { + // Trade 1 WETH for USDC + // Takes 1% fee at the end + + vm.startPrank(FEE_SETTER); + tychoRouter.setFee(100); + tychoRouter.setFeeReceiver(FEE_RECEIVER); + vm.stopPrank(); + + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + bytes[] memory swaps = _getSequentialSwaps(); + + uint256 amountOut = tychoRouter.sequentialSwapPermit2( + amountIn, + WETH_ADDR, + USDC_ADDR, + 1000_000000, + false, + false, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + + uint256 expectedAmount = 2618213190; + assertEq(amountOut, expectedAmount); + uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE); + assertEq(usdcBalance, expectedAmount); + assertEq(IERC20(USDC_ADDR).balanceOf(FEE_RECEIVER), 26446597); + + vm.stopPrank(); + } + + function testSequentialSwapWrapETH() public { + uint256 amountIn = 1 ether; + deal(ALICE, amountIn); + vm.startPrank(ALICE); + + IAllowanceTransfer.PermitSingle memory emptyPermitSingle = + IAllowanceTransfer.PermitSingle({ + details: IAllowanceTransfer.PermitDetails({ + token: address(0), + amount: 0, + expiration: 0, + nonce: 0 + }), + spender: address(0), + sigDeadline: 0 + }); + + bytes[] memory swaps = _getSequentialSwaps(); + + uint256 amountOut = tychoRouter.sequentialSwapPermit2{value: amountIn}( + amountIn, + address(0), + USDC_ADDR, + 1000_000000, + true, + false, + ALICE, + emptyPermitSingle, + "", + pleEncode(swaps) + ); + uint256 expectedAmount = 2644659787; + assertEq(amountOut, expectedAmount); + uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE); + assertEq(usdcBalance, expectedAmount); + assertEq(ALICE.balance, 0); + + vm.stopPrank(); + } + + function testSequentialSwapUnwrapETH() public { + // Trade 3k DAI for WETH with 1 swap on Uniswap V2 and unwrap it at the end + + uint256 amountIn = 3_000 * 10 ** 6; + deal(USDC_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(USDC_ADDR, amountIn); + + bytes[] memory swaps = new bytes[](2); + + // USDC -> DAI + swaps[0] = encodeSequentialSwap( + address(usv2Executor), + encodeUniswapV2Swap( + USDC_ADDR, DAI_USDC_POOL, tychoRouterAddr, false + ) + ); + + // DAI -> WETH + swaps[1] = encodeSequentialSwap( + address(usv2Executor), + encodeUniswapV2Swap(DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true) + ); + + uint256 amountOut = tychoRouter.sequentialSwapPermit2( + amountIn, + USDC_ADDR, + address(0), + 1 * 10 ** 18, // min amount + false, + true, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + + uint256 expectedAmount = 1111174255471849849; // 1.11 ETH + assertEq(amountOut, expectedAmount); + assertEq(ALICE.balance, expectedAmount); + + vm.stopPrank(); + } + + function testCyclicSequentialSwap() public { + // This test has start and end tokens that are the same + // The flow is: + // USDC --(USV3)--> WETH --(USV3)--> USDC + 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 usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap( + WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false + ); + + bytes[] memory swaps = new bytes[](2); + // USDC -> WETH + swaps[0] = encodeSequentialSwap( + address(usv3Executor), usdcWethV3Pool1ZeroOneData + ); + // WETH -> USDC + swaps[1] = encodeSequentialSwap( + address(usv3Executor), usdcWethV3Pool2OneZeroData + ); + + tychoRouter.exposedSequentialSwap(amountIn, pleEncode(swaps)); + assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99889294); + } +} diff --git a/foundry/test/TychoRouterSingleSwap.t.sol b/foundry/test/TychoRouterSingleSwap.t.sol new file mode 100644 index 0000000..ceb4f6c --- /dev/null +++ b/foundry/test/TychoRouterSingleSwap.t.sol @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@src/executors/UniswapV4Executor.sol"; +import {TychoRouter} from "@src/TychoRouter.sol"; +import "./TychoRouterTestSetup.sol"; +import "./executors/UniswapV4Utils.sol"; +import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol"; + +contract TychoRouterSingleSwapTest is TychoRouterTestSetup { + bytes32 public constant FEE_SETTER_ROLE = + 0xe6ad9a47fbda1dc18de1eb5eeb7d935e5e81b4748f3cfc61e233e64f88182060; + + function testSingleSwapPermit2() public { + // Trade 1 WETH for DAI with 1 swap on Uniswap V2 using Permit2 + // 1 WETH -> DAI + // (USV2) + vm.startPrank(ALICE); + + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); + + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); + + tychoRouter.singleSwapPermit2( + amountIn, + WETH_ADDR, + DAI_ADDR, + 2659881924818443699786, + false, + false, + ALICE, + permitSingle, + signature, + swap + ); + + uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); + assertEq(daiBalance, 2659881924818443699787); + assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); + + vm.stopPrank(); + } + + function testSingleSwapNoPermit2() public { + // Trade 1 WETH for DAI with 1 swap on Uniswap V2 + // Checks amount out at the end + uint256 amountIn = 1 ether; + + deal(WETH_ADDR, ALICE, amountIn); + vm.startPrank(ALICE); + // Approve the tokenIn to be transferred to the router + IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn); + + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); + + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); + + uint256 minAmountOut = 2600 * 1e18; + uint256 amountOut = tychoRouter.singleSwap( + amountIn, + WETH_ADDR, + DAI_ADDR, + minAmountOut, + false, + false, + ALICE, + swap + ); + + uint256 expectedAmount = 2659881924818443699787; + assertEq(amountOut, expectedAmount); + uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); + assertEq(daiBalance, expectedAmount); + assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); + + vm.stopPrank(); + } + + function testSingleSwapUndefinedMinAmount() public { + // Trade 1 WETH for DAI with 1 swap on Uniswap V2 + // Checks amount out at the end + uint256 amountIn = 1 ether; + + deal(WETH_ADDR, ALICE, amountIn); + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn); + + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); + + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); + + vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector); + tychoRouter.singleSwap( + amountIn, WETH_ADDR, DAI_ADDR, 0, false, false, ALICE, swap + ); + } + + function testSingleSwapInsufficientApproval() public { + // Trade 1 WETH for DAI with 1 swap on Uniswap V2 + // Checks amount out at the end + uint256 amountIn = 1 ether; + + deal(WETH_ADDR, ALICE, amountIn); + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn - 1); + + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); + + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); + + uint256 minAmountOut = 2600 * 1e18; + vm.expectRevert(); + tychoRouter.singleSwap( + amountIn, + WETH_ADDR, + DAI_ADDR, + minAmountOut, + false, + false, + ALICE, + swap + ); + } + + function testSingleSwapNegativeSlippageFailure() public { + // Trade 1 WETH for DAI with 1 swap on Uniswap V2 + // Checks amount out at the end + uint256 amountIn = 1 ether; + + deal(WETH_ADDR, ALICE, amountIn); + vm.startPrank(ALICE); + // Approve the tokenIn to be transferred to the router + IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn); + + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); + + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); + + uint256 minAmountOut = 5600 * 1e18; + + vm.expectRevert( + abi.encodeWithSelector( + TychoRouter__NegativeSlippage.selector, + 2659881924818443699787, // actual amountOut + minAmountOut + ) + ); + tychoRouter.singleSwap( + amountIn, + WETH_ADDR, + DAI_ADDR, + minAmountOut, + false, + false, + ALICE, + swap + ); + } + + function testSingleSwapFee() public { + // Trade 1 WETH for DAI with 1 swap on Uniswap V2 + // Takes 1% fee at the end + + vm.startPrank(FEE_SETTER); + tychoRouter.setFee(100); + tychoRouter.setFeeReceiver(FEE_RECEIVER); + vm.stopPrank(); + + uint256 amountIn = 1 ether; + + deal(WETH_ADDR, ALICE, amountIn); + vm.startPrank(ALICE); + // Approve the tokenIn to be transferred to the router + IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn); + + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); + + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); + + uint256 minAmountOut = 2600 * 1e18; + uint256 amountOut = tychoRouter.singleSwap( + amountIn, + WETH_ADDR, + DAI_ADDR, + minAmountOut, + false, + false, + ALICE, + swap + ); + + uint256 expectedAmount = 2633283105570259262790; + assertEq(amountOut, expectedAmount); + uint256 usdcBalance = IERC20(DAI_ADDR).balanceOf(ALICE); + assertEq(usdcBalance, expectedAmount); + assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26598819248184436997); + + vm.stopPrank(); + } + + function testSingleSwapWrapETH() public { + uint256 amountIn = 1 ether; + deal(ALICE, amountIn); + vm.startPrank(ALICE); + + IAllowanceTransfer.PermitSingle memory emptyPermitSingle = + IAllowanceTransfer.PermitSingle({ + details: IAllowanceTransfer.PermitDetails({ + token: address(0), + amount: 0, + expiration: 0, + nonce: 0 + }), + spender: address(0), + sigDeadline: 0 + }); + + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); + + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); + + uint256 amountOut = tychoRouter.singleSwapPermit2{value: amountIn}( + amountIn, + address(0), + DAI_ADDR, + 1000_000000, + true, + false, + ALICE, + emptyPermitSingle, + "", + swap + ); + uint256 expectedAmount = 2659881924818443699787; + assertEq(amountOut, expectedAmount); + uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); + assertEq(daiBalance, expectedAmount); + assertEq(ALICE.balance, 0); + + vm.stopPrank(); + } + + function testSingleSwapUnwrapETH() public { + // DAI -> WETH with unwrapping to ETH + uint256 amountIn = 3000 ether; + deal(DAI_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(DAI_ADDR, amountIn); + + bytes memory protocolData = + encodeUniswapV2Swap(DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true); + + bytes memory swap = + encodeSingleSwap(address(usv2Executor), protocolData); + + uint256 amountOut = tychoRouter.singleSwapPermit2( + amountIn, + DAI_ADDR, + address(0), + 1000_000000, + false, + true, + ALICE, + permitSingle, + signature, + swap + ); + + uint256 expectedAmount = 1120007305574805922; + assertEq(amountOut, expectedAmount); + assertEq(ALICE.balance, expectedAmount); + + vm.stopPrank(); + } +} diff --git a/foundry/test/TychoRouterSplitSwap.t.sol b/foundry/test/TychoRouterSplitSwap.t.sol new file mode 100644 index 0000000..8e8e20e --- /dev/null +++ b/foundry/test/TychoRouterSplitSwap.t.sol @@ -0,0 +1,683 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@src/executors/UniswapV4Executor.sol"; +import {TychoRouter} from "@src/TychoRouter.sol"; +import "./TychoRouterTestSetup.sol"; +import "./executors/UniswapV4Utils.sol"; +import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol"; + +contract TychoRouterSplitSwapTest is TychoRouterTestSetup { + bytes32 public constant FEE_SETTER_ROLE = + 0xe6ad9a47fbda1dc18de1eb5eeb7d935e5e81b4748f3cfc61e233e64f88182060; + + function _getSplitSwaps() private view returns (bytes[] memory) { + // Trade 1 WETH for USDC through DAI and WBTC with 4 swaps on Uniswap V2 + // -> DAI -> + // 1 WETH USDC + // -> WBTC -> + // (univ2) (univ2) + bytes[] memory swaps = new bytes[](4); + // WETH -> WBTC (60%) + swaps[0] = encodeSplitSwap( + uint8(0), + uint8(1), + (0xffffff * 60) / 100, // 60% + address(usv2Executor), + encodeUniswapV2Swap( + WETH_ADDR, WETH_WBTC_POOL, tychoRouterAddr, false + ) + ); + // WBTC -> USDC + swaps[1] = encodeSplitSwap( + uint8(1), + uint8(2), + uint24(0), + address(usv2Executor), + encodeUniswapV2Swap( + WBTC_ADDR, USDC_WBTC_POOL, tychoRouterAddr, true + ) + ); + // WETH -> DAI + swaps[2] = encodeSplitSwap( + uint8(0), + uint8(3), + uint24(0), + address(usv2Executor), + encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ) + ); + + // DAI -> USDC + swaps[3] = encodeSplitSwap( + uint8(3), + uint8(2), + uint24(0), + address(usv2Executor), + encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) + ); + + return swaps; + } + + function testSplitSwapInternalMethod() public { + // Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info + + uint256 amountIn = 1 ether; + deal(WETH_ADDR, tychoRouterAddr, amountIn); + bytes[] memory swaps = _getSplitSwaps(); + tychoRouter.exposedSplitSwap(amountIn, 4, pleEncode(swaps)); + + uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); + assertEq(usdcBalance, 2615491639); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } + + function testSplitSwapPermit2() public { + // Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info + + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + bytes[] memory swaps = _getSplitSwaps(); + + tychoRouter.splitSwapPermit2( + amountIn, + WETH_ADDR, + USDC_ADDR, + 1, // min amount + false, + false, + 4, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + + uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE); + assertEq(usdcBalance, 2615491639); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } + + function testSplitSwapNoPermit2() public { + // Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn); + + bytes[] memory swaps = _getSplitSwaps(); + + tychoRouter.splitSwap( + amountIn, + WETH_ADDR, + USDC_ADDR, + 1000_000000, // min amount + false, + false, + 4, + ALICE, + pleEncode(swaps) + ); + + uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE); + assertEq(usdcBalance, 2615491639); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } + + function testSplitSwapUndefinedMinAmount() public { + // Min amount should always be non-zero. If zero, swap attempt should revert. + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn); + + bytes[] memory swaps = _getSplitSwaps(); + + vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector); + tychoRouter.splitSwap( + amountIn, + WETH_ADDR, + USDC_ADDR, + 0, // min amount + false, + false, + 4, + ALICE, + pleEncode(swaps) + ); + vm.stopPrank(); + } + + function testSplitSwapInsufficientApproval() public { + // Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info + uint256 amountIn = 1 ether; + + deal(WETH_ADDR, ALICE, amountIn); + vm.startPrank(ALICE); + // Approve less than the amountIn + IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn - 1); + bytes[] memory swaps = _getSplitSwaps(); + + vm.expectRevert(); + tychoRouter.splitSwap( + amountIn, + WETH_ADDR, + USDC_ADDR, + 1000_000000, // min amount + false, + false, + 2, + ALICE, + pleEncode(swaps) + ); + + vm.stopPrank(); + } + + function testSplitSwapNegativeSlippageFailure() public { + // Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info + + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + vm.startPrank(ALICE); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + bytes[] memory swaps = _getSplitSwaps(); + + uint256 minAmountOut = 3000 * 1e18; + + vm.expectRevert( + abi.encodeWithSelector( + TychoRouter__NegativeSlippage.selector, + 2615491639, // actual amountOut + minAmountOut + ) + ); + tychoRouter.splitSwapPermit2( + amountIn, + WETH_ADDR, + DAI_ADDR, + minAmountOut, + false, + false, + 4, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + vm.stopPrank(); + } + + function testSplitSwapFee() public { + // Trade 1 WETH for DAI with 1 swap on Uniswap V2 + // Does permit2 token approval and transfer + // Takes fee at the end + + vm.startPrank(FEE_SETTER); + tychoRouter.setFee(100); + tychoRouter.setFeeReceiver(FEE_RECEIVER); + vm.stopPrank(); + + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); + + bytes memory swap = encodeSplitSwap( + uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData + ); + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + uint256 amountOut = tychoRouter.splitSwapPermit2( + amountIn, + WETH_ADDR, + DAI_ADDR, + 2633283105570259262780, + false, + false, + 2, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + + uint256 expectedAmount = 2633283105570259262790; + assertEq(amountOut, expectedAmount); + uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); + assertEq(daiBalance, expectedAmount); + assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26598819248184436997); + + vm.stopPrank(); + } + + function testSplitSwapWrapETH() public { + // Trade 1 ETH (and wrap it) for DAI with 1 swap on Uniswap V2 + + uint256 amountIn = 1 ether; + deal(ALICE, amountIn); + + vm.startPrank(ALICE); + + IAllowanceTransfer.PermitSingle memory emptyPermitSingle = + IAllowanceTransfer.PermitSingle({ + details: IAllowanceTransfer.PermitDetails({ + token: address(0), + amount: 0, + expiration: 0, + nonce: 0 + }), + spender: address(0), + sigDeadline: 0 + }); + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); + + bytes memory swap = encodeSplitSwap( + uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData + ); + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + uint256 amountOut = tychoRouter.splitSwapPermit2{value: amountIn}( + amountIn, + address(0), + DAI_ADDR, + 2659881924818443699780, + true, + false, + 2, + ALICE, + emptyPermitSingle, + "", + pleEncode(swaps) + ); + uint256 expectedAmount = 2659881924818443699787; + assertEq(amountOut, expectedAmount); + uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); + assertEq(daiBalance, expectedAmount); + assertEq(ALICE.balance, 0); + + vm.stopPrank(); + } + + function testSplitSwapUnwrapETH() public { + // Trade 3k DAI for WETH with 1 swap on Uniswap V2 and unwrap it at the end + + uint256 amountIn = 3_000 * 10 ** 18; + deal(DAI_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(DAI_ADDR, amountIn); + + bytes memory protocolData = + encodeUniswapV2Swap(DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true); + + bytes memory swap = encodeSplitSwap( + uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData + ); + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + uint256 amountOut = tychoRouter.splitSwapPermit2( + amountIn, + DAI_ADDR, + address(0), + 1120007305574805920, + false, + true, + 2, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + + uint256 expectedAmount = 1120007305574805922; // 1.12 ETH + assertEq(amountOut, expectedAmount); + assertEq(ALICE.balance, expectedAmount); + + vm.stopPrank(); + } + + function testSplitSwapSingleUSV3Permit2() public { + // Trade 1 WETH for DAI with 1 swap on Uniswap V3 using Permit2 + // Tests entire USV3 flow including callback + // 1 WETH -> DAI + // (USV3) + vm.startPrank(ALICE); + uint256 amountIn = 10 ** 18; + deal(WETH_ADDR, ALICE, amountIn); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI + bool zeroForOne = false; + bytes memory protocolData = encodeUniswapV3Swap( + WETH_ADDR, DAI_ADDR, tychoRouterAddr, DAI_WETH_USV3, zeroForOne + ); + bytes memory swap = encodeSplitSwap( + uint8(0), uint8(1), uint24(0), address(usv3Executor), protocolData + ); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + tychoRouter.splitSwapPermit2( + amountIn, + WETH_ADDR, + DAI_ADDR, + expAmountOut - 1, + false, + false, + 2, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + + uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(ALICE); + assertGe(finalBalance, expAmountOut); + + vm.stopPrank(); + } + + function testEmptySwapsRevert() public { + uint256 amountIn = 10 ** 18; + bytes memory swaps = ""; + vm.expectRevert(TychoRouter__EmptySwaps.selector); + tychoRouter.exposedSplitSwap(amountIn, 2, swaps); + } + + function testSplitSwapAmountInNotFullySpent() public { + // Trade 1 WETH for DAI with 1 swap on Uniswap V2 + // Has invalid data as input! There is only one swap with 60% of the input amount + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); + + vm.startPrank(ALICE); + + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(WETH_ADDR, amountIn); + + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); + + bytes memory swap = encodeSplitSwap( + uint8(0), + uint8(1), + (0xffffff * 60) / 100, // 60% + address(usv2Executor), + protocolData + ); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + vm.expectRevert( + abi.encodeWithSelector( + TychoRouter__AmountInDiffersFromConsumed.selector, + 1000000000000000000, + 600000000000000000 + ) + ); + + tychoRouter.splitSwapPermit2( + amountIn, + WETH_ADDR, + DAI_ADDR, + 1, + false, + false, + 2, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + + vm.stopPrank(); + } + + function testSplitSwapSingleUSV4CallbackPermit2() public { + vm.startPrank(ALICE); + uint256 amountIn = 100 ether; + deal(USDE_ADDR, ALICE, amountIn); + ( + IAllowanceTransfer.PermitSingle memory permitSingle, + bytes memory signature + ) = handlePermit2Approval(USDE_ADDR, amountIn); + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](1); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: uint24(100), + tickSpacing: int24(1) + }); + + bytes memory protocolData = UniswapV4Utils.encodeExactInput( + USDE_ADDR, USDT_ADDR, true, address(usv4Executor), pools + ); + + bytes memory swap = encodeSplitSwap( + uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData + ); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + tychoRouter.splitSwapPermit2( + amountIn, + USDE_ADDR, + USDT_ADDR, + 99943850, + false, + false, + 2, + ALICE, + permitSingle, + signature, + pleEncode(swaps) + ); + + assertEq(IERC20(USDT_ADDR).balanceOf(ALICE), 99943852); + vm.stopPrank(); + } + + function testSplitSwapMultipleUSV4Callback() public { + // This test has two uniswap v4 hops that will be executed inside of the V4 pool manager + // USDE -> USDT -> WBTC + uint256 amountIn = 100 ether; + deal(USDE_ADDR, tychoRouterAddr, amountIn); + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](2); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: uint24(100), + tickSpacing: int24(1) + }); + pools[1] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: WBTC_ADDR, + fee: uint24(3000), + tickSpacing: int24(60) + }); + + bytes memory protocolData = UniswapV4Utils.encodeExactInput( + USDE_ADDR, WBTC_ADDR, true, address(usv4Executor), pools + ); + + bytes memory swap = encodeSplitSwap( + uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData + ); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); + + assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718); + } + + function testSplitInputCyclicSwapInternalMethod() public { + // This test has start and end tokens that are the same + // The flow is: + // ┌─ (USV3, 60% split) ──> WETH ─┐ + // │ │ + // USDC ──────┤ ├──(USV2)──> USDC + // │ │ + // └─ (USV3, 40% split) ──> 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 usdcWethV3Pool2ZeroOneData = encodeUniswapV3Swap( + USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, true + ); + + bytes memory wethUsdcV2OneZeroData = encodeUniswapV2Swap( + WETH_ADDR, USDC_WETH_USV2, tychoRouterAddr, false + ); + + bytes[] memory swaps = new bytes[](3); + // USDC -> WETH (60% split) + swaps[0] = encodeSplitSwap( + uint8(0), + uint8(1), + (0xffffff * 60) / 100, // 60% + address(usv3Executor), + usdcWethV3Pool1ZeroOneData + ); + // USDC -> WETH (40% remainder) + swaps[1] = encodeSplitSwap( + uint8(0), + uint8(1), + uint24(0), + address(usv3Executor), + usdcWethV3Pool2ZeroOneData + ); + // WETH -> USDC + swaps[2] = encodeSplitSwap( + uint8(1), + uint8(0), + uint24(0), + address(usv2Executor), + wethUsdcV2OneZeroData + ); + tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); + assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99574171); + } + + function testSplitOutputCyclicSwapInternalMethod() public { + // This test has start and end tokens that are the same + // The flow is: + // ┌─── (USV3, 60% split) ───┐ + // │ │ + // USDC ──(USV2) ── WETH──| ├─> USDC + // │ │ + // └─── (USV3, 40% split) ───┘ + + 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] = encodeSplitSwap( + uint8(0), uint8(1), uint24(0), address(usv2Executor), usdcWethV2Data + ); + // WETH -> USDC + swaps[1] = encodeSplitSwap( + uint8(1), + uint8(0), + (0xffffff * 60) / 100, + address(usv3Executor), + usdcWethV3Pool1OneZeroData + ); + + // WETH -> USDC + swaps[2] = encodeSplitSwap( + uint8(1), + uint8(0), + uint24(0), + address(usv3Executor), + usdcWethV3Pool2OneZeroData + ); + + tychoRouter.exposedSplitSwap(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 testSplitSwapInternalMethodBase() public { + vm.skip(true); + vm.rollFork(26857267); + uint256 amountIn = 10 * 10 ** 6; + deal(BASE_USDC, tychoRouterAddr, amountIn); + + bytes memory protocolData = encodeUniswapV2Swap( + BASE_USDC, USDC_MAG7_POOL, tychoRouterAddr, true + ); + + bytes memory swap = encodeSplitSwap( + uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData + ); + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); + assertGt(IERC20(BASE_MAG7).balanceOf(tychoRouterAddr), 1379830606); + } +} From 8f2346330a0ef3a47ea887da9d5217c7b14ea209 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Mon, 17 Mar 2025 13:47:32 -0400 Subject: [PATCH 08/26] fix: fix slither CI action - Need to manually install foundry and compile the project, or else it attempts to install the latest foundry version, which fails due to GLIBC not being found --- .github/workflows/slither.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml index 4134b29..349f500 100644 --- a/.github/workflows/slither.yml +++ b/.github/workflows/slither.yml @@ -11,7 +11,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - name: Build the contracts + run: forge build --build-info + with: + target: 'foundry/' - uses: crytic/slither-action@v0.4.0 with: target: 'foundry/' + ignore-compile: true slither-args: '--filter-paths foundry/lib/' \ No newline at end of file From 3f6bc5643e003792943f8a151bef44bea01c062d Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Thu, 3 Apr 2025 18:20:24 +0200 Subject: [PATCH 09/26] fix: Integration tests after merge Took 50 seconds --- foundry/test/TychoRouterIntegration.t.sol | 93 ++++++++++++----------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/foundry/test/TychoRouterIntegration.t.sol b/foundry/test/TychoRouterIntegration.t.sol index 4ed4431..a473d9d 100644 --- a/foundry/test/TychoRouterIntegration.t.sol +++ b/foundry/test/TychoRouterIntegration.t.sol @@ -16,11 +16,8 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { vm.startPrank(ALICE); IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using `test_split_swap_strategy_encoder_simple` - // but manually replacing the executor address - // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test - // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call( - hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000903146e5f6c59c064b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067e4225a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067bc9c620000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000411fdbe0ac6bdafd51044f24b158235effa29797f468cd4684efa379053d3d15d47ed8b8206e3f6e7349f40aad231cc7e04ed25cbea1ac659b575be8cc168fc2361c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" + hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000903146e5f6c59c064b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000681362ea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ebdcf2000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041a82e9bdde90314de4b1bf918cc2e8b27da98adcab46e8e99d4e77472a572d6381837e9453095f4cc5e9b25691b678288174e547e040a67d12b36ddfdd1e672d21b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" ); vm.stopPrank(); @@ -41,9 +38,6 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { IERC20(WETH_ADDR).approve(address(tychoRouterAddr), 1 ether); uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); // Encoded solution generated using `test_split_swap_strategy_encoder_simple_route_no_permit2` - // but manually replacing the executor address - // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test - // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call( hex"79b9b93b0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000008f1d5c1cae37400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" ); @@ -69,10 +63,8 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { vm.startPrank(ALICE); IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using `test_split_encoding_strategy_usv4` - // and ensuring that the encoded executor address is the one in this test - // `f62849f9a0b5bf2913b396098f7c7019b51a820a` (bool success,) = tychoRouterAddr.call( - hex"7c553846000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000005064ff624d54346285543f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000067e4237600000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067bc9d7e00000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004166b5d3bb274c323e08eeba45d308cc9c11216f9aaafad2a22e94b94fec39293e5480f65f6238d7c8f1e8177f39118373e1041b0ab3a674d3041d119bdb6bc39c1b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c008a0001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d231193300f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000000000000000000000000bb800003c6982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000000000000000" + hex"7c553846000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000005064ff624d54346285543f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000006814875700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ed015f0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000413a7c6367c69ac46fc2b633fd53e583b74b20ec9b3ea83b069fe564765560a4cb335af200fd90ddb5f56d11e469c11a97420499f1b3ee0c1db13149a74daa90db1b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c008a0001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d231193300f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000000000000000000000000bb800003c6982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000000000000000" ); vm.stopPrank(); @@ -94,10 +86,8 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { uint256 balancerBefore = IERC20(PEPE_ADDR).balanceOf(ALICE); // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_in` - // and ensuring that the encoded executor address is the one in this test - // `f62849f9a0b5bf2913b396098f7c7019b51a820a` (bool success,) = tychoRouterAddr.call{value: 1 ether}( - hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d2311933000000000000000000000000000000000000000000c87c939ae635f92dc2379c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067f01a7800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c894800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000416642950b804a47a0abcb17d81cc2a7967d606e00e8de470e0e7827347658160a28b9892f147248b9bf31aad8faa06181aee0c4a612151e9ef4889991b9930b791b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007200700001000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d231193301f62849f9a0b5bf2913b396098f7c7019b51a820a6982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000" + hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d2311933000000000000000000000000000000000000000000c87c939ae635f92dc2379c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006814877000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ed017800000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004193acc98d79044e8ec1bc3ced832dc679e38ac8c6fe9b5befd1e5e44cb44edb0e365f1c5d6e3ca6590ed1a053f1841aede29e5b573f046387aff794520a0f22581b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007200700001000000f62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d231193301f62849f9a0b5bf2913b396098f7c7019b51a820a6982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000" ); vm.stopPrank(); @@ -123,10 +113,8 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_out` - // and ensuring that the encoded executor address is the one in this test - // `f62849f9a0b5bf2913b396098f7c7019b51a820a` (bool success,) = tychoRouterAddr.call( - hex"7c55384600000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f81490b4f29aade000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000067f01af000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c894f80000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000417811cd10b02278128a9e4df9ef2e099cff6ad46ec6ead5ba0b70dd1db5749d573cf4a8821a524bd6cc5b61ce0faf69d1d4b1f9233b93a4b203e79668f250b1a71c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007200700001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000000000000000000000000bb800003c0000000000000000000000000000" + hex"7c55384600000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f81490b4f29aade000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000000000000000000000000000000000006814878000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ed018800000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004190134d2d142caff6dbea417292a15685119bd676b2b73bad35fe39f720f7c3163f16d057327499019506b6f690a3916fd3375c579c9cb814113b1516187380531b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007200700001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000000000000000000000000bb800003c0000000000000000000000000000" ); vm.stopPrank(); @@ -149,13 +137,9 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { // Approve permit2 vm.startPrank(ALICE); - // Encoded solution generated using - // `test_split_swap_strategy_encoder_simple_route_wrap` - // but manually replacing the executor address - // `f6c5be66fff9dc69962d73da0a617a827c382329` with the one in this test - // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` + // Encoded solution generated using `test_split_swap_strategy_encoder_simple_route_wrap` (bool success,) = tychoRouterAddr.call{value: 1 ether}( - hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000903146e5f6c59c064b000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067f0192a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c893320000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000419849ede1f59ad3092a3d8f6b5d7a4d3d854c8013d0a728b8556dc9744ddeed6c7edc4987c7724c280d493ca8dd55dd5aa5f5a66a66d85683f8a5b744908752a21b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600020000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" + hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000903146e5f6c59c064b000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006813638900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ebdd91000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041f6ff7411a7ec76cb7dcafecf5e7f11121b1aa88af505635dc7faae6057e4f44e2859712f58331a14a1624f1e5edf2af80ddd2d90b5453d74df1b1fea10b9a2f91c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600020000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" ); vm.stopPrank(); @@ -178,13 +162,9 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { // Approve permit2 vm.startPrank(ALICE); IERC20(DAI_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using - // `test_split_swap_strategy_encoder_simple_route_unwrap` - // but manually replacing the executor address - // `f6c5be66fff9dc69962d73da0a617a827c382329` with the one in this test - // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` + // Encoded solution generated using `test_split_swap_strategy_encoder_simple_route_unwrap` (bool success,) = tychoRouterAddr.call( - hex"7c5538460000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbd2fc137a30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000000000000000000000000000000000000067f017d700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c891df00000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004181b0d9c7bbf7bd3270e22a7ff337b019b006ea60d9e357035b622bfc8e48126343fa9c1342383d3d072c2ddea2072fd5e447e7b6a4b56f5e7973963d18664e5d1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395010000000000000000" + hex"7c5538460000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be00000000000000000000000000000000000000000000000000000000000006813615200000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ebdb5a000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041a7da748b04674485a5da185055affefc85b6d8fe412accce55b6f67842116f0f7f7130de5d74c68c20e1cedcdf93b8741b9171de2e6a3f2567887382a0712e3f1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395010000000000000000" ); vm.stopPrank(); @@ -195,6 +175,43 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { assertEq(balancerAfter - balancerBefore, 1120007305574805922); } + function testSplitEkuboIntegration() public { + // Test needs to be run on block 22082754 or later + // notice that the addresses for the tycho router and the executors are different because we are redeploying + vm.rollFork(22082754); + tychoRouter = deployRouter(); + address[] memory executors = deployExecutors(); + vm.startPrank(EXECUTOR_SETTER); + tychoRouter.setExecutors(executors); + vm.stopPrank(); + + // TEMPORARY while the Ekubo executor address is hardcoded in TychoRouter + // This allows us to change the code at that address to be the testing executor code + vm.etch( + 0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D, + 0x2a07706473244BC757E10F2a9E86fB532828afe3.code + ); + + deal(ALICE, 1 ether); + uint256 balancerBefore = IERC20(USDC_ADDR).balanceOf(ALICE); + + // Approve permit2 + vm.startPrank(ALICE); + // Encoded solution generated using `test_split_encoding_strategy_ekubo` + (bool success,) = address(tychoRouter).call{value: 1 ether}( + hex"79b9b93b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000077007500010000002a07706473244bc757e10f2a9e86fb532828afe31d1499e622d69689cdf9004d05ec547d650ff2110000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000000000000000000000" + ); + + uint256 balancerAfter = IERC20(USDC_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertGe(balancerAfter - balancerBefore, 26173932); + + // All input tokens are transferred to the router at first. Make sure we used + // all of it (and thus our splits are correct). + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } + function testSplitSwapIntegration() public { // Test created with calldata from our router encoder, replacing the executor // address with the USV2 executor address. @@ -211,11 +228,8 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { vm.startPrank(ALICE); IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using `test_split_swap_strategy_encoder_complex` - // but manually replacing the executor address - // `f6c5be66FFf9DC69962d73da0A617a827c382329` with the one in this test - // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call( - hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067f0198700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067c8938f000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041308a3ba881e23ac794deca324cfd959b808c86bb239b81c9db8873c8392382411f87902e6ceb8e59636d8d6fab4ead1863727f9a2168246c93b678f3ae4ae37b1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160005600028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d0139500005602030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d53ede3eca2a72b3aecc820e955b36f38437d0139501005601030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d0139501" + hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000681363a300000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ebddab0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000415b7ff43991de10c4c3a0372653891d27eb305ce04228bfe46a7d84a0978063fc4cb05183f19b83511bcb689b002d4f8e170f1d3cd77cf18c638229ccb67e0cac1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160005600028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d0139500005602030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d53ede3eca2a72b3aecc820e955b36f38437d0139501005601030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d0139501" ); vm.stopPrank(); @@ -237,11 +251,8 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { vm.startPrank(ALICE); IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using `test_cyclic_sequential_swap` - // but manually replacing the executor address - // `dd8559c917393fc8dd2b4dd289c52ff445fde1b0` with the one in this test - // `2e234dae75c793f67a35089c9d99245e1c58470b` (bool success,) = tychoRouterAddr.call( - hex"7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f4308e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000067f67a8b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067cef493000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041c07077fc73bb0f5129006061288fa0583c101631307377281d6b8f3feb50aa2d564f9948c92e0e4abc3771d592bd2f22ebb18ccf21b270459b05f272251ce1c71b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d8000000" + hex"7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f4308e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000681363d200000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ebddda0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000418d58a54a3b8afc5d2e228ce6c5a1ab6b342cb5bfd9a00d57b869a4703ca2bb084d10d21f6842be9652a9ff2392673fbdcb961439ccc962de09f6bc64e5e665fe1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d8000000" ); assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99889294); @@ -256,11 +267,8 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { vm.startPrank(ALICE); IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using `test_split_input_cyclic_swap` - // but manually replacing the executor addresses with the ones in this test - // `dd8559c917393fc8dd2b4dd289c52ff445fde1b0` to `2e234dae75c793f67a35089c9d99245e1c58470b` - // `f6c5be66fff9dc69962d73da0a617a827c382329` to `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call( - hex"7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ef619b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000067f6c08700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067cf3a8f000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041f248bfa39e6801b4173cd4d61e5e5d0c31942eb3c194785f964a82b2c3e05b4b302bccc0924fa4c4ef90854e42865db11f458d3b6a62afddee833f3eb069cd521b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000136006d00019999992e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d801005601000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000" + hex"7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ef619b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006816408300000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067eeba8b0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000416de253b927fdcf110d157372e620e70c7220d3c01f04e01cdffb076edbb8b42052d281dd6c55a2349502742a0a8de58d2d1dbdc452f6c9d695b1c732c023d0561c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000136006d00019999992e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d801005601000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000" ); assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99574171); @@ -275,11 +283,8 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { vm.startPrank(ALICE); IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); // Encoded solution generated using `test_split_output_cyclic_swap` - // but manually replacing the executor addresses with the ones in this test - // `dd8559c917393fc8dd2b4dd289c52ff445fde1b0` to `2e234dae75c793f67a35089c9d99245e1c58470b` - // `f6c5be66fff9dc69962d73da0a617a827c382329` to `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call( - hex"7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005eea514000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000067f6be9400000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067cf389c000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041c02ad8eceede50085f35ce8e8313ebbac9b379396c6e72a35bb4df0970cbdaaa1a91e6f787641af55b13b926199c844df42fdd2ae7bb287db7e5cc2a8bc1d7f51b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000136005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d0139501006d01009999992e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564000006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d80000000000000000000000" + hex"7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005eea514000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006816418400000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067eebb8c0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000412c44c7de8f7eaaea61e49dbdefdc5606925db6f93db0789e632899ac88d3c7677cc8b69719603ab1b5ecef07d659b7254881d0667a49ebccbf43949b760b041a1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000136005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d0139501006d01009999992e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564000006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d80000000000000000000000" ); assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99525908); From a5f07a25ef3edc099f45ab5d4da5acce694b155a Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Wed, 26 Mar 2025 16:05:54 +0100 Subject: [PATCH 10/26] feat: SingleSwapStrategyEncoder - Had to take the implementation of the swap header encoding out of the main EVMStrategyEncoder trait, since it will now be difference for single and split swap strategies. - Integration tests will be added in separate task/PR. --- .../evm/strategy_encoder/strategy_encoders.rs | 103 ++++++++++++++++-- 1 file changed, 91 insertions(+), 12 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 4498c3b..cadeb30 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -916,7 +916,7 @@ mod tests { Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), ) - .unwrap(); + .unwrap(); let solution = Solution { exact_out: false, given_token: weth, @@ -951,15 +951,15 @@ mod tests { // it's hard to assert let expected_swap = String::from(concat!( - // length of swap bytes - "0000000000000000000000000000000000000000000000000000000000000051", - "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id - "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver - "00", // zero2one - "00", // exact out - "0000000000000000000000000000", // padding + // length of swap bytes + "0000000000000000000000000000000000000000000000000000000000000051", + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id + "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver + "00", // zero2one + "00", // exact out + "0000000000000000000000000000", // padding )); let hex_calldata = encode(&calldata); @@ -1362,6 +1362,85 @@ mod tests { println!("{}", hex_calldata); } + #[test] + fn test_single_swap_strategy_encoder_no_permit2() { + // Performs a single swap from WETH to DAI on a USV2 pool, without permit2 and no grouping + // optimizations. + + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + let expected_amount = Some(BigUint::from_str("2_650_000000000000000000").unwrap()); + let slippage = Some(0.01f64); + let checked_amount = Some(BigUint::from_str("2_640_000000000000000000").unwrap()); + let expected_min_amount = U256::from_str("2_640_000000000000000000").unwrap(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SingleSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: dai, + expected_amount, + slippage, + checked_amount, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + swaps: vec![swap], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); + let expected_input = [ + "0f6cbbe8", // Function selector + "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out + &expected_min_amount_encoded, // min amount out + "0000000000000000000000000000000000000000000000000000000000000000", // wrap + "0000000000000000000000000000000000000000000000000000000000000000", // unwrap + "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "0000000000000000000000000000000000000000000000000000000000000100", // offset of swap bytes + "0000000000000000000000000000000000000000000000000000000000000051", // length of swap bytes without padding + + // Swap data + "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id + "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver + "00", // zero2one + "00", // exact out + "0000000000000000000000000000", // padding + ] + .join(""); + + let hex_calldata = encode(&calldata); + + assert_eq!(hex_calldata, expected_input); + println!("{}", hex_calldata); + } + #[test] fn test_split_encoding_strategy_ekubo() { // ETH ──(EKUBO)──> USDC @@ -1397,7 +1476,7 @@ mod tests { None, Some(Bytes::from_str("0x1d1499e622D69689cdf9004d05Ec547d650Ff211").unwrap()), ) - .unwrap(); + .unwrap(); let solution = Solution { exact_out: false, @@ -1423,7 +1502,7 @@ mod tests { } #[test] - fn test_single_swap_strategy_encoder_no_permit2() { + fn test_split_swap_strategy_encoder_simple_route_no_permit2() { // Performs a single swap from WETH to DAI on a USV2 pool, without permit2 and no grouping // optimizations. From 5d586c25e3e084511b34677684cd1bb71e249fcc Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Wed, 26 Mar 2025 17:06:56 +0100 Subject: [PATCH 11/26] feat: SingleSwapStrategyEncoder - Had to take the implementation of the swap header encoding out of the main EVMStrategyEncoder trait, since it will now be difference for single and split swap strategies. - Integration tests will be added in separate task/PR. --- .../evm/strategy_encoder/strategy_encoders.rs | 236 +++++++++++++++++- 1 file changed, 235 insertions(+), 1 deletion(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index cadeb30..6821e61 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -199,6 +199,154 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { } } +/// Represents the encoder for a swap strategy which supports single swaps. +/// +/// # Fields +/// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders +/// * `permit2`: Permit2, responsible for managing permit2 operations and providing necessary +/// signatures and permit2 objects for calling the router +/// * `selector`: String, the selector for the swap function in the router contract +/// * `native_address`: Address of the chain's native token +/// * `wrapped_address`: Address of the chain's wrapped token +/// * `router_address`: Address of the router to be used to execute swaps +#[derive(Clone)] +pub struct SequentialSwapStrategyEncoder { + swap_encoder_registry: SwapEncoderRegistry, + permit2: Option, + selector: String, + router_address: Bytes, +} + +impl SequentialSwapStrategyEncoder { + pub fn new( + blockchain: tycho_core::models::Chain, + swap_encoder_registry: SwapEncoderRegistry, + swapper_pk: Option, + router_address: Bytes, + ) -> Result { + let chain = Chain::from(blockchain); + let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { + (Some(Permit2::new(swapper_pk, chain.clone())?), "sequentialSwapPermit2(uint256,address,address,uint256,bool,bool,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) + } else { + ( + None, + "sequentialSwap(uint256,address,address,uint256,bool,bool,address,bytes)" + .to_string(), + ) + }; + Ok(Self { permit2, selector, swap_encoder_registry, router_address }) + } + + /// Encodes information necessary for performing a single swap against a given executor for + /// a protocol. + fn encode_swap_header(&self, executor_address: Bytes, protocol_data: Vec) -> Vec { + let mut encoded = Vec::new(); + encoded.extend(executor_address.to_vec()); + encoded.extend(protocol_data); + encoded + } +} + +impl EVMStrategyEncoder for SequentialSwapStrategyEncoder {} + +impl StrategyEncoder for SequentialSwapStrategyEncoder { + fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { + // TODO validate sequential swaps: check valid cycles, empty swaps, etc. + + let min_amount_out = get_min_amount_for_solution(solution.clone()); + let grouped_swaps = group_swaps(solution.swaps); + + let (mut unwrap, mut wrap) = (false, false); + if let Some(action) = solution.native_action.clone() { + match action { + NativeAction::Wrap => wrap = true, + NativeAction::Unwrap => unwrap = true, + } + } + + let mut swaps = vec![]; + for grouped_swap in grouped_swaps.iter() { + let swap_encoder = self + .get_swap_encoder(&grouped_swap.protocol_system) + .ok_or_else(|| { + EncodingError::InvalidInput(format!( + "Swap encoder not found for protocol: {}", + grouped_swap.protocol_system + )) + })?; + + let mut grouped_protocol_data: Vec = vec![]; + for swap in grouped_swap.swaps.iter() { + let encoding_context = EncodingContext { + receiver: solution.router_address.clone(), + exact_out: solution.exact_out, + router_address: self.router_address.clone(), + group_token_in: grouped_swap.input_token.clone(), + group_token_out: grouped_swap.output_token.clone(), + }; + let protocol_data = + swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; + grouped_protocol_data.extend(protocol_data); + } + + let swap_data = self.encode_swap_header( + Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { + EncodingError::FatalError("Invalid executor address".to_string()) + })?, + grouped_protocol_data, + ); + swaps.push(swap_data); + } + + let encoded_swaps = self.ple_encode(swaps); + let method_calldata = if let Some(permit2) = self.permit2.clone() { + let (permit, signature) = permit2.get_permit( + &self.router_address, + &solution.sender, + &solution.given_token, + &solution.given_amount, + )?; + ( + biguint_to_u256(&solution.given_amount), + bytes_to_address(&solution.given_token)?, + bytes_to_address(&solution.checked_token)?, + biguint_to_u256(&min_amount_out), + wrap, + unwrap, + bytes_to_address(&solution.receiver)?, + permit, + signature.as_bytes().to_vec(), + encoded_swaps, + ) + .abi_encode() + } else { + ( + biguint_to_u256(&solution.given_amount), + bytes_to_address(&solution.given_token)?, + bytes_to_address(&solution.checked_token)?, + biguint_to_u256(&min_amount_out), + wrap, + unwrap, + bytes_to_address(&solution.receiver)?, + encoded_swaps, + ) + .abi_encode() + }; + + let contract_interaction = encode_input(&self.selector, method_calldata); + Ok((contract_interaction, solution.router_address)) + } + + fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box> { + self.swap_encoder_registry + .get_encoder(protocol_system) + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + /// Represents the encoder for a swap strategy which supports single, sequential and split swaps. /// /// # Fields @@ -885,13 +1033,99 @@ mod tests { Some(BigUint::from_str("2_999_000000000000000000").unwrap()), U256::from_str("2_999_000000000000000000").unwrap(), )] - fn test_single_swap_strategy_encoder( + fn test_sequential_swap_strategy_encoder_simple_route( #[case] expected_amount: Option, #[case] slippage: Option, #[case] checked_amount: Option, #[case] expected_min_amount: U256, ) { // Performs a single swap from WETH to DAI on a USV2 pool, with no grouping optimizations. + + // Set up a mock private key for signing + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SequentialSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: dai, + expected_amount, + slippage, + checked_amount, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + swaps: vec![swap], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); + let expected_input = [ + "51bcc7b6", // Function selector + "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out + &expected_min_amount_encoded, // min amount out + "0000000000000000000000000000000000000000000000000000000000000000", // wrap + "0000000000000000000000000000000000000000000000000000000000000000", // unwrap + "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + ] + .join(""); + + // after this there is the permit and because of the deadlines (that depend on block time) + // it's hard to assert + + let expected_swaps = String::from(concat!( + // length of ple encoded swaps without padding + "0000000000000000000000000000000000000000000000000000000000000053", + // ple encoded swaps + "0051", + // Swap data + "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id + "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver + "00", // zero2one + "00", // exact out + "000000000000000000000000", // padding + )); + let hex_calldata = encode(&calldata); + + assert_eq!(hex_calldata[..456], expected_input); + assert_eq!(hex_calldata[1224..], expected_swaps); + } + + #[test] + fn test_single_swap_strategy_encoder_wrap() { + // Performs a single swap from WETH to DAI on a USV2 pool, wrapping ETH + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + // Set up a mock private key for signing let private_key = "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); From 0d8150e22f2a354a41583a722bd4eba60ab1de59 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Thu, 27 Mar 2025 11:19:00 +0100 Subject: [PATCH 12/26] feat: sequential swap solution validation - Basically reuse all methods of split swap validation, minus those specific to splits. --- .../evm/strategy_encoder/strategy_encoders.rs | 32 +++- .../strategy_encoder/strategy_validators.rs | 180 +++++++++--------- 2 files changed, 124 insertions(+), 88 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 6821e61..4f49eee 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -12,7 +12,10 @@ use crate::encoding::{ evm::{ approvals::permit2::Permit2, constants::DEFAULT_ROUTERS_JSON, - strategy_encoder::{group_swaps::group_swaps, strategy_validators::SplitSwapValidator}, + strategy_encoder::{ + group_swaps::group_swaps, + strategy_validators::{SequentialSwapValidator, SplitSwapValidator, SwapValidator}, + }, swap_encoder::swap_encoder_registry::SwapEncoderRegistry, utils::{ biguint_to_u256, bytes_to_address, encode_input, get_min_amount_for_solution, @@ -209,12 +212,17 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { /// * `native_address`: Address of the chain's native token /// * `wrapped_address`: Address of the chain's wrapped token /// * `router_address`: Address of the router to be used to execute swaps +/// * `sequential_swap_validator`: SequentialSwapValidator, responsible for checking validity of +/// sequential swap solutions #[derive(Clone)] pub struct SequentialSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, permit2: Option, selector: String, router_address: Bytes, + native_address: Bytes, + wrapped_address: Bytes, + sequential_swap_validator: SequentialSwapValidator, } impl SequentialSwapStrategyEncoder { @@ -234,7 +242,15 @@ impl SequentialSwapStrategyEncoder { .to_string(), ) }; - Ok(Self { permit2, selector, swap_encoder_registry, router_address }) + Ok(Self { + permit2, + selector, + swap_encoder_registry, + router_address, + native_address: chain.native_token()?, + wrapped_address: chain.wrapped_token()?, + sequential_swap_validator: SequentialSwapValidator, + }) } /// Encodes information necessary for performing a single swap against a given executor for @@ -251,7 +267,17 @@ impl EVMStrategyEncoder for SequentialSwapStrategyEncoder {} impl StrategyEncoder for SequentialSwapStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { - // TODO validate sequential swaps: check valid cycles, empty swaps, etc. + self.sequential_swap_validator + .validate_solution_min_amounts(&solution)?; + self.sequential_swap_validator + .validate_swap_path( + &solution.swaps, + &solution.given_token, + &solution.checked_token, + &solution.native_action, + &self.native_address, + &self.wrapped_address, + )?; let min_amount_out = get_min_amount_for_solution(solution.clone()); let grouped_swaps = group_swaps(solution.swaps); diff --git a/src/encoding/evm/strategy_encoder/strategy_validators.rs b/src/encoding/evm/strategy_encoder/strategy_validators.rs index e7ae722..04396de 100644 --- a/src/encoding/evm/strategy_encoder/strategy_validators.rs +++ b/src/encoding/evm/strategy_encoder/strategy_validators.rs @@ -7,92 +7,10 @@ use crate::encoding::{ models::{NativeAction, Solution, Swap}, }; -/// Validates whether a sequence of split swaps represents a valid solution. -#[derive(Clone)] -pub struct SplitSwapValidator; - -impl SplitSwapValidator { - /// Raises an error if the split percentages are invalid. - /// - /// Split percentages are considered valid if all the following conditions are met: - /// * Each split amount is < 1 (100%) - /// * There is exactly one 0% split for each token, and it's the last swap specified, signifying - /// to the router to send the remainder of the token to the designated protocol - /// * The sum of all non-remainder splits for each token is < 1 (100%) - /// * There are no negative split amounts - pub fn validate_split_percentages(&self, swaps: &[Swap]) -> Result<(), EncodingError> { - let mut swaps_by_token: HashMap> = HashMap::new(); - for swap in swaps { - if swap.split >= 1.0 { - return Err(EncodingError::InvalidInput(format!( - "Split percentage must be less than 1 (100%), got {}", - swap.split - ))); - } - swaps_by_token - .entry(swap.token_in.clone()) - .or_default() - .push(swap); - } - - for (token, token_swaps) in swaps_by_token { - // Single swaps don't need remainder handling - if token_swaps.len() == 1 { - if token_swaps[0].split != 0.0 { - return Err(EncodingError::InvalidInput(format!( - "Single swap must have 0% split for token {:?}", - token - ))); - } - continue; - } - - let mut found_zero_split = false; - let mut total_percentage = 0.0; - for (i, swap) in token_swaps.iter().enumerate() { - match (swap.split == 0.0, i == token_swaps.len() - 1) { - (true, false) => { - return Err(EncodingError::InvalidInput(format!( - "The 0% split for token {:?} must be the last swap", - token - ))) - } - (true, true) => found_zero_split = true, - (false, _) => { - if swap.split < 0.0 { - return Err(EncodingError::InvalidInput(format!( - "All splits must be >= 0% for token {:?}", - token - ))); - } - total_percentage += swap.split; - } - } - } - - if !found_zero_split { - return Err(EncodingError::InvalidInput(format!( - "Token {:?} must have exactly one 0% split for remainder handling", - token - ))); - } - - // Total must be <100% to leave room for remainder - if total_percentage >= 1.0 { - return Err(EncodingError::InvalidInput(format!( - "Total of non-remainder splits for token {:?} must be <100%, got {}%", - token, - total_percentage * 100.0 - ))); - } - } - - Ok(()) - } - +pub trait SwapValidator { /// Raises an error if the solution does not have checked amount set or slippage with checked /// amount set. - pub fn validate_solution_min_amounts(&self, solution: &Solution) -> Result<(), EncodingError> { + fn validate_solution_min_amounts(&self, solution: &Solution) -> Result<(), EncodingError> { if solution.checked_amount.is_none() && (solution.slippage.is_none() || solution.expected_amount.is_none()) { @@ -113,7 +31,7 @@ impl SplitSwapValidator { /// If the given token is the native token and the native action is WRAP, it will be converted /// to the wrapped token before validating the swap path. The same principle applies for the /// checked token and the UNWRAP action. - pub fn validate_swap_path( + fn validate_swap_path( &self, swaps: &[Swap], given_token: &Bytes, @@ -197,6 +115,98 @@ impl SplitSwapValidator { } } +/// Validates whether a sequence of split swaps represents a valid solution. +#[derive(Clone)] +pub struct SplitSwapValidator; + +impl SwapValidator for SplitSwapValidator {} + +impl SplitSwapValidator { + /// Raises an error if the split percentages are invalid. + /// + /// Split percentages are considered valid if all the following conditions are met: + /// * Each split amount is < 1 (100%) + /// * There is exactly one 0% split for each token, and it's the last swap specified, signifying + /// to the router to send the remainder of the token to the designated protocol + /// * The sum of all non-remainder splits for each token is < 1 (100%) + /// * There are no negative split amounts + pub fn validate_split_percentages(&self, swaps: &[Swap]) -> Result<(), EncodingError> { + let mut swaps_by_token: HashMap> = HashMap::new(); + for swap in swaps { + if swap.split >= 1.0 { + return Err(EncodingError::InvalidInput(format!( + "Split percentage must be less than 1 (100%), got {}", + swap.split + ))); + } + swaps_by_token + .entry(swap.token_in.clone()) + .or_default() + .push(swap); + } + + for (token, token_swaps) in swaps_by_token { + // Single swaps don't need remainder handling + if token_swaps.len() == 1 { + if token_swaps[0].split != 0.0 { + return Err(EncodingError::InvalidInput(format!( + "Single swap must have 0% split for token {:?}", + token + ))); + } + continue; + } + + let mut found_zero_split = false; + let mut total_percentage = 0.0; + for (i, swap) in token_swaps.iter().enumerate() { + match (swap.split == 0.0, i == token_swaps.len() - 1) { + (true, false) => { + return Err(EncodingError::InvalidInput(format!( + "The 0% split for token {:?} must be the last swap", + token + ))) + } + (true, true) => found_zero_split = true, + (false, _) => { + if swap.split < 0.0 { + return Err(EncodingError::InvalidInput(format!( + "All splits must be >= 0% for token {:?}", + token + ))); + } + total_percentage += swap.split; + } + } + } + + if !found_zero_split { + return Err(EncodingError::InvalidInput(format!( + "Token {:?} must have exactly one 0% split for remainder handling", + token + ))); + } + + // Total must be <100% to leave room for remainder + if total_percentage >= 1.0 { + return Err(EncodingError::InvalidInput(format!( + "Total of non-remainder splits for token {:?} must be <100%, got {}%", + token, + total_percentage * 100.0 + ))); + } + } + + Ok(()) + } +} + +/// Validates whether a sequence of sequential swaps represents a valid solution. +#[derive(Clone)] +pub struct SequentialSwapValidator; + +impl SwapValidator for SequentialSwapValidator {} + #[cfg(test)] mod tests { use std::str::FromStr; From 11a05e4f18688eb0fce0a2bc2c171c9e0a355177 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Wed, 26 Mar 2025 16:05:54 +0100 Subject: [PATCH 13/26] feat: SingleSwapStrategyEncoder - Had to take the implementation of the swap header encoding out of the main EVMStrategyEncoder trait, since it will now be difference for single and split swap strategies. - Integration tests will be added in separate task/PR. Took 7 seconds --- .../evm/strategy_encoder/strategy_encoders.rs | 436 +++++++++++++++++- 1 file changed, 412 insertions(+), 24 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 891e3c2..8158a57 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -26,25 +26,6 @@ use crate::encoding::{ /// Encodes a solution using a specific strategy for execution on the EVM-compatible network. pub trait EVMStrategyEncoder: StrategyEncoder { - /// Encodes information necessary for performing a single swap against a given executor for - /// a protocol. - fn encode_swap_header( - &self, - token_in: U8, - token_out: U8, - split: U24, - executor_address: Bytes, - protocol_data: Vec, - ) -> Vec { - let mut encoded = Vec::new(); - encoded.push(token_in.to_be_bytes_vec()[0]); - encoded.push(token_out.to_be_bytes_vec()[0]); - encoded.extend_from_slice(&split.to_be_bytes_vec()); - encoded.extend(executor_address.to_vec()); - encoded.extend(protocol_data); - encoded - } - /// Uses prefix-length encoding to efficient encode action data. /// /// Prefix-length encoding is a data encoding method where the beginning of a data segment @@ -61,6 +42,163 @@ pub trait EVMStrategyEncoder: StrategyEncoder { } } +/// Represents the encoder for a swap strategy which supports single swaps. +/// +/// # Fields +/// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders +/// * `permit2`: Permit2, responsible for managing permit2 operations and providing necessary +/// signatures and permit2 objects for calling the router +/// * `selector`: String, the selector for the swap function in the router contract +/// * `native_address`: Address of the chain's native token +/// * `wrapped_address`: Address of the chain's wrapped token +/// * `router_address`: Address of the router to be used to execute swaps +#[derive(Clone)] +pub struct SingleSwapStrategyEncoder { + swap_encoder_registry: SwapEncoderRegistry, + permit2: Option, + selector: String, + router_address: Bytes, +} + +impl SingleSwapStrategyEncoder { + pub fn new( + blockchain: tycho_core::models::Chain, + swap_encoder_registry: SwapEncoderRegistry, + swapper_pk: Option, + router_address: Bytes, + ) -> Result { + let chain = Chain::from(blockchain); + let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { + (Some(Permit2::new(swapper_pk, chain.clone())?), "singleSwapPermit2(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) + } else { + ( + None, + "singleSwap(uint256,address,address,uint256,bool,bool,uint256,address,bytes)" + .to_string(), + ) + }; + Ok(Self { permit2, selector, swap_encoder_registry, router_address }) + } + + /// Encodes information necessary for performing a single swap against a given executor for + /// a protocol. + fn encode_swap_header(&self, executor_address: Bytes, protocol_data: Vec) -> Vec { + let mut encoded = Vec::new(); + encoded.extend(executor_address.to_vec()); + encoded.extend(protocol_data); + encoded + } +} + +impl EVMStrategyEncoder for SingleSwapStrategyEncoder {} + +impl StrategyEncoder for SingleSwapStrategyEncoder { + fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { + let grouped_swaps = group_swaps(solution.clone().swaps); + let number_of_groups = grouped_swaps.len(); + if number_of_groups != 1 { + return Err(EncodingError::InvalidInput(format!( + "Executor strategy only supports exactly one swap for non-groupable protocols. Found {}", + number_of_groups + ))) + } + + let grouped_swap = grouped_swaps + .first() + .ok_or_else(|| EncodingError::FatalError("Swap grouping failed".to_string()))?; + + if grouped_swap.split != 0f64 { + return Err(EncodingError::InvalidInput( + "Splits not supported for single swaps.".to_string(), + )) + } + + let min_amount_out = get_min_amount_for_solution(solution.clone()); + + let (mut unwrap, mut wrap) = (false, false); + if let Some(action) = solution.native_action.clone() { + match action { + NativeAction::Wrap => wrap = true, + NativeAction::Unwrap => unwrap = true, + } + } + + let swap_encoder = self + .get_swap_encoder(&grouped_swap.protocol_system) + .ok_or_else(|| { + EncodingError::InvalidInput(format!( + "Swap encoder not found for protocol: {}", + grouped_swap.protocol_system + )) + })?; + + let mut grouped_protocol_data: Vec = vec![]; + for swap in grouped_swap.swaps.iter() { + let encoding_context = EncodingContext { + receiver: self.router_address.clone(), + exact_out: solution.exact_out, + router_address: self.router_address.clone(), + group_token_in: grouped_swap.input_token.clone(), + group_token_out: grouped_swap.output_token.clone(), + }; + let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; + grouped_protocol_data.extend(protocol_data); + } + + let swap_data = self.encode_swap_header( + Bytes::from_str(swap_encoder.executor_address()) + .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?, + grouped_protocol_data, + ); + + let method_calldata = if let Some(permit2) = self.permit2.clone() { + let (permit, signature) = permit2.get_permit( + &self.router_address, + &solution.sender, + &solution.given_token, + &solution.given_amount, + )?; + ( + biguint_to_u256(&solution.given_amount), + bytes_to_address(&solution.given_token)?, + bytes_to_address(&solution.checked_token)?, + biguint_to_u256(&min_amount_out), + wrap, + unwrap, + bytes_to_address(&solution.receiver)?, + permit, + signature.as_bytes().to_vec(), + swap_data, + ) + .abi_encode() + } else { + ( + biguint_to_u256(&solution.given_amount), + bytes_to_address(&solution.given_token)?, + bytes_to_address(&solution.checked_token)?, + biguint_to_u256(&min_amount_out), + wrap, + unwrap, + bytes_to_address(&solution.receiver)?, + swap_data, + ) + .abi_encode() + }; + + let contract_interaction = encode_input(&self.selector, method_calldata); + Ok((contract_interaction, self.router_address.clone())) + } + + fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box> { + self.swap_encoder_registry + .get_encoder(protocol_system) + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + /// Represents the encoder for a swap strategy which supports single, sequential and split swaps. /// /// # Fields @@ -126,7 +264,27 @@ impl SplitSwapStrategyEncoder { router_address: tycho_router_address, }) } + + /// Encodes information necessary for performing a single swap against a given executor for + /// a protocol as part of a split swap solution. + fn encode_swap_header( + &self, + token_in: U8, + token_out: U8, + split: U24, + executor_address: Bytes, + protocol_data: Vec, + ) -> Vec { + let mut encoded = Vec::new(); + encoded.push(token_in.to_be_bytes_vec()[0]); + encoded.push(token_out.to_be_bytes_vec()[0]); + encoded.extend_from_slice(&split.to_be_bytes_vec()); + encoded.extend(executor_address.to_vec()); + encoded.extend(protocol_data); + encoded + } } + impl EVMStrategyEncoder for SplitSwapStrategyEncoder {} impl StrategyEncoder for SplitSwapStrategyEncoder { @@ -586,7 +744,7 @@ mod tests { #[case::with_check_no_slippage( None, None, - Some(BigUint::from_str("2659881924818443699787").unwrap()), + Some(BigUint::from_str("2659881924818443699787").unwrap()), U256::from_str("2659881924818443699787").unwrap(), )] #[case::no_check_with_slippage( @@ -708,8 +866,110 @@ mod tests { assert_eq!(hex_calldata[1288..], expected_swaps); } + #[rstest] + #[case::with_check_no_slippage( + None, + None, + Some(BigUint::from_str("2659881924818443699787").unwrap()), + U256::from_str("2659881924818443699787").unwrap(), + )] + #[case::no_check_with_slippage( + Some(BigUint::from_str("3_000_000000000000000000").unwrap()), + Some(0.01f64), + None, + U256::from_str("2_970_000000000000000000").unwrap(), + )] + #[case::with_check_and_slippage( + Some(BigUint::from_str("3_000_000000000000000000").unwrap()), + Some(0.01f64), + Some(BigUint::from_str("2_999_000000000000000000").unwrap()), + U256::from_str("2_999_000000000000000000").unwrap(), + )] + fn test_single_swap_strategy_encoder( + #[case] expected_amount: Option, + #[case] slippage: Option, + #[case] checked_amount: Option, + #[case] expected_min_amount: U256, + ) { + // Performs a single swap from WETH to DAI on a USV2 pool, with no grouping optimizations. + // Set up a mock private key for signing + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SingleSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: dai, + expected_amount, + slippage, + checked_amount, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + swaps: vec![swap], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); + let expected_input = [ + "c378044e", // Function selector + "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out + &expected_min_amount_encoded, // min amount out + "0000000000000000000000000000000000000000000000000000000000000000", // wrap + "0000000000000000000000000000000000000000000000000000000000000000", // unwrap + "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + ] + .join(""); + + // after this there is the permit and because of the deadlines (that depend on block time) + // it's hard to assert + + let expected_swap = String::from(concat!( + // length of swap bytes + "0000000000000000000000000000000000000000000000000000000000000051", + "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id + "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver + "00", // zero2one + "00", // exact out + "0000000000000000000000000000", // padding + )); + let hex_calldata = encode(&calldata); + + assert_eq!(hex_calldata[..456], expected_input); + assert_eq!(hex_calldata[1224..], expected_swap); + } + #[test] - fn test_split_swap_strategy_encoder_simple_route_wrap() { + fn test_single_swap_strategy_encoder_wrap() { // Performs a single swap from WETH to DAI on a USV2 pool, wrapping ETH // Note: This test does not assert anything. It is only used to obtain integration test // data for our router solidity test. @@ -761,7 +1021,56 @@ mod tests { } #[test] - fn test_split_swap_strategy_encoder_simple_route_unwrap() { + fn test_split_swap_strategy_encoder_wrap() { + // Performs a single swap from WETH to DAI on a USV2 pool, wrapping ETH + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + + // Set up a mock private key for signing + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = + SplitSwapStrategyEncoder::new(eth_chain(), swap_encoder_registry, Some(private_key)) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: eth(), + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: dai, + expected_amount: None, + checked_amount: Some(BigUint::from_str("2659881924818443699787").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + println!("{}", hex_calldata); + } + + #[test] + fn test_split_swap_strategy_encoder_unwrap() { // Performs a single swap from DAI to WETH on a USV2 pool, unwrapping ETH at the end // Note: This test does not assert anything. It is only used to obtain integration test // data for our router solidity test. @@ -1112,7 +1421,86 @@ mod tests { } #[test] - fn test_split_swap_strategy_encoder_simple_route_no_permit2() { + fn test_single_swap_strategy_encoder_no_permit2() { + // Performs a single swap from WETH to DAI on a USV2 pool, without permit2 and no grouping + // optimizations. + + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + let expected_amount = Some(BigUint::from_str("2_650_000000000000000000").unwrap()); + let slippage = Some(0.01f64); + let checked_amount = Some(BigUint::from_str("2_640_000000000000000000").unwrap()); + let expected_min_amount = U256::from_str("2_640_000000000000000000").unwrap(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SingleSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: dai, + expected_amount, + slippage, + checked_amount, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + swaps: vec![swap], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); + let expected_input = [ + "0f6cbbe8", // Function selector + "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out + &expected_min_amount_encoded, // min amount out + "0000000000000000000000000000000000000000000000000000000000000000", // wrap + "0000000000000000000000000000000000000000000000000000000000000000", // unwrap + "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "0000000000000000000000000000000000000000000000000000000000000100", // offset of swap bytes + "0000000000000000000000000000000000000000000000000000000000000051", // length of swap bytes without padding + + // Swap data + "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id + "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver + "00", // zero2one + "00", // exact out + "0000000000000000000000000000", // padding + ] + .join(""); + + let hex_calldata = encode(&calldata); + + assert_eq!(hex_calldata, expected_input); + println!("{}", hex_calldata); + } + + #[test] + fn test_split_swap_strategy_encoder_no_permit2() { // Performs a single swap from WETH to DAI on a USV2 pool, without permit2 and no grouping // optimizations. @@ -1326,7 +1714,7 @@ mod tests { } #[test] - fn test_cyclic_sequential_swap() { + fn test_cyclic_sequential_swap_split_strategy() { // This test has start and end tokens that are the same // The flow is: // USDC -> WETH -> USDC using two pools From c67c9acac45d780d61493b83e0ed6384f4d6777a Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Thu, 3 Apr 2025 23:35:48 +0200 Subject: [PATCH 14/26] test: Sequential swap integration test - And other small fixes after merge --- foundry/test/TychoRouterIntegration.t.sol | 63 ++++++ .../evm/strategy_encoder/strategy_encoders.rs | 190 ++++++++---------- 2 files changed, 151 insertions(+), 102 deletions(-) diff --git a/foundry/test/TychoRouterIntegration.t.sol b/foundry/test/TychoRouterIntegration.t.sol index a473d9d..b87c322 100644 --- a/foundry/test/TychoRouterIntegration.t.sol +++ b/foundry/test/TychoRouterIntegration.t.sol @@ -28,6 +28,39 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { assertEq(balancerAfter - balancerBefore, 2659881924818443699787); } + function testSequentialIntegration() public { + // Test created with calldata from our router encoder, replacing the executor + // address with the USV2 executor address. + + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + // + // Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools + // + // WETH ───(USV2)──> WBTC ───(USV2)──> USDC + + deal(WETH_ADDR, ALICE, 1 ether); + uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + // Encoded solution generated using `test_split_swap_strategy_encoder_simple` + // but manually replacing the executor address + // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test + // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` + (bool success,) = tychoRouterAddr.call( + hex"51bcc7b60000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000681632fc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067eead0400000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041e396cbca5c5cfa3ea8a9ed8b63a4ac3a8b079e4de510d79f656cd660303e69482f89ed071d177b3ac73030619cc15e5ca42a41543551e47d67089437166978a01c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a60051f6c5be66fff9dc69962d73da0a617a827c382329c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d01395000051f6c5be66fff9dc69962d73da0a617a827c3823292260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d01395010000000000000000000000000000000000000000000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertEq(balancerAfter - balancerBefore, 2659881924818443699787); + } + function testSplitSwapSingleWithoutPermit2Integration() public { // Test created with calldata from our router encoder, replacing the executor // address with the USV2 executor address. @@ -244,6 +277,36 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } + function testSequentialSwapIntegration() public { + // Test created with calldata from our router encoder, replacing the executor + // address with the USV2 executor address. + + // Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools + // + // WETH ──(USV2)──> WBTC ───(USV2)──> USDC + deal(WETH_ADDR, ALICE, 1 ether); + uint256 balancerBefore = IERC20(USDC_ADDR).balanceOf(ALICE); + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + // Encoded solution generated using `test_split_swap_strategy_encoder_complex` + (bool success,) = tychoRouterAddr.call( + hex"51bcc7b60000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068168aea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ef04f200000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004154956683effd126a9182e2d82ebd3d778e5283b93d571b13cdbc9dfbf3d9f655057a2332ed566f79bed7514a22ef1c52969132bc71a5a2ef125d78e39ec264511c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a600515615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950000515615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d01395010000000000000000000000000000000000000000000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = IERC20(USDC_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertEq(balancerAfter - balancerBefore, 2552915143); + + // All input tokens are transferred to the router at first. Make sure we used + // all of it (and thus our splits are correct). + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } + function testCyclicSequentialSwapIntegration() public { deal(USDC_ADDR, ALICE, 100 * 10 ** 6); diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 4f49eee..b3ed871 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -227,7 +227,7 @@ pub struct SequentialSwapStrategyEncoder { impl SequentialSwapStrategyEncoder { pub fn new( - blockchain: tycho_core::models::Chain, + blockchain: tycho_common::models::Chain, swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, router_address: Bytes, @@ -304,9 +304,9 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { let encoding_context = EncodingContext { - receiver: solution.router_address.clone(), + receiver: self.router_address.clone(), exact_out: solution.exact_out, - router_address: self.router_address.clone(), + router_address: Some(self.router_address.clone()), group_token_in: grouped_swap.input_token.clone(), group_token_out: grouped_swap.output_token.clone(), }; @@ -360,7 +360,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { }; let contract_interaction = encode_input(&self.selector, method_calldata); - Ok((contract_interaction, solution.router_address)) + Ok((contract_interaction, self.router_address.clone())) } fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box> { @@ -1091,7 +1091,7 @@ mod tests { Some(private_key), Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), ) - .unwrap(); + .unwrap(); let solution = Solution { exact_out: false, given_token: weth, @@ -1102,7 +1102,6 @@ mod tests { checked_amount, sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap], ..Default::default() }; @@ -1127,18 +1126,18 @@ mod tests { // it's hard to assert let expected_swaps = String::from(concat!( - // length of ple encoded swaps without padding - "0000000000000000000000000000000000000000000000000000000000000053", - // ple encoded swaps - "0051", - // Swap data - "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id - "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver - "00", // zero2one - "00", // exact out - "000000000000000000000000", // padding + // length of ple encoded swaps without padding + "0000000000000000000000000000000000000000000000000000000000000053", + // ple encoded swaps + "0051", + // Swap data + "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id + "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver + "00", // zero2one + "00", // exact out + "000000000000000000000000", // padding )); let hex_calldata = encode(&calldata); @@ -1146,87 +1145,6 @@ mod tests { assert_eq!(hex_calldata[1224..], expected_swaps); } - #[test] - fn test_single_swap_strategy_encoder_wrap() { - // Performs a single swap from WETH to DAI on a USV2 pool, wrapping ETH - // Note: This test does not assert anything. It is only used to obtain integration test - // data for our router solidity test. - - // Set up a mock private key for signing - let private_key = - "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - - let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); - let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0f64, - }; - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SingleSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - Some(private_key), - Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), - ) - .unwrap(); - let solution = Solution { - exact_out: false, - given_token: weth, - given_amount: BigUint::from_str("1_000000000000000000").unwrap(), - checked_token: dai, - expected_amount, - slippage, - checked_amount, - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap], - ..Default::default() - }; - - let (calldata, _) = encoder - .encode_strategy(solution) - .unwrap(); - let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); - let expected_input = [ - "c378044e", // Function selector - "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out - "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out - &expected_min_amount_encoded, // min amount out - "0000000000000000000000000000000000000000000000000000000000000000", // wrap - "0000000000000000000000000000000000000000000000000000000000000000", // unwrap - "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver - ] - .join(""); - - // after this there is the permit and because of the deadlines (that depend on block time) - // it's hard to assert - - let expected_swap = String::from(concat!( - // length of swap bytes - "0000000000000000000000000000000000000000000000000000000000000051", - "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id - "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver - "00", // zero2one - "00", // exact out - "0000000000000000000000000000", // padding - )); - let hex_calldata = encode(&calldata); - - assert_eq!(hex_calldata[..456], expected_input); - assert_eq!(hex_calldata[1224..], expected_swap); - } - #[test] fn test_single_swap_strategy_encoder_wrap() { // Performs a single swap from WETH to DAI on a USV2 pool, wrapping ETH @@ -1475,6 +1393,74 @@ mod tests { println!("{}", _hex_calldata); } + #[test] + fn test_sequential_swap_strategy_encoder_complex_route() { + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + // + // Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools + // + // WETH ───(USV2)──> WBTC ───(USV2)──> USDC + + // Set up a mock private key for signing + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let weth = weth(); + let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + let swap_weth_wbtc = Swap { + component: ProtocolComponent { + id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: wbtc.clone(), + // This represents the remaining 50%, but to avoid any rounding errors we set this to + // 0 to signify "the remainder of the WETH value". It should still be very close to 50% + split: 0f64, + }; + let swap_wbtc_usdc = Swap { + component: ProtocolComponent { + id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: wbtc.clone(), + token_out: usdc.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SequentialSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: usdc, + expected_amount: None, + checked_amount: Some(BigUint::from_str("26173932").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap_weth_wbtc, swap_wbtc_usdc], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let _hex_calldata = encode(&calldata); + println!("{}", _hex_calldata); + } + #[test] fn test_split_encoding_strategy_usv4() { // Performs a sequential swap from USDC to PEPE though ETH using two consecutive USV4 pools @@ -1650,9 +1636,10 @@ mod tests { eth_chain(), swap_encoder_registry, None, + // TODO this should be OPTION Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), ) - .unwrap(); + .unwrap(); let solution = Solution { exact_out: false, given_token: weth, @@ -1663,7 +1650,6 @@ mod tests { checked_amount, sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap], ..Default::default() }; @@ -1736,7 +1722,7 @@ mod tests { None, Some(Bytes::from_str("0x1d1499e622D69689cdf9004d05Ec547d650Ff211").unwrap()), ) - .unwrap(); + .unwrap(); let solution = Solution { exact_out: false, From fcedd39f30033a4ce569f5ecc9a995526f164cdc Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Thu, 3 Apr 2025 23:14:32 +0200 Subject: [PATCH 15/26] fix: Remove router_address from Solution object after merge --- .../evm/strategy_encoder/strategy_encoders.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 8158a57..132e6c9 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -62,7 +62,7 @@ pub struct SingleSwapStrategyEncoder { impl SingleSwapStrategyEncoder { pub fn new( - blockchain: tycho_core::models::Chain, + blockchain: tycho_common::models::Chain, swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, router_address: Bytes, @@ -137,7 +137,7 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { let encoding_context = EncodingContext { receiver: self.router_address.clone(), exact_out: solution.exact_out, - router_address: self.router_address.clone(), + router_address: Some(self.router_address.clone()), group_token_in: grouped_swap.input_token.clone(), group_token_out: grouped_swap.output_token.clone(), }; @@ -927,7 +927,6 @@ mod tests { checked_amount, sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap], ..Default::default() }; @@ -1043,9 +1042,13 @@ mod tests { split: 0f64, }; let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = - SplitSwapStrategyEncoder::new(eth_chain(), swap_encoder_registry, Some(private_key)) - .unwrap(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + Some(private_key), + Some(Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap()), + ) + .unwrap(); let solution = Solution { exact_out: false, given_token: eth(), @@ -1055,7 +1058,6 @@ mod tests { checked_amount: Some(BigUint::from_str("2659881924818443699787").unwrap()), sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap], native_action: Some(NativeAction::Wrap), ..Default::default() @@ -1461,7 +1463,6 @@ mod tests { checked_amount, sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), swaps: vec![swap], ..Default::default() }; From e77a7bc3e1c2734d5b211bd6237ee6ded8958716 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Thu, 3 Apr 2025 23:58:00 +0200 Subject: [PATCH 16/26] test: Fix duplicate test --- foundry/test/TychoRouterIntegration.t.sol | 35 +---------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/foundry/test/TychoRouterIntegration.t.sol b/foundry/test/TychoRouterIntegration.t.sol index b87c322..64d77f5 100644 --- a/foundry/test/TychoRouterIntegration.t.sol +++ b/foundry/test/TychoRouterIntegration.t.sol @@ -28,39 +28,6 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { assertEq(balancerAfter - balancerBefore, 2659881924818443699787); } - function testSequentialIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - - // Note: This test does not assert anything. It is only used to obtain integration test - // data for our router solidity test. - // - // Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools - // - // WETH ───(USV2)──> WBTC ───(USV2)──> USDC - - deal(WETH_ADDR, ALICE, 1 ether); - uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); - - // Approve permit2 - vm.startPrank(ALICE); - IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using `test_split_swap_strategy_encoder_simple` - // but manually replacing the executor address - // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test - // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` - (bool success,) = tychoRouterAddr.call( - hex"51bcc7b60000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000681632fc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067eead0400000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041e396cbca5c5cfa3ea8a9ed8b63a4ac3a8b079e4de510d79f656cd660303e69482f89ed071d177b3ac73030619cc15e5ca42a41543551e47d67089437166978a01c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a60051f6c5be66fff9dc69962d73da0a617a827c382329c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d01395000051f6c5be66fff9dc69962d73da0a617a827c3823292260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d01395010000000000000000000000000000000000000000000000000000" - ); - - vm.stopPrank(); - - uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); - - assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 2659881924818443699787); - } - function testSplitSwapSingleWithoutPermit2Integration() public { // Test created with calldata from our router encoder, replacing the executor // address with the USV2 executor address. @@ -290,7 +257,7 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { // Approve permit2 vm.startPrank(ALICE); IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); - // Encoded solution generated using `test_split_swap_strategy_encoder_complex` + // Encoded solution generated using `test_sequential_swap_strategy_encoder_complex_route` (bool success,) = tychoRouterAddr.call( hex"51bcc7b60000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000068168aea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ef04f200000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004154956683effd126a9182e2d82ebd3d778e5283b93d571b13cdbc9dfbf3d9f655057a2332ed566f79bed7514a22ef1c52969132bc71a5a2ef125d78e39ec264511c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a600515615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950000515615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d01395010000000000000000000000000000000000000000000000000000" ); From f12bebcdfb3ed62b9d4f09520dad331e685c53b0 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Thu, 3 Apr 2025 23:21:24 +0200 Subject: [PATCH 17/26] test: Fix executor address in swap test --- src/encoding/evm/strategy_encoder/strategy_encoders.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 132e6c9..4498c3b 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -953,7 +953,7 @@ mod tests { let expected_swap = String::from(concat!( // length of swap bytes "0000000000000000000000000000000000000000000000000000000000000051", - "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver @@ -1484,7 +1484,7 @@ mod tests { "0000000000000000000000000000000000000000000000000000000000000051", // length of swap bytes without padding // Swap data - "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver From 2f81b167d428c8947b795929db2a96010efa9817 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 4 Apr 2025 15:37:43 +0100 Subject: [PATCH 18/26] fix: Fix executor address in test and remove duplicated test --- don't change below this line --- ENG-4306 Took 12 minutes Took 18 seconds --- .../evm/strategy_encoder/strategy_encoders.rs | 99 ++----------------- 1 file changed, 9 insertions(+), 90 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index b3ed871..14ba38c 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -1059,7 +1059,7 @@ mod tests { Some(BigUint::from_str("2_999_000000000000000000").unwrap()), U256::from_str("2_999_000000000000000000").unwrap(), )] - fn test_sequential_swap_strategy_encoder_simple_route( + fn test_single_swap_strategy_encoder( #[case] expected_amount: Option, #[case] slippage: Option, #[case] checked_amount: Option, @@ -1085,7 +1085,7 @@ mod tests { split: 0f64, }; let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SequentialSwapStrategyEncoder::new( + let encoder = SingleSwapStrategyEncoder::new( eth_chain(), swap_encoder_registry, Some(private_key), @@ -1111,7 +1111,7 @@ mod tests { .unwrap(); let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); let expected_input = [ - "51bcc7b6", // Function selector + "c378044e", // Function selector "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out @@ -1125,24 +1125,22 @@ mod tests { // after this there is the permit and because of the deadlines (that depend on block time) // it's hard to assert - let expected_swaps = String::from(concat!( + let expected_swap = String::from(concat!( // length of ple encoded swaps without padding - "0000000000000000000000000000000000000000000000000000000000000053", - // ple encoded swaps - "0051", + "0000000000000000000000000000000000000000000000000000000000000051", // Swap data - "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver "00", // zero2one "00", // exact out - "000000000000000000000000", // padding + "0000000000000000000000000000", // padding )); let hex_calldata = encode(&calldata); assert_eq!(hex_calldata[..456], expected_input); - assert_eq!(hex_calldata[1224..], expected_swaps); + assert_eq!(hex_calldata[1224..], expected_swap); } #[test] @@ -1608,85 +1606,6 @@ mod tests { println!("{}", hex_calldata); } - #[test] - fn test_single_swap_strategy_encoder_no_permit2() { - // Performs a single swap from WETH to DAI on a USV2 pool, without permit2 and no grouping - // optimizations. - - let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); - let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - - let expected_amount = Some(BigUint::from_str("2_650_000000000000000000").unwrap()); - let slippage = Some(0.01f64); - let checked_amount = Some(BigUint::from_str("2_640_000000000000000000").unwrap()); - let expected_min_amount = U256::from_str("2_640_000000000000000000").unwrap(); - - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth.clone(), - token_out: dai.clone(), - split: 0f64, - }; - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = SingleSwapStrategyEncoder::new( - eth_chain(), - swap_encoder_registry, - None, - // TODO this should be OPTION - Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), - ) - .unwrap(); - let solution = Solution { - exact_out: false, - given_token: weth, - given_amount: BigUint::from_str("1_000000000000000000").unwrap(), - checked_token: dai, - expected_amount, - slippage, - checked_amount, - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap], - ..Default::default() - }; - - let (calldata, _) = encoder - .encode_strategy(solution) - .unwrap(); - let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); - let expected_input = [ - "0f6cbbe8", // Function selector - "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out - "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out - &expected_min_amount_encoded, // min amount out - "0000000000000000000000000000000000000000000000000000000000000000", // wrap - "0000000000000000000000000000000000000000000000000000000000000000", // unwrap - "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver - "0000000000000000000000000000000000000000000000000000000000000100", // offset of swap bytes - "0000000000000000000000000000000000000000000000000000000000000051", // length of swap bytes without padding - - // Swap data - "f6c5be66fff9dc69962d73da0a617a827c382329", // executor address - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in - "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id - "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver - "00", // zero2one - "00", // exact out - "0000000000000000000000000000", // padding - ] - .join(""); - - let hex_calldata = encode(&calldata); - - assert_eq!(hex_calldata, expected_input); - println!("{}", hex_calldata); - } - #[test] fn test_split_encoding_strategy_ekubo() { // ETH ──(EKUBO)──> USDC @@ -1748,7 +1667,7 @@ mod tests { } #[test] - fn test_split_swap_strategy_encoder_simple_route_no_permit2() { + fn test_single_swap_strategy_encoder_no_permit2() { // Performs a single swap from WETH to DAI on a USV2 pool, without permit2 and no grouping // optimizations. From 6430c99d7665e855d950dde7f199e9f69cf1f642 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 4 Apr 2025 15:47:30 +0100 Subject: [PATCH 19/26] feat: Delete EVMStrategyEncoder (this is now unnecessary) Moved ple_encode into utils.rs --- don't change below this line --- ENG-4306 Took 5 minutes --- .../evm/strategy_encoder/strategy_encoders.rs | 28 ++----------------- src/encoding/evm/utils.rs | 17 +++++++++++ 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 4498c3b..f2192bb 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -16,7 +16,7 @@ use crate::encoding::{ swap_encoder::swap_encoder_registry::SwapEncoderRegistry, utils::{ biguint_to_u256, bytes_to_address, encode_input, get_min_amount_for_solution, - get_token_position, percentage_to_uint24, + get_token_position, percentage_to_uint24, ple_encode, }, }, models::{Chain, EncodingContext, NativeAction, Solution}, @@ -24,24 +24,6 @@ use crate::encoding::{ swap_encoder::SwapEncoder, }; -/// Encodes a solution using a specific strategy for execution on the EVM-compatible network. -pub trait EVMStrategyEncoder: StrategyEncoder { - /// Uses prefix-length encoding to efficient encode action data. - /// - /// Prefix-length encoding is a data encoding method where the beginning of a data segment - /// (the "prefix") contains information about the length of the following data. - fn ple_encode(&self, action_data_array: Vec>) -> Vec { - let mut encoded_action_data: Vec = Vec::new(); - - for action_data in action_data_array { - let args = (encoded_action_data, action_data.len() as u16, action_data); - encoded_action_data = args.abi_encode_packed(); - } - - encoded_action_data - } -} - /// Represents the encoder for a swap strategy which supports single swaps. /// /// # Fields @@ -90,8 +72,6 @@ impl SingleSwapStrategyEncoder { } } -impl EVMStrategyEncoder for SingleSwapStrategyEncoder {} - impl StrategyEncoder for SingleSwapStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { let grouped_swaps = group_swaps(solution.clone().swaps); @@ -285,8 +265,6 @@ impl SplitSwapStrategyEncoder { } } -impl EVMStrategyEncoder for SplitSwapStrategyEncoder {} - impl StrategyEncoder for SplitSwapStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { self.split_swap_validator @@ -387,7 +365,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { swaps.push(swap_data); } - let encoded_swaps = self.ple_encode(swaps); + let encoded_swaps = ple_encode(swaps); let tokens_len = if solution.given_token == solution.checked_token { tokens.len() - 1 } else { @@ -458,7 +436,7 @@ impl ExecutorStrategyEncoder { Self { swap_encoder_registry } } } -impl EVMStrategyEncoder for ExecutorStrategyEncoder {} + impl StrategyEncoder for ExecutorStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { let grouped_swaps = group_swaps(solution.clone().swaps); diff --git a/src/encoding/evm/utils.rs b/src/encoding/evm/utils.rs index b8af4f5..0ac8f01 100644 --- a/src/encoding/evm/utils.rs +++ b/src/encoding/evm/utils.rs @@ -1,6 +1,7 @@ use std::{cmp::max, sync::Arc}; use alloy_primitives::{aliases::U24, keccak256, Address, FixedBytes, Keccak256, U256, U8}; +use alloy_sol_types::SolValue; use num_bigint::BigUint; use tokio::runtime::{Handle, Runtime}; use tycho_common::Bytes; @@ -133,6 +134,22 @@ pub fn get_runtime() -> Result<(Handle, Option>), EncodingError> { } } } + +/// Uses prefix-length encoding to efficient encode action data. +/// +/// Prefix-length encoding is a data encoding method where the beginning of a data segment +/// (the "prefix") contains information about the length of the following data. +pub fn ple_encode(action_data_array: Vec>) -> Vec { + let mut encoded_action_data: Vec = Vec::new(); + + for action_data in action_data_array { + let args = (encoded_action_data, action_data.len() as u16, action_data); + encoded_action_data = args.abi_encode_packed(); + } + + encoded_action_data +} + #[cfg(test)] mod tests { use num_bigint::BigUint; From 6e7bf3c019c32d586d406bf420daa0080dcda1fd Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 4 Apr 2025 16:48:20 +0100 Subject: [PATCH 20/26] feat: Add SequentialSwap integration test with regular approvals Improve docstrings --- don't change below this line --- ENG-4306 Took 20 minutes --- foundry/test/TychoRouterIntegration.t.sol | 48 ++++++------ .../evm/strategy_encoder/strategy_encoders.rs | 74 +++++++++++++++++-- 2 files changed, 91 insertions(+), 31 deletions(-) diff --git a/foundry/test/TychoRouterIntegration.t.sol b/foundry/test/TychoRouterIntegration.t.sol index 64d77f5..85ade39 100644 --- a/foundry/test/TychoRouterIntegration.t.sol +++ b/foundry/test/TychoRouterIntegration.t.sol @@ -5,9 +5,6 @@ import "./TychoRouterTestSetup.sol"; contract TychoRouterTestIntegration is TychoRouterTestSetup { function testSplitSwapSingleIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - // Tests swapping WETH -> DAI on a USV2 pool deal(WETH_ADDR, ALICE, 1 ether); uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); @@ -29,9 +26,6 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { } function testSplitSwapSingleWithoutPermit2Integration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - // Tests swapping WETH -> DAI on a USV2 pool without permit2 deal(WETH_ADDR, ALICE, 1 ether); vm.startPrank(ALICE); @@ -127,9 +121,6 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { } function testSplitSwapSingleWithWrapIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - // Tests swapping WETH -> DAI on a USV2 pool, but ETH is received from the user // and wrapped before the swap deal(ALICE, 1 ether); @@ -151,9 +142,6 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { } function testSplitSwapSingleWithUnwrapIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - // Tests swapping DAI -> WETH on a USV2 pool, and WETH is unwrapped to ETH // before sending back to the user deal(DAI_ADDR, ALICE, 3000 ether); @@ -206,16 +194,10 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { assertTrue(success, "Call Failed"); assertGe(balancerAfter - balancerBefore, 26173932); - - // All input tokens are transferred to the router at first. Make sure we used - // all of it (and thus our splits are correct). assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } function testSplitSwapIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - // Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools // // ┌──(USV2)──> WBTC ───(USV2)──> USDC @@ -244,10 +226,7 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSequentialSwapIntegration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV2 executor address. - + function testSequentialSwapIntegrationPermit2() public { // Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools // // WETH ──(USV2)──> WBTC ───(USV2)──> USDC @@ -268,9 +247,30 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { assertTrue(success, "Call Failed"); assertEq(balancerAfter - balancerBefore, 2552915143); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); + } - // All input tokens are transferred to the router at first. Make sure we used - // all of it (and thus our splits are correct). + function testSequentialSwapIntegration() public { + // Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools + // + // WETH ──(USV2)──> WBTC ───(USV2)──> USDC + deal(WETH_ADDR, ALICE, 1 ether); + uint256 balancerBefore = IERC20(USDC_ADDR).balanceOf(ALICE); + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max); + // Encoded solution generated using `test_sequential_swap_strategy_encoder_no_permit2` + (bool success,) = tychoRouterAddr.call( + hex"e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000a600515615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950000515615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d01395010000000000000000000000000000000000000000000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = IERC20(USDC_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertEq(balancerAfter - balancerBefore, 2552915143); assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 14ba38c..adffe4d 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -83,7 +83,7 @@ impl SingleSwapStrategyEncoder { Ok(Self { permit2, selector, swap_encoder_registry, router_address }) } - /// Encodes information necessary for performing a single swap against a given executor for + /// Encodes information necessary for performing a single hop against a given executor for /// a protocol. fn encode_swap_header(&self, executor_address: Bytes, protocol_data: Vec) -> Vec { let mut encoded = Vec::new(); @@ -202,7 +202,7 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { } } -/// Represents the encoder for a swap strategy which supports single swaps. +/// Represents the encoder for a swap strategy which supports sequential swaps. /// /// # Fields /// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders @@ -253,7 +253,7 @@ impl SequentialSwapStrategyEncoder { }) } - /// Encodes information necessary for performing a single swap against a given executor for + /// Encodes information necessary for performing a single hop against a given executor for /// a protocol. fn encode_swap_header(&self, executor_address: Bytes, protocol_data: Vec) -> Vec { let mut encoded = Vec::new(); @@ -373,7 +373,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { } } -/// Represents the encoder for a swap strategy which supports single, sequential and split swaps. +/// Represents the encoder for a swap strategy which supports split swaps. /// /// # Fields /// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders @@ -439,7 +439,7 @@ impl SplitSwapStrategyEncoder { }) } - /// Encodes information necessary for performing a single swap against a given executor for + /// Encodes information necessary for performing a single hop against a given executor for /// a protocol as part of a split swap solution. fn encode_swap_header( &self, @@ -1416,8 +1416,6 @@ mod tests { }, token_in: weth.clone(), token_out: wbtc.clone(), - // This represents the remaining 50%, but to avoid any rounding errors we set this to - // 0 to signify "the remainder of the WETH value". It should still be very close to 50% split: 0f64, }; let swap_wbtc_usdc = Swap { @@ -1459,6 +1457,68 @@ mod tests { println!("{}", _hex_calldata); } + #[test] + fn test_sequential_swap_strategy_encoder_no_permit2() { + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + // + // Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools + // + // WETH ───(USV2)──> WBTC ───(USV2)──> USDC + + let weth = weth(); + let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + let swap_weth_wbtc = Swap { + component: ProtocolComponent { + id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: wbtc.clone(), + split: 0f64, + }; + let swap_wbtc_usdc = Swap { + component: ProtocolComponent { + id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: wbtc.clone(), + token_out: usdc.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SequentialSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: usdc, + expected_amount: None, + checked_amount: Some(BigUint::from_str("26173932").unwrap()), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap_weth_wbtc, swap_wbtc_usdc], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let _hex_calldata = encode(&calldata); + println!("{}", _hex_calldata); + } + #[test] fn test_split_encoding_strategy_usv4() { // Performs a sequential swap from USDC to PEPE though ETH using two consecutive USV4 pools From df1c05ea00d23855787dd2df45e7937ec9fc23ec Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 4 Apr 2025 16:19:20 +0100 Subject: [PATCH 21/26] feat: Add SingleSwap integration test and fix bug in method signatures --- don't change below this line --- ENG-4306 Took 27 minutes Took 15 seconds --- foundry/test/TychoRouterSingleSwap.t.sol | 38 +++++++++++++++++++ .../evm/strategy_encoder/strategy_encoders.rs | 11 +++--- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/foundry/test/TychoRouterSingleSwap.t.sol b/foundry/test/TychoRouterSingleSwap.t.sol index ceb4f6c..9ca7e40 100644 --- a/foundry/test/TychoRouterSingleSwap.t.sol +++ b/foundry/test/TychoRouterSingleSwap.t.sol @@ -304,4 +304,42 @@ contract TychoRouterSingleSwapTest is TychoRouterTestSetup { vm.stopPrank(); } + + function testSingleSwapIntegration() public { + // Tests swapping WETH -> DAI on a USV2 pool with regular approvals + deal(WETH_ADDR, ALICE, 1 ether); + uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); + + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max); + // Encoded solution generated using `test_single_swap_strategy_encoder_no_permit2` + (bool success,) = tychoRouterAddr.call( + hex"20144a070000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000008f1d5c1cae3740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000515615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); + assertTrue(success, "Call Failed"); + assertEq(balancerAfter - balancerBefore, 2659881924818443699787); + } + + function testSingleSwapIntegrationPermit2() public { + // Tests swapping WETH -> DAI on a USV2 pool with permit2 + deal(WETH_ADDR, ALICE, 1 ether); + uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); + + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + // Encoded solution generated using `test_single_swap_strategy_encoder` + (bool success,) = tychoRouterAddr.call( + hex"30ace1b10000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000903146e5f6c59c064b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006817833200000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067effd3a00000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000417efea09004d5d40d8d072e1ce0a425507717ea485c765eb90c170859197d362b502fb039b4f5cdce57318ecfe3ab276d1ac87771eb5d017b253a8f4107e6a20b1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000515615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); + assertTrue(success, "Call Failed"); + assertEq(balancerAfter - balancerBefore, 2659881924818443699787); + } } diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index f2192bb..948f4a5 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -51,12 +51,11 @@ impl SingleSwapStrategyEncoder { ) -> Result { let chain = Chain::from(blockchain); let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { - (Some(Permit2::new(swapper_pk, chain.clone())?), "singleSwapPermit2(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) + (Some(Permit2::new(swapper_pk, chain.clone())?), "singleSwapPermit2(uint256,address,address,uint256,bool,bool,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) } else { ( None, - "singleSwap(uint256,address,address,uint256,bool,bool,uint256,address,bytes)" - .to_string(), + "singleSwap(uint256,address,address,uint256,bool,bool,address,bytes)".to_string(), ) }; Ok(Self { permit2, selector, swap_encoder_registry, router_address }) @@ -914,7 +913,7 @@ mod tests { .unwrap(); let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); let expected_input = [ - "c378044e", // Function selector + "30ace1b1", // Function selector "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out @@ -1450,8 +1449,8 @@ mod tests { .unwrap(); let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); let expected_input = [ - "0f6cbbe8", // Function selector - "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount out + "20144a07", // Function selector + "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount in "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out &expected_min_amount_encoded, // min amount out From 8b4b79b353a34011fb25877273d78962bdce60f6 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Mon, 7 Apr 2025 18:41:14 +0100 Subject: [PATCH 22/26] feat: Refactor TychoEncoder We have a trait TychoEncoder and then two implementations: TychoRouterEncoder and TychoExecutorEncoder. This way we go a level above with the decision if it is a direct execution or if it should use the tycho router. - Created two builders: one for each tycho encoder - Delete ExecutorStrategyEncoder and move code straight into the TychoExecutorEncoder - Add validate_solution to trait TychoEncoder - Move group_swaps.rs a level up - Update tests and usage cases Doing this we get rid of all that weird stuff we were doing before --- don't change below this line --- ENG-4306 Took 2 hours 6 minutes Took 12 seconds --- examples/encoding-example/main.rs | 12 +- src/bin/tycho-encode.rs | 51 +- src/encoding/evm/encoder_builder.rs | 150 --- src/encoding/evm/encoder_builders.rs | 137 +++ .../evm/{strategy_encoder => }/group_swaps.rs | 0 src/encoding/evm/mod.rs | 5 +- src/encoding/evm/strategy_encoder/mod.rs | 1 - .../evm/strategy_encoder/strategy_encoders.rs | 273 +---- src/encoding/evm/tycho_encoder.rs | 720 ------------ src/encoding/evm/tycho_encoders.rs | 1039 +++++++++++++++++ src/encoding/tycho_encoder.rs | 7 +- 11 files changed, 1217 insertions(+), 1178 deletions(-) delete mode 100644 src/encoding/evm/encoder_builder.rs create mode 100644 src/encoding/evm/encoder_builders.rs rename src/encoding/evm/{strategy_encoder => }/group_swaps.rs (100%) delete mode 100644 src/encoding/evm/tycho_encoder.rs create mode 100644 src/encoding/evm/tycho_encoders.rs diff --git a/examples/encoding-example/main.rs b/examples/encoding-example/main.rs index 7ccc0bc..d60a144 100644 --- a/examples/encoding-example/main.rs +++ b/examples/encoding-example/main.rs @@ -6,9 +6,8 @@ use tycho_common::{ Bytes, }; use tycho_execution::encoding::{ - evm::encoder_builder::EVMEncoderBuilder, + evm::encoder_builders::TychoRouterEncoderBuilder, models::{Solution, Swap}, - tycho_encoder::TychoEncoder, }; fn main() { @@ -19,10 +18,9 @@ fn main() { .expect("Failed to create user address"); // Initialize the encoder - let encoder = EVMEncoderBuilder::new() + let encoder = TychoRouterEncoderBuilder::new() .chain(Chain::Ethereum) - .initialize_tycho_router_with_permit2(swapper_pk) - .expect("Failed to create encoder builder") + .swapper_pk(swapper_pk) .build() .expect("Failed to build encoder"); @@ -64,7 +62,7 @@ fn main() { // Encode the solution let tx = encoder - .encode_router_calldata(vec![solution.clone()]) + .encode_calldata(vec![solution.clone()]) .expect("Failed to encode router calldata")[0] .clone(); println!(" ====== Simple swap WETH -> USDC ======"); @@ -137,7 +135,7 @@ fn main() { // Encode the solution let complex_tx = encoder - .encode_router_calldata(vec![complex_solution]) + .encode_calldata(vec![complex_solution]) .expect("Failed to encode router calldata")[0] .clone(); diff --git a/src/bin/tycho-encode.rs b/src/bin/tycho-encode.rs index c0216fa..1edefd7 100644 --- a/src/bin/tycho-encode.rs +++ b/src/bin/tycho-encode.rs @@ -3,7 +3,9 @@ use std::io::{self, Read}; use clap::{Parser, Subcommand}; use tycho_common::{hex_bytes::Bytes, models::Chain}; use tycho_execution::encoding::{ - evm::encoder_builder::EVMEncoderBuilder, models::Solution, tycho_encoder::TychoEncoder, + evm::encoder_builders::{TychoExecutorEncoderBuilder, TychoRouterEncoderBuilder}, + models::Solution, + tycho_encoder::TychoEncoder, }; #[derive(Parser)] @@ -45,19 +47,16 @@ pub struct Cli { executors_file_path: Option, #[arg(short, long)] router_address: Option, + #[arg(short, long)] + swapper_pk: Option, } #[derive(Subcommand)] pub enum Commands { - /// Use the Tycho router encoding strategy + /// Use Tycho router encoding TychoRouter, - /// Use the Tycho router encoding strategy with Permit2 approval and token in transfer - TychoRouterPermit2 { - #[arg(short, long)] - swapper_pk: String, - }, - /// Use the direct execution encoding strategy - DirectExecution, + /// Use direct execution encoding + TychoExecutor, } fn main() -> Result<(), Box> { @@ -75,24 +74,26 @@ fn main() -> Result<(), Box> { } let solution: Solution = serde_json::from_str(&buffer)?; - let mut builder = EVMEncoderBuilder::new().chain(chain); - - if let Some(config_path) = cli.executors_file_path { - builder = builder.executors_file_path(config_path); - } - if let Some(router_address) = cli.router_address { - builder = builder.router_address(router_address); - } - - builder = match cli.command { - Commands::TychoRouter => builder.initialize_tycho_router()?, - Commands::TychoRouterPermit2 { swapper_pk } => { - builder.initialize_tycho_router_with_permit2(swapper_pk)? + let encoder: Box = match cli.command { + Commands::TychoRouter => { + let mut builder = TychoRouterEncoderBuilder::new().chain(chain); + if let Some(config_path) = cli.executors_file_path { + builder = builder.executors_file_path(config_path); + } + if let Some(router_address) = cli.router_address { + builder = builder.router_address(router_address); + } + if let Some(swapper_pk) = cli.swapper_pk { + builder = builder.swapper_pk(swapper_pk); + } + builder.build()? } - Commands::DirectExecution => builder.initialize_direct_execution()?, + Commands::TychoExecutor => TychoExecutorEncoderBuilder::new() + .chain(chain) + .build()?, }; - let encoder = builder.build()?; - let transactions = encoder.encode_router_calldata(vec![solution])?; + + let transactions = encoder.encode_calldata(vec![solution])?; let encoded = serde_json::json!({ "to": format!("0x{}", hex::encode(&transactions[0].to)), "value": format!("0x{}", hex::encode(transactions[0].value.to_bytes_be())), diff --git a/src/encoding/evm/encoder_builder.rs b/src/encoding/evm/encoder_builder.rs deleted file mode 100644 index e20c148..0000000 --- a/src/encoding/evm/encoder_builder.rs +++ /dev/null @@ -1,150 +0,0 @@ -use tycho_common::{models::Chain, Bytes}; - -use crate::encoding::{ - errors::EncodingError, - evm::{ - strategy_encoder::strategy_encoders::{ExecutorStrategyEncoder, SplitSwapStrategyEncoder}, - swap_encoder::swap_encoder_registry::SwapEncoderRegistry, - tycho_encoder::EVMTychoEncoder, - }, - strategy_encoder::StrategyEncoder, -}; - -/// Builder pattern for constructing an `EVMTychoEncoder` with customizable options. -/// -/// This struct allows setting a chain and strategy encoder before building the final encoder. -pub struct EVMEncoderBuilder { - strategy: Option>, - chain: Option, - executors_file_path: Option, - router_address: Option, -} - -impl Default for EVMEncoderBuilder { - fn default() -> Self { - Self::new() - } -} - -impl EVMEncoderBuilder { - pub fn new() -> Self { - EVMEncoderBuilder { - chain: None, - strategy: None, - executors_file_path: None, - router_address: None, - } - } - pub fn chain(mut self, chain: Chain) -> Self { - self.chain = Some(chain); - self - } - - /// Sets the `executors_file_path` manually. - /// If it's not set, the default path will be used (config/executor_addresses.json) - pub fn executors_file_path(mut self, executors_file_path: String) -> Self { - self.executors_file_path = Some(executors_file_path); - self - } - - /// Sets the `router_address` manually. - /// If it's not set, the default router address will be used (config/router_addresses.json) - pub fn router_address(mut self, router_address: Bytes) -> Self { - self.router_address = Some(router_address); - self - } - - /// Sets the `strategy_encoder` manually. - /// - /// **Note**: This method should not be used in combination with `tycho_router` or - /// `direct_execution`. - pub fn strategy_encoder(mut self, strategy: Box) -> Self { - self.strategy = Some(strategy); - self - } - - /// Shortcut method to initialize a `SplitSwapStrategyEncoder` without any approval nor token in - /// transfer. **Note**: Should not be used at the same time as `strategy_encoder`. - pub fn initialize_tycho_router(self) -> Result { - if let Some(chain) = self.chain { - let swap_encoder_registry = - SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; - let strategy = Box::new(SplitSwapStrategyEncoder::new( - chain, - swap_encoder_registry, - None, - self.router_address.clone(), - )?); - Ok(EVMEncoderBuilder { - chain: Some(chain), - strategy: Some(strategy), - executors_file_path: self.executors_file_path, - router_address: self.router_address, - }) - } else { - Err(EncodingError::FatalError( - "Please set the chain before setting the tycho router".to_string(), - )) - } - } - - /// Shortcut method to initialize a `SplitSwapStrategyEncoder` with Permit2 approval and token - /// in transfer. **Note**: Should not be used at the same time as `strategy_encoder`. - pub fn initialize_tycho_router_with_permit2( - self, - swapper_pk: String, - ) -> Result { - if let Some(chain) = self.chain { - let swap_encoder_registry = - SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; - let strategy = Box::new(SplitSwapStrategyEncoder::new( - chain, - swap_encoder_registry, - Some(swapper_pk), - self.router_address.clone(), - )?); - Ok(EVMEncoderBuilder { - chain: Some(chain), - strategy: Some(strategy), - executors_file_path: self.executors_file_path, - router_address: self.router_address, - }) - } else { - Err(EncodingError::FatalError( - "Please set the chain before setting the tycho router".to_string(), - )) - } - } - - /// Shortcut method to initialize an `ExecutorStrategyEncoder`. - /// **Note**: Should not be used at the same time as `strategy_encoder`. - pub fn initialize_direct_execution(self) -> Result { - if let Some(chain) = self.chain { - let swap_encoder_registry = - SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; - let strategy = Box::new(ExecutorStrategyEncoder::new(swap_encoder_registry)); - Ok(EVMEncoderBuilder { - chain: Some(chain), - strategy: Some(strategy), - executors_file_path: self.executors_file_path, - router_address: self.router_address, - }) - } else { - Err(EncodingError::FatalError( - "Please set the chain before setting the strategy".to_string(), - )) - } - } - - /// Builds the `EVMTychoEncoder` instance using the configured chain and strategy. - /// Returns an error if either the chain or strategy has not been set. - pub fn build(self) -> Result { - if let (Some(chain), Some(strategy)) = (self.chain, self.strategy) { - EVMTychoEncoder::new(chain, strategy) - } else { - Err(EncodingError::FatalError( - "Please set the chain and strategy before building the encoder".to_string(), - )) - } - } -} diff --git a/src/encoding/evm/encoder_builders.rs b/src/encoding/evm/encoder_builders.rs new file mode 100644 index 0000000..13e3355 --- /dev/null +++ b/src/encoding/evm/encoder_builders.rs @@ -0,0 +1,137 @@ +use tycho_common::{models::Chain, Bytes}; + +use crate::encoding::{ + errors::EncodingError, + evm::{ + strategy_encoder::strategy_encoders::SplitSwapStrategyEncoder, + swap_encoder::swap_encoder_registry::SwapEncoderRegistry, + tycho_encoders::{TychoExecutorEncoder, TychoRouterEncoder}, + }, + strategy_encoder::StrategyEncoder, + tycho_encoder::TychoEncoder, +}; + +/// Builder pattern for constructing a `TychoRouterEncoder` with customizable options. +/// +/// This struct allows setting a chain and strategy encoder before building the final encoder. +pub struct TychoRouterEncoderBuilder { + swapper_pk: Option, + strategy: Option>, + chain: Option, + executors_file_path: Option, + router_address: Option, +} + +impl Default for TychoRouterEncoderBuilder { + fn default() -> Self { + Self::new() + } +} + +impl TychoRouterEncoderBuilder { + pub fn new() -> Self { + TychoRouterEncoderBuilder { + swapper_pk: None, + chain: None, + strategy: None, + executors_file_path: None, + router_address: None, + } + } + pub fn chain(mut self, chain: Chain) -> Self { + self.chain = Some(chain); + self + } + + /// Sets the `executors_file_path` manually. + /// If it's not set, the default path will be used (config/executor_addresses.json) + pub fn executors_file_path(mut self, executors_file_path: String) -> Self { + self.executors_file_path = Some(executors_file_path); + self + } + + /// Sets the `router_address` manually. + /// If it's not set, the default router address will be used (config/router_addresses.json) + pub fn router_address(mut self, router_address: Bytes) -> Self { + self.router_address = Some(router_address); + self + } + + pub fn swapper_pk(mut self, swapper_pk: String) -> Self { + self.swapper_pk = Some(swapper_pk); + self + } + + /// Sets the `strategy_encoder` manually. + /// + /// **Note**: This method should not be used in combination with `tycho_router` or + /// `direct_execution`. + pub fn strategy_encoder(mut self, strategy: Box) -> Self { + self.strategy = Some(strategy); + self + } + + /// Builds the `TychoRouterEncoder` instance using the configured chain and strategy. + /// Returns an error if either the chain or strategy has not been set. + pub fn build(self) -> Result, EncodingError> { + if let Some(chain) = self.chain { + let swap_encoder_registry = + SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; + + let strategy = Box::new(SplitSwapStrategyEncoder::new( + chain, + swap_encoder_registry, + self.swapper_pk, + self.router_address.clone(), + )?); + Ok(Box::new(TychoRouterEncoder::new(chain, strategy)?)) + } else { + Err(EncodingError::FatalError( + "Please set the chain and strategy before building the encoder".to_string(), + )) + } + } +} + +/// Builder pattern for constructing a `TychoExecutorEncoder` with customizable options. +pub struct TychoExecutorEncoderBuilder { + chain: Option, + executors_file_path: Option, +} + +impl Default for TychoExecutorEncoderBuilder { + fn default() -> Self { + Self::new() + } +} + +impl TychoExecutorEncoderBuilder { + pub fn new() -> Self { + TychoExecutorEncoderBuilder { chain: None, executors_file_path: None } + } + pub fn chain(mut self, chain: Chain) -> Self { + self.chain = Some(chain); + self + } + + /// Sets the `executors_file_path` manually. + /// If it's not set, the default path will be used (config/executor_addresses.json) + pub fn executors_file_path(mut self, executors_file_path: String) -> Self { + self.executors_file_path = Some(executors_file_path); + self + } + + /// Builds the `TychoExecutorEncoder` instance using the configured chain and strategy. + /// Returns an error if either the chain or strategy has not been set. + pub fn build(self) -> Result, EncodingError> { + if let Some(chain) = self.chain { + let swap_encoder_registry = + SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; + Ok(Box::new(TychoExecutorEncoder::new(chain, swap_encoder_registry)?)) + } else { + Err(EncodingError::FatalError( + "Please set the chain and strategy before building the encoder".to_string(), + )) + } + } +} diff --git a/src/encoding/evm/strategy_encoder/group_swaps.rs b/src/encoding/evm/group_swaps.rs similarity index 100% rename from src/encoding/evm/strategy_encoder/group_swaps.rs rename to src/encoding/evm/group_swaps.rs diff --git a/src/encoding/evm/mod.rs b/src/encoding/evm/mod.rs index b86b4dd..344aaa1 100644 --- a/src/encoding/evm/mod.rs +++ b/src/encoding/evm/mod.rs @@ -1,7 +1,8 @@ pub mod approvals; mod constants; -pub mod encoder_builder; +pub mod encoder_builders; +mod group_swaps; pub mod strategy_encoder; mod swap_encoder; -pub mod tycho_encoder; +pub mod tycho_encoders; pub mod utils; diff --git a/src/encoding/evm/strategy_encoder/mod.rs b/src/encoding/evm/strategy_encoder/mod.rs index 14137b6..8f53524 100644 --- a/src/encoding/evm/strategy_encoder/mod.rs +++ b/src/encoding/evm/strategy_encoder/mod.rs @@ -1,3 +1,2 @@ -mod group_swaps; pub mod strategy_encoders; mod strategy_validators; diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 61a171a..94d55a1 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -12,9 +12,9 @@ use crate::encoding::{ evm::{ approvals::permit2::Permit2, constants::DEFAULT_ROUTERS_JSON, - strategy_encoder::{ - group_swaps::group_swaps, - strategy_validators::{SequentialSwapValidator, SplitSwapValidator, SwapValidator}, + group_swaps::group_swaps, + strategy_encoder::strategy_validators::{ + SequentialSwapValidator, SplitSwapValidator, SwapValidator, }, swap_encoder::swap_encoder_registry::SwapEncoderRegistry, utils::{ @@ -242,8 +242,6 @@ impl SequentialSwapStrategyEncoder { } } -impl EVMStrategyEncoder for SequentialSwapStrategyEncoder {} - impl StrategyEncoder for SequentialSwapStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { self.sequential_swap_validator @@ -303,7 +301,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { swaps.push(swap_data); } - let encoded_swaps = self.ple_encode(swaps); + let encoded_swaps = ple_encode(swaps); let method_calldata = if let Some(permit2) = self.permit2.clone() { let (permit, signature) = permit2.get_permit( &self.router_address, @@ -594,77 +592,6 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { } } -/// This strategy encoder is used for solutions that are sent directly to the executor, bypassing -/// the router. Only one solution with one swap is supported. -/// -/// # Fields -/// * `swap_encoder_registry`: SwapEncoderRegistry, containing all possible swap encoders -#[derive(Clone)] -pub struct ExecutorStrategyEncoder { - swap_encoder_registry: SwapEncoderRegistry, -} - -impl ExecutorStrategyEncoder { - pub fn new(swap_encoder_registry: SwapEncoderRegistry) -> Self { - Self { swap_encoder_registry } - } -} - -impl StrategyEncoder for ExecutorStrategyEncoder { - fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { - let grouped_swaps = group_swaps(solution.clone().swaps); - let number_of_groups = grouped_swaps.len(); - if number_of_groups > 1 { - return Err(EncodingError::InvalidInput(format!( - "Executor strategy only supports one swap for non-groupable protocols. Found {}", - number_of_groups - ))) - } - - let grouped_swap = grouped_swaps - .first() - .ok_or_else(|| EncodingError::FatalError("Swap grouping failed".to_string()))?; - - let receiver = solution.receiver; - - let swap_encoder = self - .get_swap_encoder(&grouped_swap.protocol_system) - .ok_or_else(|| { - EncodingError::InvalidInput(format!( - "Swap encoder not found for protocol: {}", - grouped_swap.protocol_system - )) - })?; - - let mut grouped_protocol_data: Vec = vec![]; - for swap in grouped_swap.swaps.iter() { - let encoding_context = EncodingContext { - receiver: receiver.clone(), - exact_out: solution.exact_out, - router_address: None, - group_token_in: grouped_swap.input_token.clone(), - group_token_out: grouped_swap.output_token.clone(), - }; - let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; - grouped_protocol_data.extend(protocol_data); - } - - let executor_address = Bytes::from_str(swap_encoder.executor_address()) - .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?; - - Ok((grouped_protocol_data, executor_address)) - } - - fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box> { - self.swap_encoder_registry - .get_encoder(protocol_system) - } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } -} - #[cfg(test)] mod tests { use std::{collections::HashMap, str::FromStr}; @@ -699,198 +626,6 @@ mod tests { .unwrap() } - #[test] - fn test_executor_strategy_encode() { - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = ExecutorStrategyEncoder::new(swap_encoder_registry); - - let token_in = weth(); - let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); - - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: token_in, - given_amount: BigUint::from(1000000000000000000u64), - expected_amount: Some(BigUint::from(1000000000000000000u64)), - checked_token: token_out, - checked_amount: None, - sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), - // The receiver was generated with `makeAddr("bob") using forge` - receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), - swaps: vec![swap], - slippage: None, - native_action: None, - }; - - let (protocol_data, executor_address) = encoder - .encode_strategy(solution) - .unwrap(); - let hex_protocol_data = encode(&protocol_data); - assert_eq!( - executor_address, - Bytes::from_str("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").unwrap() - ); - assert_eq!( - hex_protocol_data, - String::from(concat!( - // in token - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - // component id - "a478c2975ab1ea89e8196811f51a7b7ade33eb11", - // receiver - "1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", - // zero for one - "00", - )) - ); - } - - #[test] - fn test_executor_strategy_encode_too_many_swaps() { - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = ExecutorStrategyEncoder::new(swap_encoder_registry); - - let token_in = weth(); - let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); - - let swap = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: token_in, - given_amount: BigUint::from(1000000000000000000u64), - expected_amount: Some(BigUint::from(1000000000000000000u64)), - checked_token: token_out, - checked_amount: None, - sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), - receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), - swaps: vec![swap.clone(), swap], - slippage: None, - native_action: None, - }; - - let result = encoder.encode_strategy(solution); - assert!(result.is_err()); - } - - #[test] - fn test_executor_strategy_encode_grouped_swaps() { - let swap_encoder_registry = get_swap_encoder_registry(); - let encoder = ExecutorStrategyEncoder::new(swap_encoder_registry); - - let eth = eth(); - let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - let pepe = Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap(); - - // Fee and tick spacing information for this test is obtained by querying the - // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e - // Using the poolKeys function with the first 25 bytes of the pool id - let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); - let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); - let mut static_attributes_usdc_eth: HashMap = HashMap::new(); - static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); - static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); - - let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); - let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); - let mut static_attributes_eth_pepe: HashMap = HashMap::new(); - static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); - static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); - - let swap_usdc_eth = Swap { - component: ProtocolComponent { - id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" - .to_string(), - protocol_system: "uniswap_v4".to_string(), - static_attributes: static_attributes_usdc_eth, - ..Default::default() - }, - token_in: usdc.clone(), - token_out: eth.clone(), - split: 0f64, - }; - - let swap_eth_pepe = Swap { - component: ProtocolComponent { - id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" - .to_string(), - protocol_system: "uniswap_v4".to_string(), - static_attributes: static_attributes_eth_pepe, - ..Default::default() - }, - token_in: eth.clone(), - token_out: pepe.clone(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: usdc, - given_amount: BigUint::from_str("1000_000000").unwrap(), - checked_token: pepe, - expected_amount: Some(BigUint::from_str("105_152_000000000000000000").unwrap()), - checked_amount: None, - slippage: None, - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - swaps: vec![swap_usdc_eth, swap_eth_pepe], - ..Default::default() - }; - - let (protocol_data, executor_address) = encoder - .encode_strategy(solution) - .unwrap(); - let hex_protocol_data = encode(&protocol_data); - assert_eq!( - executor_address, - Bytes::from_str("0xf62849f9a0b5bf2913b396098f7c7019b51a820a").unwrap() - ); - assert_eq!( - hex_protocol_data, - String::from(concat!( - // group in token - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // group out token - "6982508145454ce325ddbe47a25d4ec3d2311933", - // zero for one - "00", - // executor address - "f62849f9a0b5bf2913b396098f7c7019b51a820a", - // first pool intermediary token (ETH) - "0000000000000000000000000000000000000000", - // fee - "000bb8", - // tick spacing - "00003c", - // second pool intermediary token (PEPE) - "6982508145454ce325ddbe47a25d4ec3d2311933", - // fee - "0061a8", - // tick spacing - "0001f4" - )) - ); - } - #[rstest] #[case::with_check_no_slippage( None, diff --git a/src/encoding/evm/tycho_encoder.rs b/src/encoding/evm/tycho_encoder.rs deleted file mode 100644 index 85f541f..0000000 --- a/src/encoding/evm/tycho_encoder.rs +++ /dev/null @@ -1,720 +0,0 @@ -use std::collections::HashSet; - -use num_bigint::BigUint; -use tycho_common::Bytes; - -use crate::encoding::{ - errors::EncodingError, - models::{Chain, NativeAction, Solution, Transaction}, - strategy_encoder::StrategyEncoder, - tycho_encoder::TychoEncoder, -}; - -/// Represents an encoder for a swap using any strategy supported by the strategy registry. -/// -/// # Fields -/// * `strategy_encoder`: Strategy encoder to follow for encoding the solution -/// * `native_address`: Address of the chain's native token -/// * `wrapped_address`: Address of the chain's wrapped native token -pub struct EVMTychoEncoder { - strategy_encoder: Box, - native_address: Bytes, - wrapped_address: Bytes, -} - -impl Clone for EVMTychoEncoder { - fn clone(&self) -> Self { - Self { - strategy_encoder: self.strategy_encoder.clone_box(), - native_address: self.native_address.clone(), - wrapped_address: self.wrapped_address.clone(), - } - } -} - -impl EVMTychoEncoder { - pub fn new( - chain: tycho_common::models::Chain, - strategy_encoder: Box, - ) -> Result { - let chain: Chain = Chain::from(chain); - let native_address = chain.native_token()?; - let wrapped_address = chain.wrapped_token()?; - Ok(EVMTychoEncoder { strategy_encoder, native_address, wrapped_address }) - } -} - -impl EVMTychoEncoder { - /// Raises an `EncodingError` if the solution is not considered valid. - /// - /// A solution is considered valid if all the following conditions are met: - /// * The solution is not exact out. - /// * The solution has at least one swap. - /// * If the solution is wrapping, the given token is the chain's native token and the first - /// swap's input is the chain's wrapped token. - /// * If the solution is unwrapping, the checked token is the chain's native token and the last - /// swap's output is the chain's wrapped token. - /// * The token cannot appear more than once in the solution unless it is the first and last - /// token (i.e. a true cyclical swap). - fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> { - if solution.exact_out { - return Err(EncodingError::FatalError( - "Currently only exact input solutions are supported".to_string(), - )); - } - if solution.swaps.is_empty() { - return Err(EncodingError::FatalError("No swaps found in solution".to_string())); - } - if let Some(native_action) = solution.clone().native_action { - if native_action == NativeAction::Wrap { - if solution.given_token != self.native_address { - return Err(EncodingError::FatalError( - "Native token must be the input token in order to wrap".to_string(), - )); - } - if let Some(first_swap) = solution.swaps.first() { - if first_swap.token_in != self.wrapped_address { - return Err(EncodingError::FatalError( - "Wrapped token must be the first swap's input in order to wrap" - .to_string(), - )); - } - } - } else if native_action == NativeAction::Unwrap { - if solution.checked_token != self.native_address { - return Err(EncodingError::FatalError( - "Native token must be the output token in order to unwrap".to_string(), - )); - } - if let Some(last_swap) = solution.swaps.last() { - if last_swap.token_out != self.wrapped_address { - return Err(EncodingError::FatalError( - "Wrapped token must be the last swap's output in order to unwrap" - .to_string(), - )); - } - } - } - } - - let mut solution_tokens = vec![]; - let mut split_tokens_already_considered = HashSet::new(); - for (i, swap) in solution.swaps.iter().enumerate() { - // so we don't count the split tokens more than once - if swap.split != 0.0 { - if !split_tokens_already_considered.contains(&swap.token_in) { - solution_tokens.push(swap.token_in.clone()); - split_tokens_already_considered.insert(swap.token_in.clone()); - } - } else { - // it might be the last swap of the split or a regular swap - if !split_tokens_already_considered.contains(&swap.token_in) { - solution_tokens.push(swap.token_in.clone()); - } - } - if i == solution.swaps.len() - 1 { - solution_tokens.push(swap.token_out.clone()); - } - } - - if solution_tokens.len() != - solution_tokens - .iter() - .cloned() - .collect::>() - .len() - { - if let Some(last_swap) = solution.swaps.last() { - if solution.swaps[0].token_in != last_swap.token_out { - return Err(EncodingError::FatalError( - "Cyclical swaps are only allowed if they are the first and last token of a solution".to_string(), - )); - } else { - // it is a valid cyclical swap - // we don't support any wrapping or unwrapping in this case - if let Some(_native_action) = solution.clone().native_action { - return Err(EncodingError::FatalError( - "Wrapping/Unwrapping is not available in cyclical swaps".to_string(), - )); - } - } - } - } - Ok(()) - } -} - -impl TychoEncoder for EVMTychoEncoder { - fn encode_router_calldata( - &self, - solutions: Vec, - ) -> Result, EncodingError> { - let mut transactions: Vec = Vec::new(); - for solution in solutions.iter() { - self.validate_solution(solution)?; - - let (contract_interaction, target_address) = self - .strategy_encoder - .encode_strategy(solution.clone())?; - - let value = if solution.given_token == self.native_address { - solution.given_amount.clone() - } else { - BigUint::ZERO - }; - - transactions.push(Transaction { - value, - data: contract_interaction, - to: target_address, - }); - } - Ok(transactions) - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use tycho_common::models::{protocol::ProtocolComponent, Chain as TychoCoreChain}; - - use super::*; - use crate::encoding::{ - models::Swap, strategy_encoder::StrategyEncoder, swap_encoder::SwapEncoder, - }; - - fn dai() -> Bytes { - Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap() - } - - fn eth() -> Bytes { - Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap() - } - - fn weth() -> Bytes { - Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap() - } - - fn usdc() -> Bytes { - Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap() - } - - fn wbtc() -> Bytes { - Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap() - } - - #[derive(Clone)] - struct MockStrategy; - - impl StrategyEncoder for MockStrategy { - fn encode_strategy(&self, _solution: Solution) -> Result<(Vec, Bytes), EncodingError> { - Ok(( - Bytes::from_str("0x1234") - .unwrap() - .to_vec(), - Bytes::from_str("0xabcd").unwrap(), - )) - } - - fn get_swap_encoder(&self, _protocol_system: &str) -> Option<&Box> { - None - } - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } - } - - fn get_mocked_tycho_encoder() -> EVMTychoEncoder { - let strategy_encoder = Box::new(MockStrategy {}); - EVMTychoEncoder::new(TychoCoreChain::Ethereum, strategy_encoder).unwrap() - } - - #[test] - fn test_encode_router_calldata() { - let encoder = get_mocked_tycho_encoder(); - let eth_amount_in = BigUint::from(1000u32); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_amount: eth_amount_in.clone(), - given_token: eth(), - swaps: vec![swap], - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let transactions = encoder.encode_router_calldata(vec![solution]); - - assert!(transactions.is_ok()); - let transactions = transactions.unwrap(); - assert_eq!(transactions.len(), 1); - assert_eq!(transactions[0].value, eth_amount_in); - assert_eq!(transactions[0].data, Bytes::from_str("0x1234").unwrap()); - assert_eq!(transactions[0].to, Bytes::from_str("0xabcd").unwrap()); - } - - #[test] - fn test_validate_fails_for_exact_out() { - let encoder = get_mocked_tycho_encoder(); - let solution = Solution { - exact_out: true, // This should cause an error - ..Default::default() - }; - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Currently only exact input solutions are supported".to_string() - ) - ); - } - - #[test] - fn test_validate_passes_for_wrap() { - let encoder = get_mocked_tycho_encoder(); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: eth(), - checked_token: dai(), - checked_amount: None, - swaps: vec![swap], - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_ok()); - } - - #[test] - fn test_validate_fails_for_wrap_wrong_input() { - let encoder = get_mocked_tycho_encoder(); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: weth(), - swaps: vec![swap], - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Native token must be the input token in order to wrap".to_string() - ) - ); - } - - #[test] - fn test_validate_fails_for_wrap_wrong_first_swap() { - let encoder = get_mocked_tycho_encoder(); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: eth(), - token_out: dai(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: eth(), - swaps: vec![swap], - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Wrapped token must be the first swap's input in order to wrap".to_string() - ) - ); - } - - #[test] - fn test_validate_fails_no_swaps() { - let encoder = get_mocked_tycho_encoder(); - let solution = Solution { - exact_out: false, - given_token: eth(), - swaps: vec![], - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError("No swaps found in solution".to_string()) - ); - } - - #[test] - fn test_validate_passes_for_unwrap() { - let encoder = get_mocked_tycho_encoder(); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - checked_token: eth(), - checked_amount: None, - swaps: vec![swap], - native_action: Some(NativeAction::Unwrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_ok()); - } - - #[test] - fn test_validate_fails_for_unwrap_wrong_output() { - let encoder = get_mocked_tycho_encoder(); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - given_token: dai(), - checked_token: weth(), - swaps: vec![swap], - native_action: Some(NativeAction::Unwrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Native token must be the output token in order to unwrap".to_string() - ) - ); - } - - #[test] - fn test_validate_fails_for_unwrap_wrong_last_swap() { - let encoder = get_mocked_tycho_encoder(); - let swap = Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: eth(), - split: 0f64, - }; - - let solution = Solution { - exact_out: false, - checked_token: eth(), - swaps: vec![swap], - native_action: Some(NativeAction::Unwrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Wrapped token must be the last swap's output in order to unwrap".to_string() - ) - ); - } - - #[test] - fn test_validate_cyclical_swap() { - // This validation passes because the cyclical swap is the first and last token - // 50% -> WETH - // DAI - -> DAI - // 50% -> WETH - // (some of the pool addresses in this test are fake) - let encoder = get_mocked_tycho_encoder(); - let swaps = vec![ - Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0.5f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai(), - split: 0f64, - }, - ]; - - let solution = Solution { - exact_out: false, - given_token: dai(), - checked_token: dai(), - swaps, - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_ok()); - } - - #[test] - fn test_validate_cyclical_swap_fail() { - // This test should fail because the cyclical swap is not the first and last token - // DAI -> WETH -> USDC -> DAI -> WBTC - // (some of the pool addresses in this test are fake) - let encoder = get_mocked_tycho_encoder(); - let swaps = vec![ - Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0f64, - }, - Swap { - component: ProtocolComponent { - id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: usdc(), - split: 0f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: usdc(), - token_out: dai(), - split: 0f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: wbtc(), - split: 0f64, - }, - ]; - - let solution = Solution { - exact_out: false, - given_token: dai(), - checked_token: wbtc(), - swaps, - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Cyclical swaps are only allowed if they are the first and last token of a solution".to_string() - ) - ); - } - #[test] - fn test_validate_cyclical_swap_split_output() { - // This validation passes because it is a valid cyclical swap - // -> WETH - // WETH -> DAI - // -> WETH - // (some of the pool addresses in this test are fake) - let encoder = get_mocked_tycho_encoder(); - let swaps = vec![ - Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai(), - split: 0f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0.5f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0f64, - }, - ]; - - let solution = Solution { - exact_out: false, - given_token: weth(), - checked_token: weth(), - swaps, - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_ok()); - } - - #[test] - fn test_validate_cyclical_swap_native_action_fail() { - // This validation fails because there is a native action with a valid cyclical swap - // ETH -> WETH -> DAI -> WETH - // (some of the pool addresses in this test are fake) - let encoder = get_mocked_tycho_encoder(); - let swaps = vec![ - Swap { - component: ProtocolComponent { - id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: weth(), - token_out: dai(), - split: 0f64, - }, - Swap { - component: ProtocolComponent { - id: "0x0000000000000000000000000000000000000000".to_string(), - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, - token_in: dai(), - token_out: weth(), - split: 0f64, - }, - ]; - - let solution = Solution { - exact_out: false, - given_token: eth(), - checked_token: weth(), - swaps, - native_action: Some(NativeAction::Wrap), - ..Default::default() - }; - - let result = encoder.validate_solution(&solution); - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - EncodingError::FatalError( - "Wrapping/Unwrapping is not available in cyclical swaps" - .to_string() - .to_string() - ) - ); - } -} diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs new file mode 100644 index 0000000..5b7ebb8 --- /dev/null +++ b/src/encoding/evm/tycho_encoders.rs @@ -0,0 +1,1039 @@ +use std::{collections::HashSet, str::FromStr}; + +use num_bigint::BigUint; +use tycho_common::Bytes; + +use crate::encoding::{ + errors::EncodingError, + evm::{group_swaps::group_swaps, swap_encoder::swap_encoder_registry::SwapEncoderRegistry}, + models::{Chain, EncodingContext, NativeAction, Solution, Transaction}, + strategy_encoder::StrategyEncoder, + tycho_encoder::TychoEncoder, +}; + +/// Encodes solutions to be used by the TychoRouter. +/// +/// # Fields +/// * `strategy_encoder`: Strategy encoder to follow for encoding the solution +/// * `native_address`: Address of the chain's native token +/// * `wrapped_address`: Address of the chain's wrapped native token +pub struct TychoRouterEncoder { + strategy_encoder: Box, + native_address: Bytes, + wrapped_address: Bytes, +} + +impl TychoRouterEncoder { + pub fn new( + chain: tycho_common::models::Chain, + strategy_encoder: Box, + ) -> Result { + let chain: Chain = Chain::from(chain); + let native_address = chain.native_token()?; + let wrapped_address = chain.wrapped_token()?; + Ok(TychoRouterEncoder { strategy_encoder, native_address, wrapped_address }) + } +} + +impl TychoEncoder for TychoRouterEncoder { + fn encode_calldata(&self, solutions: Vec) -> Result, EncodingError> { + let mut transactions: Vec = Vec::new(); + for solution in solutions.iter() { + self.validate_solution(solution)?; + + let (contract_interaction, target_address) = self + .strategy_encoder + .encode_strategy(solution.clone())?; + + let value = if solution.given_token == self.native_address { + solution.given_amount.clone() + } else { + BigUint::ZERO + }; + + transactions.push(Transaction { + value, + data: contract_interaction, + to: target_address, + }); + } + Ok(transactions) + } + + /// Raises an `EncodingError` if the solution is not considered valid. + /// + /// A solution is considered valid if all the following conditions are met: + /// * The solution is not exact out. + /// * The solution has at least one swap. + /// * If the solution is wrapping, the given token is the chain's native token and the first + /// swap's input is the chain's wrapped token. + /// * If the solution is unwrapping, the checked token is the chain's native token and the last + /// swap's output is the chain's wrapped token. + /// * The token cannot appear more than once in the solution unless it is the first and last + /// token (i.e. a true cyclical swap). + fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> { + if solution.exact_out { + return Err(EncodingError::FatalError( + "Currently only exact input solutions are supported".to_string(), + )); + } + if solution.swaps.is_empty() { + return Err(EncodingError::FatalError("No swaps found in solution".to_string())); + } + if let Some(native_action) = solution.clone().native_action { + if native_action == NativeAction::Wrap { + if solution.given_token != self.native_address { + return Err(EncodingError::FatalError( + "Native token must be the input token in order to wrap".to_string(), + )); + } + if let Some(first_swap) = solution.swaps.first() { + if first_swap.token_in != self.wrapped_address { + return Err(EncodingError::FatalError( + "Wrapped token must be the first swap's input in order to wrap" + .to_string(), + )); + } + } + } else if native_action == NativeAction::Unwrap { + if solution.checked_token != self.native_address { + return Err(EncodingError::FatalError( + "Native token must be the output token in order to unwrap".to_string(), + )); + } + if let Some(last_swap) = solution.swaps.last() { + if last_swap.token_out != self.wrapped_address { + return Err(EncodingError::FatalError( + "Wrapped token must be the last swap's output in order to unwrap" + .to_string(), + )); + } + } + } + } + + let mut solution_tokens = vec![]; + let mut split_tokens_already_considered = HashSet::new(); + for (i, swap) in solution.swaps.iter().enumerate() { + // so we don't count the split tokens more than once + if swap.split != 0.0 { + if !split_tokens_already_considered.contains(&swap.token_in) { + solution_tokens.push(swap.token_in.clone()); + split_tokens_already_considered.insert(swap.token_in.clone()); + } + } else { + // it might be the last swap of the split or a regular swap + if !split_tokens_already_considered.contains(&swap.token_in) { + solution_tokens.push(swap.token_in.clone()); + } + } + if i == solution.swaps.len() - 1 { + solution_tokens.push(swap.token_out.clone()); + } + } + + if solution_tokens.len() != + solution_tokens + .iter() + .cloned() + .collect::>() + .len() + { + if let Some(last_swap) = solution.swaps.last() { + if solution.swaps[0].token_in != last_swap.token_out { + return Err(EncodingError::FatalError( + "Cyclical swaps are only allowed if they are the first and last token of a solution".to_string(), + )); + } else { + // it is a valid cyclical swap + // we don't support any wrapping or unwrapping in this case + if let Some(_native_action) = solution.clone().native_action { + return Err(EncodingError::FatalError( + "Wrapping/Unwrapping is not available in cyclical swaps".to_string(), + )); + } + } + } + } + Ok(()) + } +} + +/// Represents an encoder for one swap to be executed directly against an Executor. This is useful +/// when you want to bypass the Tycho Router, use your own Router contract and just need the +/// calldata for a particular swap. # Fields +/// * `swap_encoder_registry`: Registry of swap encoders +/// * `native_address`: Address of the chain's native token +pub struct TychoExecutorEncoder { + swap_encoder_registry: SwapEncoderRegistry, + native_address: Bytes, +} + +impl TychoExecutorEncoder { + pub fn new( + chain: tycho_common::models::Chain, + swap_encoder_registry: SwapEncoderRegistry, + ) -> Result { + let chain: Chain = Chain::from(chain); + let native_address = chain.native_token()?; + Ok(TychoExecutorEncoder { swap_encoder_registry, native_address }) + } + + fn encode_executor_calldata( + &self, + solution: Solution, + ) -> Result<(Vec, Bytes), EncodingError> { + let grouped_swaps = group_swaps(solution.clone().swaps); + let number_of_groups = grouped_swaps.len(); + if number_of_groups > 1 { + return Err(EncodingError::InvalidInput(format!( + "Tycho executor encoder only supports one swap. Found {}", + number_of_groups + ))) + } + + let grouped_swap = grouped_swaps + .first() + .ok_or_else(|| EncodingError::FatalError("Swap grouping failed".to_string()))?; + + let receiver = solution.receiver; + + let swap_encoder = self + .swap_encoder_registry + .get_encoder(&grouped_swap.protocol_system) + .ok_or_else(|| { + EncodingError::InvalidInput(format!( + "Swap encoder not found for protocol: {}", + grouped_swap.protocol_system + )) + })?; + + let mut grouped_protocol_data: Vec = vec![]; + for swap in grouped_swap.swaps.iter() { + let encoding_context = EncodingContext { + receiver: receiver.clone(), + exact_out: solution.exact_out, + router_address: None, + group_token_in: grouped_swap.input_token.clone(), + group_token_out: grouped_swap.output_token.clone(), + }; + let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; + grouped_protocol_data.extend(protocol_data); + } + + let executor_address = Bytes::from_str(swap_encoder.executor_address()) + .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?; + + Ok((grouped_protocol_data, executor_address)) + } +} + +impl TychoEncoder for TychoExecutorEncoder { + fn encode_calldata(&self, solutions: Vec) -> Result, EncodingError> { + let mut transactions: Vec = Vec::new(); + let solution = solutions + .first() + .ok_or(EncodingError::FatalError("No solutions found".to_string()))?; + self.validate_solution(solution)?; + + let (contract_interaction, target_address) = + self.encode_executor_calldata(solution.clone())?; + + let value = if solution.given_token == self.native_address { + solution.given_amount.clone() + } else { + BigUint::ZERO + }; + + transactions.push(Transaction { value, data: contract_interaction, to: target_address }); + Ok(transactions) + } + + /// Raises an `EncodingError` if the solution is not considered valid. + /// + /// A solution is considered valid if all the following conditions are met: + /// * The solution is not exact out. + fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> { + if solution.exact_out { + return Err(EncodingError::FatalError( + "Currently only exact input solutions are supported".to_string(), + )); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use tycho_common::models::{protocol::ProtocolComponent, Chain as TychoCoreChain}; + + use super::*; + use crate::encoding::{ + models::Swap, strategy_encoder::StrategyEncoder, swap_encoder::SwapEncoder, + }; + + fn dai() -> Bytes { + Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap() + } + + fn eth() -> Bytes { + Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap() + } + + fn weth() -> Bytes { + Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap() + } + + fn usdc() -> Bytes { + Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap() + } + + fn wbtc() -> Bytes { + Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap() + } + + mod router_encoder { + use super::*; + + #[derive(Clone)] + struct MockStrategy; + + impl StrategyEncoder for MockStrategy { + fn encode_strategy( + &self, + _solution: Solution, + ) -> Result<(Vec, Bytes), EncodingError> { + Ok(( + Bytes::from_str("0x1234") + .unwrap() + .to_vec(), + Bytes::from_str("0xabcd").unwrap(), + )) + } + + fn get_swap_encoder(&self, _protocol_system: &str) -> Option<&Box> { + None + } + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + } + + fn get_mocked_tycho_router_encoder() -> TychoRouterEncoder { + let strategy_encoder = Box::new(MockStrategy {}); + TychoRouterEncoder::new(TychoCoreChain::Ethereum, strategy_encoder).unwrap() + } + #[test] + fn test_encode_router_calldata() { + let encoder = get_mocked_tycho_router_encoder(); + let eth_amount_in = BigUint::from(1000u32); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_amount: eth_amount_in.clone(), + given_token: eth(), + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let transactions = encoder.encode_calldata(vec![solution]); + + assert!(transactions.is_ok()); + let transactions = transactions.unwrap(); + assert_eq!(transactions.len(), 1); + assert_eq!(transactions[0].value, eth_amount_in); + assert_eq!(transactions[0].data, Bytes::from_str("0x1234").unwrap()); + assert_eq!(transactions[0].to, Bytes::from_str("0xabcd").unwrap()); + } + + #[test] + fn test_validate_fails_for_exact_out() { + let encoder = get_mocked_tycho_router_encoder(); + let solution = Solution { + exact_out: true, // This should cause an error + ..Default::default() + }; + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Currently only exact input solutions are supported".to_string() + ) + ); + } + + #[test] + fn test_validate_passes_for_wrap() { + let encoder = get_mocked_tycho_router_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: eth(), + checked_token: dai(), + checked_amount: None, + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_ok()); + } + + #[test] + fn test_validate_fails_for_wrap_wrong_input() { + let encoder = get_mocked_tycho_router_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: weth(), + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Native token must be the input token in order to wrap".to_string() + ) + ); + } + + #[test] + fn test_validate_fails_for_wrap_wrong_first_swap() { + let encoder = get_mocked_tycho_router_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: eth(), + token_out: dai(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: eth(), + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Wrapped token must be the first swap's input in order to wrap".to_string() + ) + ); + } + + #[test] + fn test_validate_fails_no_swaps() { + let encoder = get_mocked_tycho_router_encoder(); + let solution = Solution { + exact_out: false, + given_token: eth(), + swaps: vec![], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError("No swaps found in solution".to_string()) + ); + } + + #[test] + fn test_validate_passes_for_unwrap() { + let encoder = get_mocked_tycho_router_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + checked_token: eth(), + checked_amount: None, + swaps: vec![swap], + native_action: Some(NativeAction::Unwrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_ok()); + } + + #[test] + fn test_validate_fails_for_unwrap_wrong_output() { + let encoder = get_mocked_tycho_router_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: dai(), + checked_token: weth(), + swaps: vec![swap], + native_action: Some(NativeAction::Unwrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Native token must be the output token in order to unwrap".to_string() + ) + ); + } + + #[test] + fn test_validate_fails_for_unwrap_wrong_last_swap() { + let encoder = get_mocked_tycho_router_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: eth(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + checked_token: eth(), + swaps: vec![swap], + native_action: Some(NativeAction::Unwrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Wrapped token must be the last swap's output in order to unwrap".to_string() + ) + ); + } + + #[test] + fn test_validate_cyclical_swap() { + // This validation passes because the cyclical swap is the first and last token + // 50% -> WETH + // DAI - -> DAI + // 50% -> WETH + // (some of the pool addresses in this test are fake) + let encoder = get_mocked_tycho_router_encoder(); + let swaps = vec![ + Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0.5f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }, + ]; + + let solution = Solution { + exact_out: false, + given_token: dai(), + checked_token: dai(), + swaps, + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_ok()); + } + + #[test] + fn test_validate_cyclical_swap_fail() { + // This test should fail because the cyclical swap is not the first and last token + // DAI -> WETH -> USDC -> DAI -> WBTC + // (some of the pool addresses in this test are fake) + let encoder = get_mocked_tycho_router_encoder(); + let swaps = vec![ + Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0f64, + }, + Swap { + component: ProtocolComponent { + id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: usdc(), + split: 0f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: usdc(), + token_out: dai(), + split: 0f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: wbtc(), + split: 0f64, + }, + ]; + + let solution = Solution { + exact_out: false, + given_token: dai(), + checked_token: wbtc(), + swaps, + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Cyclical swaps are only allowed if they are the first and last token of a solution".to_string() + ) + ); + } + #[test] + fn test_validate_cyclical_swap_split_output() { + // This validation passes because it is a valid cyclical swap + // -> WETH + // WETH -> DAI + // -> WETH + // (some of the pool addresses in this test are fake) + let encoder = get_mocked_tycho_router_encoder(); + let swaps = vec![ + Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0.5f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0f64, + }, + ]; + + let solution = Solution { + exact_out: false, + given_token: weth(), + checked_token: weth(), + swaps, + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_ok()); + } + + #[test] + fn test_validate_cyclical_swap_native_action_fail() { + // This validation fails because there is a native action with a valid cyclical swap + // ETH -> WETH -> DAI -> WETH + // (some of the pool addresses in this test are fake) + let encoder = get_mocked_tycho_router_encoder(); + let swaps = vec![ + Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }, + Swap { + component: ProtocolComponent { + id: "0x0000000000000000000000000000000000000000".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: weth(), + split: 0f64, + }, + ]; + + let solution = Solution { + exact_out: false, + given_token: eth(), + checked_token: weth(), + swaps, + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "Wrapping/Unwrapping is not available in cyclical swaps" + .to_string() + .to_string() + ) + ); + } + } + + mod executor_encoder { + use std::{collections::HashMap, str::FromStr}; + + use alloy::hex::encode; + use num_bigint::{BigInt, BigUint}; + use tycho_common::{models::protocol::ProtocolComponent, Bytes}; + + use super::*; + use crate::encoding::{ + evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry, + models::{Solution, Swap}, + }; + + fn get_swap_encoder_registry() -> SwapEncoderRegistry { + SwapEncoderRegistry::new( + Some("config/test_executor_addresses.json".to_string()), + TychoCoreChain::Ethereum, + ) + .unwrap() + } + + #[test] + fn test_executor_encoder_encode() { + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = + TychoExecutorEncoder::new(TychoCoreChain::Ethereum, swap_encoder_registry).unwrap(); + + let token_in = weth(); + let token_out = dai(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: BigUint::from(1000000000000000000u64), + expected_amount: Some(BigUint::from(1000000000000000000u64)), + checked_token: token_out, + checked_amount: None, + sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), + // The receiver was generated with `makeAddr("bob") using forge` + receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), + swaps: vec![swap], + slippage: None, + native_action: None, + }; + + let transactions = encoder + .encode_calldata(vec![solution]) + .unwrap(); + let transaction = transactions + .first() + .expect("Expected at least one transaction"); + let hex_protocol_data = encode(&transaction.data); + assert_eq!( + transaction.to, + Bytes::from_str("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").unwrap() + ); + assert_eq!( + hex_protocol_data, + String::from(concat!( + // in token + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + // component id + "a478c2975ab1ea89e8196811f51a7b7ade33eb11", + // receiver + "1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", + // zero for one + "00", + )) + ); + } + + #[test] + fn test_executor_encoder_too_many_swaps() { + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = + TychoExecutorEncoder::new(TychoCoreChain::Ethereum, swap_encoder_registry).unwrap(); + + let token_in = weth(); + let token_out = dai(); + + let swap = Swap { + component: ProtocolComponent { + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: BigUint::from(1000000000000000000u64), + expected_amount: Some(BigUint::from(1000000000000000000u64)), + checked_token: token_out, + checked_amount: None, + sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(), + receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(), + swaps: vec![swap.clone(), swap], + slippage: None, + native_action: None, + }; + + let result = encoder.encode_calldata(vec![solution]); + assert!(result.is_err()); + } + + #[test] + fn test_executor_encoder_grouped_swaps() { + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = + TychoExecutorEncoder::new(TychoCoreChain::Ethereum, swap_encoder_registry).unwrap(); + + let eth = eth(); + let usdc = usdc(); + let pepe = Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap(); + + // Fee and tick spacing information for this test is obtained by querying the + // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e + // Using the poolKeys function with the first 25 bytes of the pool id + let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); + let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); + let mut static_attributes_usdc_eth: HashMap = HashMap::new(); + static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); + static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); + + let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); + let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); + let mut static_attributes_eth_pepe: HashMap = HashMap::new(); + static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); + static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); + + let swap_usdc_eth = Swap { + component: ProtocolComponent { + id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_usdc_eth, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: eth.clone(), + split: 0f64, + }; + + let swap_eth_pepe = Swap { + component: ProtocolComponent { + id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_eth_pepe, + ..Default::default() + }, + token_in: eth.clone(), + token_out: pepe.clone(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: usdc, + given_amount: BigUint::from_str("1000_000000").unwrap(), + checked_token: pepe, + expected_amount: Some(BigUint::from_str("105_152_000000000000000000").unwrap()), + checked_amount: None, + slippage: None, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap_usdc_eth, swap_eth_pepe], + ..Default::default() + }; + + let transactions = encoder + .encode_calldata(vec![solution]) + .unwrap(); + let transaction = transactions + .first() + .expect("Expected at least one transaction"); + let hex_protocol_data = encode(&transaction.data); + assert_eq!( + transaction.to, + Bytes::from_str("0xf62849f9a0b5bf2913b396098f7c7019b51a820a").unwrap() + ); + assert_eq!( + hex_protocol_data, + String::from(concat!( + // group in token + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // group out token + "6982508145454ce325ddbe47a25d4ec3d2311933", + // zero for one + "00", + // executor address + "f62849f9a0b5bf2913b396098f7c7019b51a820a", + // first pool intermediary token (ETH) + "0000000000000000000000000000000000000000", + // fee + "000bb8", + // tick spacing + "00003c", + // second pool intermediary token (PEPE) + "6982508145454ce325ddbe47a25d4ec3d2311933", + // fee + "0061a8", + // tick spacing + "0001f4" + )) + ); + } + } +} diff --git a/src/encoding/tycho_encoder.rs b/src/encoding/tycho_encoder.rs index 09a9d46..16e0bb0 100644 --- a/src/encoding/tycho_encoder.rs +++ b/src/encoding/tycho_encoder.rs @@ -14,8 +14,7 @@ pub trait TychoEncoder { /// /// # Returns /// * `Result, EncodingError>` - Vector of executable transactions - fn encode_router_calldata( - &self, - solutions: Vec, - ) -> Result, EncodingError>; + fn encode_calldata(&self, solutions: Vec) -> Result, EncodingError>; + + fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError>; } From 56d3eee6184b705bf1bfd091b23dfee12ab641bc Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Mon, 7 Apr 2025 19:59:21 -0400 Subject: [PATCH 23/26] fix: No more EVMStrategyEncoder bad merge --- src/encoding/evm/strategy_encoder/strategy_encoders.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 61a171a..519045b 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -242,8 +242,6 @@ impl SequentialSwapStrategyEncoder { } } -impl EVMStrategyEncoder for SequentialSwapStrategyEncoder {} - impl StrategyEncoder for SequentialSwapStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { self.sequential_swap_validator @@ -303,7 +301,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { swaps.push(swap_data); } - let encoded_swaps = self.ple_encode(swaps); + let encoded_swaps = ple_encode(swaps); let method_calldata = if let Some(permit2) = self.permit2.clone() { let (permit, signature) = permit2.get_permit( &self.router_address, From 6fd0ab54c1de5a3f753b118d83c77c4ba55f3e1b Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 8 Apr 2025 09:35:33 +0100 Subject: [PATCH 24/26] docs: Improve doc --- don't change below this line --- ENG-4306 Took 4 minutes --- src/encoding/evm/tycho_encoders.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs index 5b7ebb8..6176786 100644 --- a/src/encoding/evm/tycho_encoders.rs +++ b/src/encoding/evm/tycho_encoders.rs @@ -161,7 +161,9 @@ impl TychoEncoder for TychoRouterEncoder { /// Represents an encoder for one swap to be executed directly against an Executor. This is useful /// when you want to bypass the Tycho Router, use your own Router contract and just need the -/// calldata for a particular swap. # Fields +/// calldata for a particular swap. +/// +/// # Fields /// * `swap_encoder_registry`: Registry of swap encoders /// * `native_address`: Address of the chain's native token pub struct TychoExecutorEncoder { From f5e712e0ffa898080069583cd080decca3f7e6a5 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 8 Apr 2025 15:55:55 +0100 Subject: [PATCH 25/26] feat(tycho-router-encoder): Select strategy depending on the solution - The tycho router address default is set at the EncoderBuilder level (not inside the strategies) - Rename TychoCoreChain to TychoCommonChain - Only take TychoCommonChain as an argument at the outermost level: EncoderBuilder. Everywhere else we use the execution Chain right away --- don't change below this line --- ENG-4332 Took 1 hour 10 minutes --- src/encoding/evm/approvals/permit2.rs | 4 +- src/encoding/evm/encoder_builders.rs | 60 ++++---- .../evm/strategy_encoder/strategy_encoders.rs | 65 +++----- .../evm/swap_encoder/swap_encoder_registry.rs | 6 +- src/encoding/evm/tycho_encoders.rs | 144 ++++++++++-------- src/encoding/models.rs | 18 +-- 6 files changed, 150 insertions(+), 147 deletions(-) diff --git a/src/encoding/evm/approvals/permit2.rs b/src/encoding/evm/approvals/permit2.rs index 549197e..e41ff3f 100644 --- a/src/encoding/evm/approvals/permit2.rs +++ b/src/encoding/evm/approvals/permit2.rs @@ -175,7 +175,7 @@ mod tests { use alloy_primitives::Uint; use num_bigint::BigUint; - use tycho_common::models::Chain as TychoCoreChain; + use tycho_common::models::Chain as TychoCommonChain; use super::*; @@ -211,7 +211,7 @@ mod tests { } fn eth_chain() -> Chain { - TychoCoreChain::Ethereum.into() + TychoCommonChain::Ethereum.into() } #[test] diff --git a/src/encoding/evm/encoder_builders.rs b/src/encoding/evm/encoder_builders.rs index 13e3355..94929a7 100644 --- a/src/encoding/evm/encoder_builders.rs +++ b/src/encoding/evm/encoder_builders.rs @@ -1,13 +1,15 @@ -use tycho_common::{models::Chain, Bytes}; +use std::collections::HashMap; + +use tycho_common::{models::Chain as TychoCommonChain, Bytes}; use crate::encoding::{ errors::EncodingError, evm::{ - strategy_encoder::strategy_encoders::SplitSwapStrategyEncoder, + constants::DEFAULT_ROUTERS_JSON, swap_encoder::swap_encoder_registry::SwapEncoderRegistry, tycho_encoders::{TychoExecutorEncoder, TychoRouterEncoder}, }, - strategy_encoder::StrategyEncoder, + models::Chain, tycho_encoder::TychoEncoder, }; @@ -16,7 +18,6 @@ use crate::encoding::{ /// This struct allows setting a chain and strategy encoder before building the final encoder. pub struct TychoRouterEncoderBuilder { swapper_pk: Option, - strategy: Option>, chain: Option, executors_file_path: Option, router_address: Option, @@ -33,13 +34,12 @@ impl TychoRouterEncoderBuilder { TychoRouterEncoderBuilder { swapper_pk: None, chain: None, - strategy: None, executors_file_path: None, router_address: None, } } - pub fn chain(mut self, chain: Chain) -> Self { - self.chain = Some(chain); + pub fn chain(mut self, chain: TychoCommonChain) -> Self { + self.chain = Some(chain.into()); self } @@ -62,32 +62,36 @@ impl TychoRouterEncoderBuilder { self } - /// Sets the `strategy_encoder` manually. - /// - /// **Note**: This method should not be used in combination with `tycho_router` or - /// `direct_execution`. - pub fn strategy_encoder(mut self, strategy: Box) -> Self { - self.strategy = Some(strategy); - self - } - - /// Builds the `TychoRouterEncoder` instance using the configured chain and strategy. - /// Returns an error if either the chain or strategy has not been set. + /// Builds the `TychoRouterEncoder` instance using the configured chain. + /// Returns an error if either the chain has not been set. pub fn build(self) -> Result, EncodingError> { if let Some(chain) = self.chain { - let swap_encoder_registry = - SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; + let tycho_router_address; + if let Some(address) = self.router_address { + tycho_router_address = address; + } else { + let default_routers: HashMap = + serde_json::from_str(DEFAULT_ROUTERS_JSON)?; + tycho_router_address = default_routers + .get(&chain.name) + .ok_or(EncodingError::FatalError( + "No default router address found for chain".to_string(), + ))? + .to_owned(); + } - let strategy = Box::new(SplitSwapStrategyEncoder::new( + let swap_encoder_registry = + SwapEncoderRegistry::new(self.executors_file_path.clone(), chain.clone())?; + + Ok(Box::new(TychoRouterEncoder::new( chain, swap_encoder_registry, self.swapper_pk, - self.router_address.clone(), - )?); - Ok(Box::new(TychoRouterEncoder::new(chain, strategy)?)) + tycho_router_address, + )?)) } else { Err(EncodingError::FatalError( - "Please set the chain and strategy before building the encoder".to_string(), + "Please set the chain and router address before building the encoder".to_string(), )) } } @@ -109,8 +113,8 @@ impl TychoExecutorEncoderBuilder { pub fn new() -> Self { TychoExecutorEncoderBuilder { chain: None, executors_file_path: None } } - pub fn chain(mut self, chain: Chain) -> Self { - self.chain = Some(chain); + pub fn chain(mut self, chain: TychoCommonChain) -> Self { + self.chain = Some(chain.into()); self } @@ -126,7 +130,7 @@ impl TychoExecutorEncoderBuilder { pub fn build(self) -> Result, EncodingError> { if let Some(chain) = self.chain { let swap_encoder_registry = - SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?; + SwapEncoderRegistry::new(self.executors_file_path.clone(), chain.clone())?; Ok(Box::new(TychoExecutorEncoder::new(chain, swap_encoder_registry)?)) } else { Err(EncodingError::FatalError( diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 94d55a1..e31ddd4 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -1,7 +1,4 @@ -use std::{ - collections::{HashMap, HashSet}, - str::FromStr, -}; +use std::{collections::HashSet, str::FromStr}; use alloy_primitives::{aliases::U24, U256, U8}; use alloy_sol_types::SolValue; @@ -11,7 +8,6 @@ use crate::encoding::{ errors::EncodingError, evm::{ approvals::permit2::Permit2, - constants::DEFAULT_ROUTERS_JSON, group_swaps::group_swaps, strategy_encoder::strategy_validators::{ SequentialSwapValidator, SplitSwapValidator, SwapValidator, @@ -47,12 +43,11 @@ pub struct SingleSwapStrategyEncoder { impl SingleSwapStrategyEncoder { pub fn new( - blockchain: tycho_common::models::Chain, + chain: Chain, swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, router_address: Bytes, ) -> Result { - let chain = Chain::from(blockchain); let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { (Some(Permit2::new(swapper_pk, chain.clone())?), "singleSwapPermit2(uint256,address,address,uint256,bool,bool,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) } else { @@ -206,12 +201,11 @@ pub struct SequentialSwapStrategyEncoder { impl SequentialSwapStrategyEncoder { pub fn new( - blockchain: tycho_common::models::Chain, + chain: Chain, swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, router_address: Bytes, ) -> Result { - let chain = Chain::from(blockchain); let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { (Some(Permit2::new(swapper_pk, chain.clone())?), "sequentialSwapPermit2(uint256,address,address,uint256,bool,bool,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) } else { @@ -375,12 +369,11 @@ pub struct SplitSwapStrategyEncoder { impl SplitSwapStrategyEncoder { pub fn new( - blockchain: tycho_common::models::Chain, + chain: Chain, swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, - router_address: Option, + router_address: Bytes, ) -> Result { - let chain = Chain::from(blockchain); let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { (Some(Permit2::new(swapper_pk, chain.clone())?), "splitSwapPermit2(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) } else { @@ -391,20 +384,6 @@ impl SplitSwapStrategyEncoder { ) }; - let tycho_router_address; - if let Some(address) = router_address { - tycho_router_address = address; - } else { - let default_routers: HashMap = - serde_json::from_str(DEFAULT_ROUTERS_JSON)?; - tycho_router_address = default_routers - .get(&chain.name) - .ok_or(EncodingError::FatalError( - "No default router address found for chain".to_string(), - ))? - .to_owned(); - } - Ok(Self { permit2, selector, @@ -412,7 +391,7 @@ impl SplitSwapStrategyEncoder { native_address: chain.native_token()?, wrapped_address: chain.wrapped_token()?, split_swap_validator: SplitSwapValidator, - router_address: tycho_router_address, + router_address, }) } @@ -601,15 +580,15 @@ mod tests { use num_bigint::{BigInt, BigUint}; use rstest::rstest; use tycho_common::{ - models::{protocol::ProtocolComponent, Chain as TychoCoreChain}, + models::{protocol::ProtocolComponent, Chain as TychoCommonChain}, Bytes, }; use super::*; use crate::encoding::models::Swap; - fn eth_chain() -> TychoCoreChain { - TychoCoreChain::Ethereum + fn eth_chain() -> Chain { + TychoCommonChain::Ethereum.into() } fn eth() -> Bytes { @@ -675,7 +654,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), ) .unwrap(); let solution = Solution { @@ -882,7 +861,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), ) .unwrap(); let solution = Solution { @@ -934,7 +913,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key), - Some(Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap()), + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), ) .unwrap(); let solution = Solution { @@ -986,7 +965,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), ) .unwrap(); let solution = Solution { @@ -1079,7 +1058,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), ) .unwrap(); let solution = Solution { @@ -1291,7 +1270,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), ) .unwrap(); let solution = Solution { @@ -1411,7 +1390,7 @@ mod tests { eth_chain(), swap_encoder_registry, None, - Some(Bytes::from_str("0x1d1499e622D69689cdf9004d05Ec547d650Ff211").unwrap()), + Bytes::from_str("0x1d1499e622D69689cdf9004d05Ec547d650Ff211").unwrap(), ) .unwrap(); @@ -1544,7 +1523,7 @@ mod tests { eth_chain(), swap_encoder_registry, None, - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), ) .unwrap(); let solution = Solution { @@ -1637,7 +1616,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), ) .unwrap(); @@ -1704,7 +1683,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), ) .unwrap(); @@ -1791,7 +1770,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), ) .unwrap(); @@ -1943,7 +1922,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key.clone()), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), ) .unwrap(); @@ -2101,7 +2080,7 @@ mod tests { eth_chain(), swap_encoder_registry, Some(private_key.clone()), - Some(Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395")), + Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), ) .unwrap(); diff --git a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs index 8a5b9e4..fb22ffe 100644 --- a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs +++ b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs @@ -17,11 +17,7 @@ pub struct SwapEncoderRegistry { impl SwapEncoderRegistry { /// Populates the registry with the `SwapEncoders` for the given blockchain by parsing the /// executors' addresses in the file at the given path. - pub fn new( - executors_file_path: Option, - blockchain: tycho_common::models::Chain, - ) -> Result { - let chain = Chain::from(blockchain); + pub fn new(executors_file_path: Option, chain: Chain) -> Result { let config_str = if let Some(ref path) = executors_file_path { fs::read_to_string(path).map_err(|e| { EncodingError::FatalError(format!( diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs index 6176786..53ba904 100644 --- a/src/encoding/evm/tycho_encoders.rs +++ b/src/encoding/evm/tycho_encoders.rs @@ -5,7 +5,13 @@ use tycho_common::Bytes; use crate::encoding::{ errors::EncodingError, - evm::{group_swaps::group_swaps, swap_encoder::swap_encoder_registry::SwapEncoderRegistry}, + evm::{ + group_swaps::group_swaps, + strategy_encoder::strategy_encoders::{ + SequentialSwapStrategyEncoder, SingleSwapStrategyEncoder, SplitSwapStrategyEncoder, + }, + swap_encoder::swap_encoder_registry::SwapEncoderRegistry, + }, models::{Chain, EncodingContext, NativeAction, Solution, Transaction}, strategy_encoder::StrategyEncoder, tycho_encoder::TychoEncoder, @@ -14,24 +20,50 @@ use crate::encoding::{ /// Encodes solutions to be used by the TychoRouter. /// /// # Fields -/// * `strategy_encoder`: Strategy encoder to follow for encoding the solution +/// * `single_swap_strategy`: Encoder for single swaps +/// * `sequential_swap_strategy`: Encoder for sequential swaps +/// * `split_swap_strategy`: Encoder for split swaps /// * `native_address`: Address of the chain's native token /// * `wrapped_address`: Address of the chain's wrapped native token pub struct TychoRouterEncoder { - strategy_encoder: Box, + single_swap_strategy: SingleSwapStrategyEncoder, + sequential_swap_strategy: SequentialSwapStrategyEncoder, + split_swap_strategy: SplitSwapStrategyEncoder, native_address: Bytes, wrapped_address: Bytes, } impl TychoRouterEncoder { pub fn new( - chain: tycho_common::models::Chain, - strategy_encoder: Box, + chain: Chain, + swap_encoder_registry: SwapEncoderRegistry, + swapper_pk: Option, + router_address: Bytes, ) -> Result { - let chain: Chain = Chain::from(chain); let native_address = chain.native_token()?; let wrapped_address = chain.wrapped_token()?; - Ok(TychoRouterEncoder { strategy_encoder, native_address, wrapped_address }) + Ok(TychoRouterEncoder { + single_swap_strategy: SingleSwapStrategyEncoder::new( + chain.clone(), + swap_encoder_registry.clone(), + swapper_pk.clone(), + router_address.clone(), + )?, + sequential_swap_strategy: SequentialSwapStrategyEncoder::new( + chain.clone(), + swap_encoder_registry.clone(), + swapper_pk.clone(), + router_address.clone(), + )?, + split_swap_strategy: SplitSwapStrategyEncoder::new( + chain, + swap_encoder_registry, + None, + router_address.clone(), + )?, + native_address, + wrapped_address, + }) } } @@ -40,10 +72,20 @@ impl TychoEncoder for TychoRouterEncoder { let mut transactions: Vec = Vec::new(); for solution in solutions.iter() { self.validate_solution(solution)?; - - let (contract_interaction, target_address) = self - .strategy_encoder - .encode_strategy(solution.clone())?; + let (contract_interaction, target_address) = if solution.swaps.len() == 1 { + self.single_swap_strategy + .encode_strategy(solution.clone())? + } else if solution + .swaps + .iter() + .all(|swap| swap.split == 0.0) + { + self.sequential_swap_strategy + .encode_strategy(solution.clone())? + } else { + self.split_swap_strategy + .encode_strategy(solution.clone())? + }; let value = if solution.given_token == self.native_address { solution.given_amount.clone() @@ -173,10 +215,9 @@ pub struct TychoExecutorEncoder { impl TychoExecutorEncoder { pub fn new( - chain: tycho_common::models::Chain, + chain: Chain, swap_encoder_registry: SwapEncoderRegistry, ) -> Result { - let chain: Chain = Chain::from(chain); let native_address = chain.native_token()?; Ok(TychoExecutorEncoder { swap_encoder_registry, native_address }) } @@ -269,12 +310,10 @@ impl TychoEncoder for TychoExecutorEncoder { mod tests { use std::str::FromStr; - use tycho_common::models::{protocol::ProtocolComponent, Chain as TychoCoreChain}; + use tycho_common::models::{protocol::ProtocolComponent, Chain as TychoCommonChain}; use super::*; - use crate::encoding::{ - models::Swap, strategy_encoder::StrategyEncoder, swap_encoder::SwapEncoder, - }; + use crate::encoding::models::Swap; fn dai() -> Bytes { Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap() @@ -296,36 +335,25 @@ mod tests { Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap() } + fn get_swap_encoder_registry() -> SwapEncoderRegistry { + SwapEncoderRegistry::new( + Some("config/test_executor_addresses.json".to_string()), + TychoCommonChain::Ethereum.into(), + ) + .unwrap() + } + mod router_encoder { use super::*; - #[derive(Clone)] - struct MockStrategy; - - impl StrategyEncoder for MockStrategy { - fn encode_strategy( - &self, - _solution: Solution, - ) -> Result<(Vec, Bytes), EncodingError> { - Ok(( - Bytes::from_str("0x1234") - .unwrap() - .to_vec(), - Bytes::from_str("0xabcd").unwrap(), - )) - } - - fn get_swap_encoder(&self, _protocol_system: &str) -> Option<&Box> { - None - } - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } - } - fn get_mocked_tycho_router_encoder() -> TychoRouterEncoder { - let strategy_encoder = Box::new(MockStrategy {}); - TychoRouterEncoder::new(TychoCoreChain::Ethereum, strategy_encoder).unwrap() + TychoRouterEncoder::new( + TychoCommonChain::Ethereum.into(), + get_swap_encoder_registry(), + None, + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + ) + .unwrap() } #[test] fn test_encode_router_calldata() { @@ -346,7 +374,9 @@ mod tests { exact_out: false, given_amount: eth_amount_in.clone(), given_token: eth(), + checked_token: dai(), swaps: vec![swap], + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), native_action: Some(NativeAction::Wrap), ..Default::default() }; @@ -357,8 +387,10 @@ mod tests { let transactions = transactions.unwrap(); assert_eq!(transactions.len(), 1); assert_eq!(transactions[0].value, eth_amount_in); - assert_eq!(transactions[0].data, Bytes::from_str("0x1234").unwrap()); - assert_eq!(transactions[0].to, Bytes::from_str("0xabcd").unwrap()); + assert_eq!( + transactions[0].to, + Bytes::from_str("0x3ede3eca2a72b3aecc820e955b36f38437d01395").unwrap() + ); } #[test] @@ -824,24 +856,14 @@ mod tests { use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use super::*; - use crate::encoding::{ - evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry, - models::{Solution, Swap}, - }; - - fn get_swap_encoder_registry() -> SwapEncoderRegistry { - SwapEncoderRegistry::new( - Some("config/test_executor_addresses.json".to_string()), - TychoCoreChain::Ethereum, - ) - .unwrap() - } + use crate::encoding::models::{Solution, Swap}; #[test] fn test_executor_encoder_encode() { let swap_encoder_registry = get_swap_encoder_registry(); let encoder = - TychoExecutorEncoder::new(TychoCoreChain::Ethereum, swap_encoder_registry).unwrap(); + TychoExecutorEncoder::new(TychoCommonChain::Ethereum.into(), swap_encoder_registry) + .unwrap(); let token_in = weth(); let token_out = dai(); @@ -902,7 +924,8 @@ mod tests { fn test_executor_encoder_too_many_swaps() { let swap_encoder_registry = get_swap_encoder_registry(); let encoder = - TychoExecutorEncoder::new(TychoCoreChain::Ethereum, swap_encoder_registry).unwrap(); + TychoExecutorEncoder::new(TychoCommonChain::Ethereum.into(), swap_encoder_registry) + .unwrap(); let token_in = weth(); let token_out = dai(); @@ -939,7 +962,8 @@ mod tests { fn test_executor_encoder_grouped_swaps() { let swap_encoder_registry = get_swap_encoder_registry(); let encoder = - TychoExecutorEncoder::new(TychoCoreChain::Ethereum, swap_encoder_registry).unwrap(); + TychoExecutorEncoder::new(TychoCommonChain::Ethereum.into(), swap_encoder_registry) + .unwrap(); let eth = eth(); let usdc = usdc(); diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 43af861..9da64dd 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -2,7 +2,7 @@ use hex; use num_bigint::BigUint; use serde::{Deserialize, Serialize}; use tycho_common::{ - models::{protocol::ProtocolComponent, Chain as TychoCoreChain}, + models::{protocol::ProtocolComponent, Chain as TychoCommonChain}, Bytes, }; @@ -121,15 +121,15 @@ pub struct Chain { pub name: String, } -impl From for Chain { - fn from(chain: TychoCoreChain) -> Self { +impl From for Chain { + fn from(chain: TychoCommonChain) -> Self { match chain { - TychoCoreChain::Ethereum => Chain { id: 1, name: chain.to_string() }, - TychoCoreChain::ZkSync => Chain { id: 324, name: chain.to_string() }, - TychoCoreChain::Arbitrum => Chain { id: 42161, name: chain.to_string() }, - TychoCoreChain::Starknet => Chain { id: 0, name: chain.to_string() }, - TychoCoreChain::Base => Chain { id: 8453, name: chain.to_string() }, - TychoCoreChain::Unichain => Chain { id: 130, name: chain.to_string() }, + TychoCommonChain::Ethereum => Chain { id: 1, name: chain.to_string() }, + TychoCommonChain::ZkSync => Chain { id: 324, name: chain.to_string() }, + TychoCommonChain::Arbitrum => Chain { id: 42161, name: chain.to_string() }, + TychoCommonChain::Starknet => Chain { id: 0, name: chain.to_string() }, + TychoCommonChain::Base => Chain { id: 8453, name: chain.to_string() }, + TychoCommonChain::Unichain => Chain { id: 130, name: chain.to_string() }, } } } From fff8ef0d87d7c1071035cb62e76894cfb18305a7 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Thu, 10 Apr 2025 10:15:24 +0100 Subject: [PATCH 26/26] fix: Fix after merge with main Mostly had to delete some tests (because they were moved) and fix others Took 17 minutes Took 8 seconds Took 17 seconds --- foundry/test/TychoRouter.t.sol | 29 ---------------- foundry/test/TychoRouterIntegration.t.sol | 33 +++++++++++++++++-- .../evm/strategy_encoder/strategy_encoders.rs | 4 +-- src/encoding/evm/utils.rs | 26 +++++++-------- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 8b0202a..4a79e5c 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -212,33 +212,4 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.expectRevert(TychoRouter__EmptySwaps.selector); tychoRouter.exposedSplitSwap(amountIn, 2, swaps); } - - function testCurveIntegration() public { - deal(UWU_ADDR, ALICE, 1 ether); - - vm.startPrank(ALICE); - IERC20(UWU_ADDR).approve(tychoRouterAddr, type(uint256).max); - // Encoded solution generated using `test_split_encoding_strategy_curve` - (bool success,) = tychoRouterAddr.call( - hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000055c08ca52497e2f1534b59e2917bf524d4765257000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000005b005900010000001d1499e622d69689cdf9004d05ec547d650ff21155c08ca52497e2f1534b59e2917bf524d4765257c02aaa39b223fe8d0a0e5c4f27ead9083c756cc277146b0a1d08b6844376df6d9da99ba7f1b19e71020100010000000000" - ); - - assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 4691958787921); - - vm.stopPrank(); - } - - function testCurveIntegrationStETH() public { - deal(ALICE, 1 ether); - - vm.startPrank(ALICE); - // Encoded solution generated using `test_split_encoding_strategy_curve_st_eth` - (bool success,) = tychoRouterAddr.call{value: 1 ether}( - hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ae7ab96520de3a18e5e111b5eaab095312d7fe840000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000005b005900010000001d1499e622d69689cdf9004d05ec547d650ff211eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeae7ab96520de3a18e5e111b5eaab095312d7fe84dc24316b9ae028f1497c275eb9192a3ea0f67022010001000000000000" - ); - - assertEq(IERC20(STETH_ADDR).balanceOf(ALICE), 1000754689941529590); - - vm.stopPrank(); - } } diff --git a/foundry/test/TychoRouterIntegration.t.sol b/foundry/test/TychoRouterIntegration.t.sol index 85ade39..27ac0e9 100644 --- a/foundry/test/TychoRouterIntegration.t.sol +++ b/foundry/test/TychoRouterIntegration.t.sol @@ -177,7 +177,7 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { // This allows us to change the code at that address to be the testing executor code vm.etch( 0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D, - 0x2a07706473244BC757E10F2a9E86fB532828afe3.code + 0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7.code ); deal(ALICE, 1 ether); @@ -187,7 +187,7 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { vm.startPrank(ALICE); // Encoded solution generated using `test_split_encoding_strategy_ekubo` (bool success,) = address(tychoRouter).call{value: 1 ether}( - hex"79b9b93b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000077007500010000002a07706473244bc757e10f2a9e86fb532828afe31d1499e622d69689cdf9004d05ec547d650ff2110000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000000000000000000000" + hex"79b9b93b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000077007500010000003d7ebc40af7092e3f1c81f2e996cba5cae2090d7a4ad4f68d0b91cfd19687c881e50f3a00242828c0000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000000000000000000000" ); uint256 balancerAfter = IERC20(USDC_ADDR).balanceOf(ALICE); @@ -321,4 +321,33 @@ contract TychoRouterTestIntegration is TychoRouterTestSetup { vm.stopPrank(); } + + function testSplitCurveIntegration() public { + deal(UWU_ADDR, ALICE, 1 ether); + + vm.startPrank(ALICE); + IERC20(UWU_ADDR).approve(tychoRouterAddr, type(uint256).max); + // Encoded solution generated using `test_split_encoding_strategy_curve` + (bool success,) = tychoRouterAddr.call( + hex"79b9b93b0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000055c08ca52497e2f1534b59e2917bf524d4765257000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000005b005900010000001d1499e622d69689cdf9004d05ec547d650ff21155c08ca52497e2f1534b59e2917bf524d4765257c02aaa39b223fe8d0a0e5c4f27ead9083c756cc277146b0a1d08b6844376df6d9da99ba7f1b19e71020100010000000000" + ); + + assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 4691958787921); + + vm.stopPrank(); + } + + function testSplitCurveIntegrationStETH() public { + deal(ALICE, 1 ether); + + vm.startPrank(ALICE); + // Encoded solution generated using `test_split_encoding_strategy_curve_st_eth` + (bool success,) = tychoRouterAddr.call{value: 1 ether}( + hex"79b9b93b0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ae7ab96520de3a18e5e111b5eaab095312d7fe840000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000005b005900010000001d1499e622d69689cdf9004d05ec547d650ff211eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeae7ab96520de3a18e5e111b5eaab095312d7fe84dc24316b9ae028f1497c275eb9192a3ea0f67022010001000000000000" + ); + + assertEq(IERC20(STETH_ADDR).balanceOf(ALICE), 1000754689941529590); + + vm.stopPrank(); + } } diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 0c27841..a9ecf61 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -2194,7 +2194,7 @@ mod tests { eth_chain(), swap_encoder_registry, None, - Some(Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap()), + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), ) .unwrap(); @@ -2256,7 +2256,7 @@ mod tests { eth_chain(), swap_encoder_registry, None, - Some(Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap()), + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), ) .unwrap(); diff --git a/src/encoding/evm/utils.rs b/src/encoding/evm/utils.rs index f2a6ed1..dea3703 100644 --- a/src/encoding/evm/utils.rs +++ b/src/encoding/evm/utils.rs @@ -139,6 +139,19 @@ pub fn get_runtime() -> Result<(Handle, Option>), EncodingError> { } } +/// Gets the client used for interacting with the EVM-compatible network. +pub async fn get_client() -> Result>, EncodingError> { + dotenv::dotenv().ok(); + let eth_rpc_url = env::var("RPC_URL") + .map_err(|_| EncodingError::FatalError("Missing RPC_URL in environment".to_string()))?; + let client = ProviderBuilder::new() + .on_builtin(ð_rpc_url) + .await + .map_err(|_| EncodingError::FatalError("Failed to build provider".to_string()))?; + Ok(Arc::new(client)) +} + + /// Uses prefix-length encoding to efficient encode action data. /// /// Prefix-length encoding is a data encoding method where the beginning of a data segment @@ -154,19 +167,6 @@ pub fn ple_encode(action_data_array: Vec>) -> Vec { encoded_action_data } - -/// Gets the client used for interacting with the EVM-compatible network. -pub async fn get_client() -> Result>, EncodingError> { - dotenv::dotenv().ok(); - let eth_rpc_url = env::var("RPC_URL") - .map_err(|_| EncodingError::FatalError("Missing RPC_URL in environment".to_string()))?; - let client = ProviderBuilder::new() - .on_builtin(ð_rpc_url) - .await - .map_err(|_| EncodingError::FatalError("Failed to build provider".to_string()))?; - Ok(Arc::new(client)) -} - #[cfg(test)] mod tests { use num_bigint::BigUint;