Files
tycho-execution/foundry/test/TychoRouter.t.sol
TAMARA LIPOWSKI a7aa4d7ebb fix: Native ETH input/output integration tests/fixes
- Only for single swaps
- Used USV4 for this because it's the only DEX we support that allows native ETH swaps
- For Native ETH input single swaps, we need to properly check the remaining amount (we were treating them wronly like ERC20 tokens)
- For Native ETH output single swaps, we were passing the incorrect currency (the settle always needs to be the out token and the take always needs to be the in token, this should not depend on the zeroForOne value).
2025-02-21 00:23:24 -05:00

1048 lines
46 KiB
Solidity

// SPDX-License-Identifier: UNLICENSED
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 TychoRouterTest is TychoRouterTestSetup {
bytes32 public constant EXECUTOR_SETTER_ROLE =
0x6a1dd52dcad5bd732e45b6af4e7344fa284e2d7d4b23b5b09cb55d36b0685c87;
bytes32 public constant FEE_SETTER_ROLE =
0xe6ad9a47fbda1dc18de1eb5eeb7d935e5e81b4748f3cfc61e233e64f88182060;
bytes32 public constant PAUSER_ROLE =
0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a;
bytes32 public constant FUND_RESCUER_ROLE =
0x912e45d663a6f4cc1d0491d8f046e06c616f40352565ea1cdb86a0e1aaefa41b;
event CallbackVerifierSet(address indexed callbackVerifier);
event Withdrawal(
address indexed token, uint256 amount, address indexed receiver
);
function testSetExecutorsValidRole() public {
// Set single executor
address[] memory executors = new address[](1);
executors[0] = DUMMY;
vm.startPrank(EXECUTOR_SETTER);
tychoRouter.setExecutors(executors);
vm.stopPrank();
assert(tychoRouter.executors(DUMMY) == true);
// Set multiple executors
address[] memory executors2 = new address[](2);
executors2[0] = DUMMY2;
executors2[1] = DUMMY3;
vm.startPrank(EXECUTOR_SETTER);
tychoRouter.setExecutors(executors2);
vm.stopPrank();
assert(tychoRouter.executors(DUMMY2) == true);
assert(tychoRouter.executors(DUMMY3) == true);
}
function testRemoveExecutorValidRole() public {
vm.startPrank(EXECUTOR_SETTER);
address[] memory executors = new address[](1);
executors[0] = DUMMY;
tychoRouter.setExecutors(executors);
tychoRouter.removeExecutor(DUMMY);
vm.stopPrank();
assert(tychoRouter.executors(DUMMY) == false);
}
function testRemoveExecutorMissingSetterRole() public {
vm.expectRevert();
tychoRouter.removeExecutor(BOB);
}
function testSetExecutorsMissingSetterRole() public {
vm.expectRevert();
address[] memory executors = new address[](1);
executors[0] = DUMMY;
tychoRouter.setExecutors(executors);
}
function testWithdrawNative() public {
vm.startPrank(FUND_RESCUER);
// Send 100 ether to tychoRouter
assertEq(tychoRouterAddr.balance, 0);
assertEq(FUND_RESCUER.balance, 0);
vm.deal(tychoRouterAddr, 100 ether);
vm.expectEmit();
emit Withdrawal(address(0), 100 ether, FUND_RESCUER);
tychoRouter.withdrawNative(FUND_RESCUER);
assertEq(tychoRouterAddr.balance, 0);
assertEq(FUND_RESCUER.balance, 100 ether);
vm.stopPrank();
}
function testWithdrawNativeFailures() public {
vm.deal(tychoRouterAddr, 100 ether);
vm.startPrank(FUND_RESCUER);
vm.expectRevert(TychoRouter__AddressZero.selector);
tychoRouter.withdrawNative(address(0));
vm.stopPrank();
// Not role FUND_RESCUER
vm.startPrank(BOB);
vm.expectRevert();
tychoRouter.withdrawNative(FUND_RESCUER);
vm.stopPrank();
}
function testWithdrawERC20Tokens() public {
vm.startPrank(BOB);
mintTokens(100 ether, tychoRouterAddr);
vm.stopPrank();
vm.startPrank(FUND_RESCUER);
IERC20[] memory tokensArray = new IERC20[](3);
tokensArray[0] = IERC20(address(tokens[0]));
tokensArray[1] = IERC20(address(tokens[1]));
tokensArray[2] = IERC20(address(tokens[2]));
tychoRouter.withdraw(tokensArray, FUND_RESCUER);
// Check balances after withdrawing
for (uint256 i = 0; i < tokens.length; i++) {
// slither-disable-next-line calls-loop
assertEq(tokens[i].balanceOf(tychoRouterAddr), 0);
// slither-disable-next-line calls-loop
assertEq(tokens[i].balanceOf(FUND_RESCUER), 100 ether);
}
vm.stopPrank();
}
function testWithdrawERC20TokensFailures() public {
mintTokens(100 ether, tychoRouterAddr);
IERC20[] memory tokensArray = new IERC20[](3);
tokensArray[0] = IERC20(address(tokens[0]));
tokensArray[1] = IERC20(address(tokens[1]));
tokensArray[2] = IERC20(address(tokens[2]));
vm.startPrank(FUND_RESCUER);
vm.expectRevert(TychoRouter__AddressZero.selector);
tychoRouter.withdraw(tokensArray, address(0));
vm.stopPrank();
// Not role FUND_RESCUER
vm.startPrank(BOB);
vm.expectRevert();
tychoRouter.withdraw(tokensArray, FUND_RESCUER);
vm.stopPrank();
}
function testFeeSetting() public {
vm.startPrank(FEE_SETTER);
assertEq(tychoRouter.fee(), 0);
tychoRouter.setFee(100);
assertEq(tychoRouter.fee(), 100);
vm.stopPrank();
vm.startPrank(BOB);
vm.expectRevert();
tychoRouter.setFee(200);
vm.stopPrank();
}
function testFeeReceiverSetting() public {
vm.startPrank(FEE_SETTER);
assertEq(tychoRouter.feeReceiver(), address(0));
tychoRouter.setFeeReceiver(FEE_RECEIVER);
assertEq(tychoRouter.feeReceiver(), FEE_RECEIVER);
vm.stopPrank();
vm.startPrank(BOB);
vm.expectRevert();
tychoRouter.setFeeReceiver(FEE_RECEIVER);
vm.stopPrank();
}
function testPause() public {
vm.startPrank(PAUSER);
assertEq(tychoRouter.paused(), false);
tychoRouter.pause();
assertEq(tychoRouter.paused(), true);
// TODO: test swap calls when implemeted
vm.stopPrank();
vm.startPrank(UNPAUSER);
tychoRouter.unpause();
assertEq(tychoRouter.paused(), false);
vm.stopPrank();
vm.startPrank(UNPAUSER);
vm.expectRevert();
tychoRouter.unpause();
vm.stopPrank();
}
function testPauseNonRole() public {
vm.startPrank(BOB);
vm.expectRevert();
tychoRouter.pause();
vm.stopPrank();
}
function testWrapETH() public {
uint256 amount = 1 ether;
vm.deal(BOB, amount);
vm.startPrank(BOB);
tychoRouter.wrapETH{value: amount}(amount);
vm.stopPrank();
assertEq(tychoRouterAddr.balance, 0);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), amount);
}
function testUnwrapETH() public {
uint256 amount = 1 ether;
deal(WETH_ADDR, tychoRouterAddr, amount);
tychoRouter.unwrapETH(amount);
assertEq(tychoRouterAddr.balance, amount);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSwapSimple() 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 = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps));
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr);
assertEq(daiBalance, 2659881924818443699787);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSwapMultipleHops() 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] = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
)
);
// DAI -> USDC
swaps[1] = encodeSwap(
uint8(1),
uint8(2),
uint24(0),
address(usv2Executor),
bytes4(0),
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true)
);
tychoRouter.exposedSwap(amountIn, 3, pleEncode(swaps));
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr);
assertEq(usdcBalance, 2644659787);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSwapSplitHops() 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] = encodeSwap(
uint8(0),
uint8(1),
(0xffffff * 60) / 100, // 60%
address(usv2Executor),
bytes4(0),
encodeUniswapV2Swap(
WETH_ADDR, WETH_WBTC_POOL, tychoRouterAddr, false
)
);
// WBTC -> USDC
swaps[1] = encodeSwap(
uint8(1),
uint8(2),
uint24(0),
address(usv2Executor),
bytes4(0),
encodeUniswapV2Swap(
WBTC_ADDR, USDC_WBTC_POOL, tychoRouterAddr, true
)
);
// WETH -> DAI
swaps[2] = encodeSwap(
uint8(0),
uint8(3),
uint24(0),
address(usv2Executor),
bytes4(0),
encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
)
);
// DAI -> USDC
swaps[3] = encodeSwap(
uint8(3),
uint8(2),
uint24(0),
address(usv2Executor),
bytes4(0),
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true)
);
tychoRouter.exposedSwap(amountIn, 4, pleEncode(swaps));
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr);
assertEq(usdcBalance, 2615491639);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSwapChecked() 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 = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 minAmountOut = 2600 * 1e18;
uint256 amountOut = tychoRouter.swapPermit2(
amountIn,
WETH_ADDR,
DAI_ADDR,
minAmountOut,
false,
false,
2,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
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 testSwapCheckedNoPermit2() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// Checks amount out at the end
uint256 amountIn = 1 ether;
// Assume Alice has already transferred tokens to the TychoRouter
deal(WETH_ADDR, tychoRouterAddr, amountIn);
vm.startPrank(ALICE);
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
);
bytes memory swap = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 minAmountOut = 2600 * 1e18;
uint256 amountOut = tychoRouter.swap(
amountIn,
WETH_ADDR,
DAI_ADDR,
minAmountOut,
false,
false,
2,
ALICE,
pleEncode(swaps)
);
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 testSwapCheckedFailure() 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 = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
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.swapPermit2(
amountIn,
WETH_ADDR,
DAI_ADDR,
minAmountOut,
false,
false,
2,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
vm.stopPrank();
}
function testSwapFee() 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 = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 amountOut = tychoRouter.swapPermit2(
amountIn,
WETH_ADDR,
DAI_ADDR,
0,
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 testSwapWrapETH() 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 = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 amountOut = tychoRouter.swapPermit2{value: amountIn}(
amountIn,
address(0),
DAI_ADDR,
0,
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 testSwapUnwrapETH() 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 = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 amountOut = tychoRouter.swapPermit2(
amountIn,
DAI_ADDR,
address(0),
0,
false,
true,
2,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
uint256 expectedAmount = 1120007305574805922; // 1.12 ETH
assertEq(amountOut, expectedAmount);
assertEq(ALICE.balance, expectedAmount);
vm.stopPrank();
}
function testSwapSingleUSV3() 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 = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv3Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps));
uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr);
assertGe(finalBalance, expAmountOut);
}
function testEmptySwapsRevert() public {
uint256 amountIn = 10 ** 18;
bytes memory swaps = "";
vm.expectRevert(TychoRouter__EmptySwaps.selector);
tychoRouter.exposedSwap(amountIn, 2, swaps);
}
function testSingleSwapIntegration() public {
// Test created with calldata from our router encoder, replacing the executor
// address with the USV2 executor address.
// Tests swapping WETH -> DAI on a USV2 pool
deal(WETH_ADDR, ALICE, 1 ether);
uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(address(permit2Address), type(uint256).max);
// Encoded solution generated using `test_split_swap_strategy_encoder_simple`
// but manually replacing the executor address
// `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test
// `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f`
(bool success,) = tychoRouterAddr.call(
hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067d481bb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfbc3000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041f2740fde9662d8bc1f8fe8e8fc29447c1832d625f06f4a56ee5103ad555c12323af5d50eb840f73d17873383ae3b7573956d5df7b2bf76bddba768c2837894a51b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000000000"
);
vm.stopPrank();
uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertGt(balancerAfter - balancerBefore, 26173932);
}
function testSingleSwapWithoutPermit2Integration() public {
// Test created with calldata from our router encoder, replacing the executor
// address with the USV2 executor address.
// Tests swapping WETH -> DAI on a USV2 pool without permit2
deal(WETH_ADDR, tychoRouterAddr, 1 ether);
uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
// TODO replace this calldata once we have the encoder side
// (bool success,) = tychoRouterAddr.call(
// hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067d481bb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfbc3000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041f2740fde9662d8bc1f8fe8e8fc29447c1832d625f06f4a56ee5103ad555c12323af5d50eb840f73d17873383ae3b7573956d5df7b2bf76bddba768c2837894a51b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000000000"
// );
//
// vm.stopPrank();
//
// uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
// assertTrue(success, "Call Failed");
// assertGt(balancerAfter - balancerBefore, 26173932);
}
function testUSV4Integration() public {
// Test created with calldata from our router encoder.
// Performs a sequential swap from USDC to PEPE though ETH using two
// consecutive USV4 pools
//
// USDC ──(USV4)──> ETH ───(USV4)──> PEPE
//
deal(USDC_ADDR, ALICE, 1 ether);
uint256 balancerBefore = IERC20(PEPE_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(USDC_ADDR).approve(address(permit2Address), type(uint256).max);
// Encoded solution generated using `test_split_encoding_strategy_usv4`
// and ensuring that the encoded executor address is the one in this test
// `f62849f9a0b5bf2913b396098f7c7019b51a820a`
(bool success,) = tychoRouterAddr.call(
hex"d499aa88000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000067ddee9e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067b668a6000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041bdf91011918dcb5f59ab3588212a035c770a2839fe2c19060491370fa89685b8469def9e83c7b9cf8f0ef5088a3179556a6ba1096cefbe83c09a1182981c93e41c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009400920001000000f62849f9a0b5bf2913b396098f7c7019b51a820abd0625aba0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d231193300f62849f9a0b5bf2913b396098f7c7019b51a820a91dd73460000000000000000000000000000000000000000000bb800003c6982508145454ce325ddbe47a25d4ec3d23119330061a80001f4000000000000000000000000"
);
vm.stopPrank();
uint256 balancerAfter = IERC20(PEPE_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balancerAfter - balancerBefore, 97191013220606467325121599);
}
function testUSV4IntegrationInputETH() public {
// Test created with calldata from our router encoder.
// Performs a single swap from ETH to PEPE without wrapping or unwrapping
//
// ETH ───(USV4)──> PEPE
//
deal(ALICE, 1 ether);
uint256 balancerBefore = IERC20(PEPE_ADDR).balanceOf(ALICE);
// Encoded solution generated using `test_split_encoding_strategy_usv4_eth_in`
// and ensuring that the encoded executor address is the one in this test
// `f62849f9a0b5bf2913b396098f7c7019b51a820a`
(bool success,) = tychoRouterAddr.call{value: 1 ether}(
hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067def8e900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067b772f100000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004192bf6f59a6e114588b4b5fb00f3acae3eb2dd18b673924b9cf27d1414be469b70113e4ceef228e11c91d178fea26673d9edcd013dee23fa3c45abdfcb573c9371c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007a00780001000000f62849f9a0b5bf2913b396098f7c7019b51a820abd0625ab00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d231193301f62849f9a0b5bf2913b396098f7c7019b51a820a91dd73466982508145454ce325ddbe47a25d4ec3d23119330061a80001f4000000000000"
);
vm.stopPrank();
uint256 balancerAfter = IERC20(PEPE_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balancerAfter - balancerBefore, 242373460199848577067005852);
}
function testUSV4IntegrationOutputETH() public {
// Test created with calldata from our router encoder.
// Performs a single swap from ETH to PEPE without wrapping or unwrapping
//
// USDC ───(USV4)──> ETH
//
deal(USDC_ADDR, ALICE, 3000_000000);
uint256 balancerBefore = ALICE.balance;
// Approve permit2
vm.startPrank(ALICE);
IERC20(USDC_ADDR).approve(address(permit2Address), type(uint256).max);
// Encoded solution generated using `test_split_encoding_strategy_usv4_eth_in`
// and ensuring that the encoded executor address is the one in this test
// `f62849f9a0b5bf2913b396098f7c7019b51a820a`
(bool success,) = tychoRouterAddr.call(
hex"d499aa8800000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000067df206000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067b79a68000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041d603720f916c45405d4655476fc8f5d5e93e561d1bc1bbd944f865ac2b53638e28fa06fde0c1097d688029c85940a53ba54902b42d17378159ae4affb8b958b01b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007a00780001000000f62849f9a0b5bf2913b396098f7c7019b51a820abd0625aba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a91dd73460000000000000000000000000000000000000000000bb800003c000000000000"
);
vm.stopPrank();
uint256 balancerAfter = ALICE.balance;
assertTrue(success, "Call Failed");
console.logUint(balancerAfter - balancerBefore);
assertEq(balancerAfter - balancerBefore, 1117254495486192350);
}
function testSingleSwapWithWrapIntegration() public {
// Test created with calldata from our router encoder, replacing the executor
// address with the USV2 executor address.
// 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 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
// IERC20(WETH_ADDR).approve(address(permit2Address), type(uint256).max);
// Encoded solution generated using
// `test_split_swap_strategy_encoder_simple_route_wrap`
// but manually replacing the executor address
// `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test
// `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f`
(bool success,) = tychoRouterAddr.call{value: 1 ether}(
hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067d4806b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfa73000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041c36406a750c499ac7f79f7666650f0d4f20fc27bb49ab68121c0be6554cb5cab6caf90dc3aab2e21083a8fa46976521a1e9df41ce74be59abf03e0d3691541e91c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00020000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000000000"
);
vm.stopPrank();
uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertGt(balancerAfter - balancerBefore, 26173932);
}
function testSingleSwapWithUnwrapIntegration() public {
// Test created with calldata from our router encoder, replacing the executor
// address with the USV2 executor address.
// 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 balancerBefore = ALICE.balance;
// Approve permit2
vm.startPrank(ALICE);
IERC20(DAI_ADDR).approve(address(permit2Address), type(uint256).max);
// Encoded solution generated using
// `test_split_swap_strategy_encoder_simple_route_unwrap`
// but manually replacing the executor address
// `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test
// `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f`
(bool success,) = tychoRouterAddr.call(
hex"d499aa880000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000000000000000000000000000000000000067d4809800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfaa000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004146411c70ec7fee0d5d260803cb220f5365792426c5d94f7a0a4d37abb05205752c5418b1fadd059570a71f0911814e546728e1f21876f2a1c6d38d34bd235fd61c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950100000000"
);
vm.stopPrank();
uint256 balancerAfter = ALICE.balance;
assertTrue(success, "Call Failed");
assertGt(balancerAfter - balancerBefore, 26173932);
}
function testSplitSwapIntegration() public {
// Test created with calldata from our router encoder, replacing the executor
// address with the USV2 executor address.
// Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools
//
// ┌──(USV2)──> WBTC ───(USV2)──> USDC
// WETH ─┤
// └──(USV2)──> DAI ───(USV2)──> USDC
deal(WETH_ADDR, ALICE, 1 ether);
uint256 balancerBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(address(permit2Address), type(uint256).max);
// Encoded solution generated using `test_split_swap_strategy_encoder_complex`
// but manually replacing the executor address
// `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test
// `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f`
(bool success,) = tychoRouterAddr.call(
hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067d4810d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfb15000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041ecaab75f0791c9683b001ea2f0e01a0a6aaf03e6e49c83e9c8a8e588a38e3be9230d962926628ffbf6a5370cda559ff0e7876a63ed38eebe33dbef5b5e2e46ef1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000170005a00028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d0139500005a02030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d53ede3eca2a72b3aecc820e955b36f38437d0139501005a01030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d013950100000000000000000000000000000000"
);
vm.stopPrank();
uint256 balancerAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertGe(balancerAfter - balancerBefore, 26173932);
// All input tokens are transferred to the router at first. Make sure we used
// all of it (and thus our splits are correct).
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSwapAmountInNotFullySpent() 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 = encodeSwap(
uint8(0),
uint8(1),
(0xffffff * 60) / 100, // 60%
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
vm.expectRevert(
abi.encodeWithSelector(
TychoRouter__AmountInNotFullySpent.selector, 400000000000000000
)
);
tychoRouter.swapPermit2(
amountIn,
WETH_ADDR,
DAI_ADDR,
0,
false,
false,
2,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
vm.stopPrank();
}
function testSwapSingleUSV4Callback() 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),
SafeCallback.unlockCallback.selector,
pools
);
bytes memory swap = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv4Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps));
assertEq(IERC20(USDT_ADDR).balanceOf(tychoRouterAddr), 99943852);
}
function testSwapMultipleUSV4Callback() 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),
SafeCallback.unlockCallback.selector,
pools
);
bytes memory swap = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv4Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps));
assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718);
}
}