// 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 { 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, tychoRouterAddr, amountIn); bytes memory protocolData = encodeUniswapV2Swap( WETH_ADDR, WETH_DAI_POOL, ALICE, false, RestrictTransferFrom.TransferType.TransferFrom ); bytes memory swap = encodeSingleSwap(address(usv2Executor), protocolData); tychoRouter.singleSwapPermit2( amountIn, WETH_ADDR, DAI_ADDR, 2008817438608734439722, false, false, ALICE, permitSingle, signature, swap ); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); assertEq(daiBalance, 2018817438608734439722); 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, ALICE, false, RestrictTransferFrom.TransferType.TransferFrom ); bytes memory swap = encodeSingleSwap(address(usv2Executor), protocolData); uint256 minAmountOut = 2000 * 1e18; uint256 amountOut = tychoRouter.singleSwap( amountIn, WETH_ADDR, DAI_ADDR, minAmountOut, false, false, ALICE, true, swap ); uint256 expectedAmount = 2018817438608734439722; 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, ALICE, false, RestrictTransferFrom.TransferType.None ); bytes memory swap = encodeSingleSwap(address(usv2Executor), protocolData); vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector); tychoRouter.singleSwap( amountIn, WETH_ADDR, DAI_ADDR, 0, false, false, ALICE, true, 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, ALICE, false, RestrictTransferFrom.TransferType.TransferFrom ); bytes memory swap = encodeSingleSwap(address(usv2Executor), protocolData); uint256 minAmountOut = 2600 * 1e18; vm.expectRevert(); tychoRouter.singleSwap( amountIn, WETH_ADDR, DAI_ADDR, minAmountOut, false, false, ALICE, true, 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, ALICE, false, RestrictTransferFrom.TransferType.TransferFrom ); bytes memory swap = encodeSingleSwap(address(usv2Executor), protocolData); uint256 minAmountOut = 5600 * 1e18; vm.expectRevert( abi.encodeWithSelector( TychoRouter__NegativeSlippage.selector, 2018817438608734439722, // actual amountOut minAmountOut ) ); tychoRouter.singleSwap( amountIn, WETH_ADDR, DAI_ADDR, minAmountOut, false, false, ALICE, true, swap ); } 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, ALICE, false, RestrictTransferFrom.TransferType.Transfer // ETH has already been transferred to router ); 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 = 2018817438608734439722; 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, tychoRouterAddr, amountIn); bytes memory protocolData = encodeUniswapV2Swap( DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true, RestrictTransferFrom.TransferType.TransferFrom ); 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 = 1475644707225677606; assertEq(amountOut, expectedAmount); assertEq(ALICE.balance, expectedAmount); vm.stopPrank(); } function testSingleSwapNoTransferNeededIllegalTransfer() public { // Tokens are already in the router, there is no need to transfer them. // Failure because there will be an attempt on an illegal transfer. uint256 amountIn = 1 ether; deal(WETH_ADDR, address(tychoRouter), 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, ALICE, false, RestrictTransferFrom.TransferType.TransferFrom ); bytes memory swap = encodeSingleSwap(address(usv2Executor), protocolData); vm.expectRevert( abi.encodeWithSelector( RestrictTransferFrom__ExceededTransferFromAllowance.selector, 0, // allowed amount amountIn // attempted amount ) ); uint256 amountOut = tychoRouter.singleSwap( amountIn, WETH_ADDR, DAI_ADDR, 2000 * 1e18, false, false, ALICE, false, swap ); vm.stopPrank(); } function testSingleSwapIntegration() public { // Tests swapping WETH -> DAI on a USV2 pool with regular approvals deal(WETH_ADDR, ALICE, 1 ether); uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE); vm.startPrank(ALICE); IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max); bytes memory callData = loadCallDataFromFile("test_single_swap_strategy_encoder_no_permit2"); (bool success,) = tychoRouterAddr.call(callData); vm.stopPrank(); uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE); assertTrue(success, "Call Failed"); assertEq(balanceAfter - balanceBefore, 2018817438608734439722); } function testSingleSwapIntegrationPermit2() public { // Tests swapping WETH -> DAI on a USV2 pool with permit2 deal(WETH_ADDR, ALICE, 1 ether); uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE); vm.startPrank(ALICE); IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); bytes memory callData = loadCallDataFromFile("test_single_swap_strategy_encoder"); (bool success,) = tychoRouterAddr.call(callData); vm.stopPrank(); uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE); assertTrue(success, "Call Failed"); assertEq(balanceAfter - balanceBefore, 2018817438608734439722); } function testSingleSwapWithWrapIntegration() public { // Tests swapping WETH -> DAI on a USV2 pool, but ETH is received from the user // and wrapped before the swap deal(ALICE, 1 ether); uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE); // Approve permit2 vm.startPrank(ALICE); bytes memory callData = loadCallDataFromFile("test_single_swap_strategy_encoder_wrap"); (bool success,) = tychoRouterAddr.call{value: 1 ether}(callData); vm.stopPrank(); uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE); assertTrue(success, "Call Failed"); assertEq(balanceAfter - balanceBefore, 2018817438608734439722); } function testSingleSwapWithUnwrapIntegration() public { // Tests swapping DAI -> WETH on a USV2 pool, and WETH is unwrapped to ETH // before sending back to the user deal(DAI_ADDR, ALICE, 3000 ether); uint256 balanceBefore = ALICE.balance; // Approve permit2 vm.startPrank(ALICE); IERC20(DAI_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); bytes memory callData = loadCallDataFromFile("test_single_swap_strategy_encoder_unwrap"); (bool success,) = tychoRouterAddr.call(callData); vm.stopPrank(); uint256 balanceAfter = ALICE.balance; assertTrue(success, "Call Failed"); assertEq(balanceAfter - balanceBefore, 1475644707225677606); } function testSingleSwapIntegrationNoTransferIn() public { // Tests swapping WETH -> DAI on a USV2 pool assuming that the tokens are already inside the router deal(WETH_ADDR, tychoRouterAddr, 1 ether); uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE); vm.startPrank(ALICE); bytes memory callData = loadCallDataFromFile( "test_single_swap_strategy_encoder_no_transfer_in" ); (bool success,) = tychoRouterAddr.call(callData); vm.stopPrank(); uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE); assertTrue(success, "Call Failed"); assertEq(balanceAfter - balanceBefore, 2018817438608734439722); } }