- For protocols like Balancer and Curve, which expect funds to be in the router at the time of swap, we must support also transferring funds from the user into the router. Doing this in the router would mean we are dealing with transfers in two different places: in the router main methods and in the executors. To avoid this, we are now performing transfers just in the executors, and two transfer types have been added to support transfers into the router. TODO: - Add this for Balancer and Curve (only added for USV4 atm). - TODO consider renaming TRANSFER_FROM and TRANSFER_PERMIT2 to include "pool" in the name
328 lines
12 KiB
Solidity
328 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,
|
|
tychoRouterAddr,
|
|
false,
|
|
TokenTransfer.TransferType.TRANSFER_PERMIT2
|
|
);
|
|
|
|
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,
|
|
TokenTransfer.TransferType.TRANSFER_FROM
|
|
);
|
|
|
|
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,
|
|
TokenTransfer.TransferType.TRANSFER_FROM
|
|
);
|
|
|
|
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,
|
|
TokenTransfer.TransferType.TRANSFER_FROM
|
|
);
|
|
|
|
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,
|
|
TokenTransfer.TransferType.TRANSFER_FROM
|
|
);
|
|
|
|
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 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,
|
|
TokenTransfer.TransferType.TRANSFER
|
|
);
|
|
|
|
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, tychoRouterAddr, amountIn);
|
|
|
|
bytes memory protocolData = encodeUniswapV2Swap(
|
|
DAI_ADDR,
|
|
WETH_DAI_POOL,
|
|
tychoRouterAddr,
|
|
true,
|
|
TokenTransfer.TransferType.TRANSFER_PERMIT2
|
|
);
|
|
|
|
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();
|
|
}
|
|
|
|
function testSingleSwapIntegration() public {
|
|
// Tests swapping WETH -> DAI on a USV2 pool with regular approvals
|
|
deal(WETH_ADDR, ALICE, 1 ether);
|
|
uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
|
|
|
|
vm.startPrank(ALICE);
|
|
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
|
|
// Encoded solution generated using `test_single_swap_strategy_encoder_no_permit2`
|
|
(bool success,) = tychoRouterAddr.call(
|
|
hex"20144a070000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000008f1d5c1cae3740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500010000000000000000000000000000"
|
|
);
|
|
|
|
vm.stopPrank();
|
|
|
|
uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
|
|
assertTrue(success, "Call Failed");
|
|
assertEq(balancerAfter - balancerBefore, 2659881924818443699787);
|
|
}
|
|
|
|
function testSingleSwapIntegrationPermit2() public {
|
|
// Tests swapping WETH -> DAI on a USV2 pool with permit2
|
|
deal(WETH_ADDR, ALICE, 1 ether);
|
|
uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
|
|
|
|
vm.startPrank(ALICE);
|
|
IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
|
// Encoded solution generated using `test_single_swap_strategy_encoder`
|
|
(bool success,) = tychoRouterAddr.call(
|
|
hex"30ace1b10000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000903146e5f6c59c064b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000682169c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067f9e3c900000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000412d3f0fee3fc61987512f024f20b1448eb934f82105a91653dd169179c693aaf95d09ef666ce1d38be70b8156fa6e4ea3e8717204e02fe7ba99a1fc4e5a26e6e11b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500020000000000000000000000000000"
|
|
);
|
|
|
|
vm.stopPrank();
|
|
|
|
uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
|
|
assertTrue(success, "Call Failed");
|
|
assertEq(balancerAfter - balancerBefore, 2659881924818443699787);
|
|
}
|
|
}
|