Files
tycho-execution/foundry/test/TychoRouterSingleSwap.t.sol
Diana Carvalho ee687038c5 fix: Make all tests pass!
Delete TokenTransfer.sol
Make slither happy

Bugfixes:
- Executors
  - Ekubo:
    - Fix the POOL_DATA_OFFSET value and remove sender from callback data
    - Use SafeERC20
  - Maverick and Univ2: Use safeTransfer and not safeTransferFrom
  - Univ3: update expected data length
  - Univ4: update the selectors (the signature changed)
- Router:
  - For split swap we don't need to pass the tokenInReceiver, it should always be the router address
  - For single and sequential: change order of the parameters (to be before the permit2 specific objects)
- Encoders:
  - Update selector signatures
  - For split swap pass the transfer_from (we might not need to if the token in is ETH)

Took 2 hours 51 minutes
2025-05-15 13:11:34 +01:00

392 lines
12 KiB
Solidity

// 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,
false // funds already in WETH_DAI_POOL, no transfer necessary
);
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
tychoRouter.singleSwapPermit2(
amountIn,
WETH_ADDR,
DAI_ADDR,
2008817438608734439722,
false,
false,
ALICE,
true, // transferFrom to WETH_DAI_POOL
WETH_DAI_POOL, // receiver of input tokens
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,
false // funds already in WETH_DAI_POOL, no transfer necessary
);
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,
WETH_DAI_POOL,
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, false);
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector);
tychoRouter.singleSwap(
amountIn,
WETH_ADDR,
DAI_ADDR,
0,
false,
false,
ALICE,
true,
WETH_DAI_POOL,
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,
false // funds already in WETH_DAI_POOL, no transfer necessary
);
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,
WETH_DAI_POOL,
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,
false // funds already in WETH_DAI_POOL, no transfer necessary
);
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,
WETH_DAI_POOL,
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, true);
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
uint256 amountOut = tychoRouter.singleSwapPermit2{value: amountIn}(
amountIn,
address(0),
DAI_ADDR,
1000_000000,
true,
false,
ALICE,
false,
tychoRouterAddr,
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, false
);
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
uint256 amountOut = tychoRouter.singleSwapPermit2(
amountIn,
DAI_ADDR,
address(0),
1000_000000,
false,
true,
ALICE,
true, // transferFrom to WETH_DAI_POOL
WETH_DAI_POOL, // receiver of input tokens
permitSingle,
signature,
swap
);
uint256 expectedAmount = 1475644707225677606;
assertEq(amountOut, expectedAmount);
assertEq(ALICE.balance, expectedAmount);
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);
}
}