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,