462 lines
14 KiB
Solidity
462 lines
14 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";
|
|
|
|
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
|
|
)
|
|
);
|
|
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);
|
|
}
|
|
}
|
|
|
|
contract TychoRouterSingleSwapTestForHashflow is TychoRouterTestSetup {
|
|
function getForkBlock() public pure override returns (uint256) {
|
|
return 23175437;
|
|
}
|
|
|
|
function testUSV3HashflowIntegration() public {
|
|
// Performs a swap from USDC to WBTC using Hashflow RFQ
|
|
//
|
|
// USDC ───(Hashflow RFQ)──> WBTC
|
|
|
|
// The Hashflow order expects:
|
|
// - 4308094737 USDC input -> 3714751 WBTC output
|
|
|
|
uint256 amountIn = 4308094737;
|
|
uint256 expectedAmountOut = 3714751;
|
|
deal(USDC_ADDR, ALICE, amountIn);
|
|
uint256 balanceBefore = IERC20(WBTC_ADDR).balanceOf(ALICE);
|
|
|
|
vm.startPrank(ALICE);
|
|
IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max);
|
|
bytes memory callData = loadCallDataFromFile("test_hashflow");
|
|
(bool success,) = tychoRouterAddr.call(callData);
|
|
|
|
vm.stopPrank();
|
|
|
|
uint256 balanceAfter = IERC20(WBTC_ADDR).balanceOf(ALICE);
|
|
|
|
assertTrue(success, "Call Failed");
|
|
assertEq(balanceAfter - balanceBefore, expectedAmountOut);
|
|
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
|
}
|
|
}
|