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); + } +}