From 3ae9d3ad670e760e2f7d0cbe81e5b19b7e7a3dae Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Mon, 17 Mar 2025 23:50:39 -0400 Subject: [PATCH 1/3] 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 1dad4afb6b91cf86ea000afeaeba8882af73b713 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 18 Mar 2025 01:37:14 -0400 Subject: [PATCH 2/3] 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 c4455dcabb499ce16979058b59878f2ab0a4a344 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 18 Mar 2025 21:13:27 -0400 Subject: [PATCH 3/3] 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); + } +}