feat: Add swap method with tests
Changes: - If the tokenIn is ETH, skip permit2 approval - Make executors payable: When using delegatecall the executor inherits the execution context of whoever calls it. Our main swap function can accept ETH, it needs to be payable so by consequence the executors also need to be. - Set uniswap v2 executor in test router - Add tests for all possible cases of swap - Add tests for all cases of splitSwap - Add test functions to handle permit2 and encode swaps --- don't change below this line --- ENG-4041 Took 3 hours 50 minutes Took 49 seconds Took 14 seconds
This commit is contained in:
@@ -22,6 +22,7 @@ interface IExecutor {
|
||||
*/
|
||||
function swap(uint256 givenAmount, bytes calldata data)
|
||||
external
|
||||
payable
|
||||
returns (uint256 calculatedAmount);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,32 +87,60 @@ contract TychoRouter is
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Executes a swap graph supporting internal token amount
|
||||
* splits, checking that the user gets more than minUserAmount of buyToken.
|
||||
* @notice Executes a swap operation based on a predefined swap graph, supporting internal token amount splits.
|
||||
* This function enables multi-step swaps, optional ETH wrapping/unwrapping, and validates the output amount
|
||||
* against a user-specified minimum.
|
||||
*
|
||||
* @dev
|
||||
* - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token.
|
||||
* - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver.
|
||||
* - For ERC20 tokens, Permit2 is used to approve and transfer tokens from the caller to the router.
|
||||
* - Swaps are executed sequentially using the `_splitSwap` function.
|
||||
* - A fee is deducted from the output token if `fee > 0`, and the remaining amount is sent to the receiver.
|
||||
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `checkMinAmount` is true.
|
||||
*
|
||||
* @param amountIn The input token amount to be swapped.
|
||||
* @param tokenIn The address of the input token. Use `address(0)` for native ETH when `wrapEth` is true.
|
||||
* @param tokenOut The address of the output token. Use `address(0)` for native ETH when `unwrapEth` is true.
|
||||
* @param checkMinAmount A boolean indicating whether to enforce the `minAmountOut` check.
|
||||
* @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met.
|
||||
* @param wrapEth If true, treats the input token as native ETH and wraps it into WETH.
|
||||
* @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver.
|
||||
* @param nTokens The total number of tokens involved in the swap graph (used to initialize arrays for internal calculations).
|
||||
* @param receiver The address to receive the output tokens.
|
||||
* @param permitSingle A Permit2 structure containing token approval details for the input token. Ignored if `wrapEth` is true.
|
||||
* @param signature A valid signature authorizing the Permit2 approval. Ignored if `wrapEth` is true.
|
||||
* @param swaps Encoded swap graph data containing details of each swap.
|
||||
*
|
||||
* @return amountOut The total amount of the output token received by the receiver, after deducting fees if applicable.
|
||||
*/
|
||||
function swap(
|
||||
uint256 amountIn,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 checkAmountOut,
|
||||
bool wrapEth, // This means ETH is the sell token
|
||||
bool unwrapEth, // This means ETH is the buy token
|
||||
bool checkMinAmount,
|
||||
uint256 minAmountOut,
|
||||
bool wrapEth,
|
||||
bool unwrapEth,
|
||||
uint256 nTokens,
|
||||
address receiver,
|
||||
IAllowanceTransfer.PermitSingle calldata permitSingle,
|
||||
bytes calldata signature,
|
||||
bytes calldata swaps
|
||||
) external whenNotPaused returns (uint256 amountOut) {
|
||||
) external payable whenNotPaused returns (uint256 amountOut) {
|
||||
// For native ETH, assume funds already in our router. Else, transfer and handle approval.
|
||||
if (wrapEth) {
|
||||
_wrapETH(amountIn);
|
||||
} else {
|
||||
permit2.permit(msg.sender, permitSingle, signature);
|
||||
permit2.transferFrom(
|
||||
msg.sender,
|
||||
address(this),
|
||||
uint160(amountIn),
|
||||
permitSingle.details.token
|
||||
);
|
||||
if (tokenIn != address(0)) {
|
||||
permit2.permit(msg.sender, permitSingle, signature);
|
||||
permit2.transferFrom(
|
||||
msg.sender,
|
||||
address(this),
|
||||
uint160(amountIn),
|
||||
permitSingle.details.token
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
amountOut = _splitSwap(amountIn, nTokens, swaps);
|
||||
@@ -121,10 +149,13 @@ contract TychoRouter is
|
||||
uint256 feeAmount = (amountOut * fee) / 10000;
|
||||
amountOut -= feeAmount;
|
||||
IERC20(tokenOut).safeTransfer(feeReceiver, feeAmount);
|
||||
if (unwrapEth == false) {
|
||||
IERC20(tokenOut).safeTransfer(receiver, amountOut);
|
||||
}
|
||||
}
|
||||
|
||||
if (amountOut < checkAmountOut) {
|
||||
revert TychoRouter__NegativeSlippage(amountOut, checkAmountOut);
|
||||
if (checkMinAmount && amountOut < minAmountOut) {
|
||||
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
|
||||
}
|
||||
|
||||
if (unwrapEth) {
|
||||
@@ -152,15 +183,14 @@ contract TychoRouter is
|
||||
|
||||
while (swaps_.length > 0) {
|
||||
(swapData, swaps_) = swaps_.next();
|
||||
split = swapData.splitPercentage();
|
||||
tokenInIndex = swapData.tokenInIndex();
|
||||
tokenOutIndex = swapData.tokenOutIndex();
|
||||
split = swapData.splitPercentage();
|
||||
currentAmountIn = split > 0
|
||||
? (amounts[tokenInIndex] * split) / 0xffffff
|
||||
: remainingAmounts[tokenInIndex];
|
||||
currentAmountOut =
|
||||
_callExecutor(currentAmountIn, swapData.protocolData());
|
||||
|
||||
amounts[tokenOutIndex] += currentAmountOut;
|
||||
remainingAmounts[tokenOutIndex] += currentAmountOut;
|
||||
remainingAmounts[tokenInIndex] -= currentAmountIn;
|
||||
|
||||
@@ -12,6 +12,7 @@ contract UniswapV2Executor is IExecutor {
|
||||
|
||||
function swap(uint256 givenAmount, bytes calldata data)
|
||||
external
|
||||
payable
|
||||
returns (uint256 calculatedAmount)
|
||||
{
|
||||
address target;
|
||||
|
||||
@@ -9,6 +9,10 @@ contract Constants is Test {
|
||||
address FUND_RESCUER = makeAddr("fundRescuer");
|
||||
address FEE_SETTER = makeAddr("feeSetter");
|
||||
address FEE_RECEIVER = makeAddr("feeReceiver");
|
||||
address EXECUTOR_SETTER = makeAddr("executorSetter");
|
||||
address ALICE = 0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2;
|
||||
uint256 ALICE_PK =
|
||||
0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234;
|
||||
|
||||
// Dummy contracts
|
||||
address DUMMY = makeAddr("dummy");
|
||||
@@ -18,6 +22,14 @@ contract Constants is Test {
|
||||
// Assets
|
||||
address WETH_ADDR = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
|
||||
address DAI_ADDR = address(0x6B175474E89094C44Da98b954EedeAC495271d0F);
|
||||
address USDC_ADDR = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
|
||||
address WBTC_ADDR = address(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);
|
||||
|
||||
// uniswap v2
|
||||
address WETH_DAI_POOL = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11;
|
||||
address DAI_USDC_POOL = 0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5;
|
||||
address WETH_WBTC_POOL = 0xBb2b8038a1640196FbE3e38816F3e67Cba72D940;
|
||||
address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416;
|
||||
|
||||
/**
|
||||
* @dev Deploys a dummy contract with non-empty bytecode
|
||||
|
||||
@@ -206,11 +206,355 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
uint256 amount = 1 ether;
|
||||
deal(WETH_ADDR, address(tychoRouter), amount);
|
||||
|
||||
vm.startPrank(BOB);
|
||||
tychoRouter.unwrapETH(amount);
|
||||
vm.stopPrank();
|
||||
|
||||
assertEq(address(tychoRouter).balance, amount);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(address(tychoRouter)), 0);
|
||||
}
|
||||
|
||||
function testSplitSwapSimple() public {
|
||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
||||
// 1 WETH -> DAI
|
||||
// (univ2)
|
||||
uint256 amount_in = 1 ether;
|
||||
deal(WETH_ADDR, address(tychoRouter), amount_in);
|
||||
|
||||
bytes memory protocolData =
|
||||
encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSwap(uint8(0), uint8(1), uint24(0), protocolData);
|
||||
bytes[] memory swaps = new bytes[](1);
|
||||
swaps[0] = swap;
|
||||
|
||||
uint256 minAmountOut = 2600 * 1e18;
|
||||
uint256 amountOut =
|
||||
tychoRouter.splitSwap(amount_in, 2, pleEncode(swaps));
|
||||
|
||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
assertEq(daiBalance, 2630432278145144658455);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0);
|
||||
}
|
||||
|
||||
function testSplitSwapMultipleHops() public {
|
||||
// Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2
|
||||
// 1 WETH -> DAI -> USDC
|
||||
// (univ2) (univ2)
|
||||
uint256 amount_in = 1 ether;
|
||||
deal(WETH_ADDR, address(tychoRouter), amount_in);
|
||||
|
||||
bytes[] memory swaps = new bytes[](2);
|
||||
// WETH -> DAI
|
||||
swaps[0] = encodeSwap(
|
||||
uint8(0),
|
||||
uint8(1),
|
||||
uint24(0),
|
||||
encodeUniswapV2Swap(
|
||||
WETH_ADDR, WETH_DAI_POOL, address(tychoRouter), false
|
||||
)
|
||||
);
|
||||
|
||||
// DAI -> USDC
|
||||
swaps[1] = encodeSwap(
|
||||
uint8(1),
|
||||
uint8(2),
|
||||
uint24(0),
|
||||
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, ALICE, true)
|
||||
);
|
||||
|
||||
uint256 minAmountOut = 2600 * 1e6;
|
||||
uint256 amountOut =
|
||||
tychoRouter.splitSwap(amount_in, 3, pleEncode(swaps));
|
||||
|
||||
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
assertEq(usdcBalance, 2610580090);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0);
|
||||
}
|
||||
|
||||
function testSplitSwapSplitHops() public {
|
||||
// Trade 1 WETH for USDC through DAI and WBTC with 4 swaps on Uniswap V2
|
||||
// -> DAI ->
|
||||
// 1 WETH USDC
|
||||
// -> WBTC ->
|
||||
// (univ2) (univ2)
|
||||
uint256 amount_in = 1 ether;
|
||||
deal(WETH_ADDR, address(tychoRouter), amount_in);
|
||||
|
||||
bytes[] memory swaps = new bytes[](4);
|
||||
// WETH -> WBTC (60%)
|
||||
swaps[0] = encodeSwap(
|
||||
uint8(0),
|
||||
uint8(1),
|
||||
(0xffffff * 60) / 100, // 60%
|
||||
encodeUniswapV2Swap(
|
||||
WETH_ADDR, WETH_WBTC_POOL, address(tychoRouter), false
|
||||
)
|
||||
);
|
||||
// WBTC -> USDC
|
||||
swaps[1] = encodeSwap(
|
||||
uint8(1),
|
||||
uint8(2),
|
||||
uint24(0),
|
||||
encodeUniswapV2Swap(WBTC_ADDR, USDC_WBTC_POOL, ALICE, true)
|
||||
);
|
||||
// WETH -> DAI
|
||||
swaps[2] = encodeSwap(
|
||||
uint8(0),
|
||||
uint8(3),
|
||||
uint24(0),
|
||||
encodeUniswapV2Swap(
|
||||
WETH_ADDR, WETH_DAI_POOL, address(tychoRouter), false
|
||||
)
|
||||
);
|
||||
|
||||
// DAI -> USDC
|
||||
swaps[3] = encodeSwap(
|
||||
uint8(3),
|
||||
uint8(2),
|
||||
uint24(0),
|
||||
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, ALICE, true)
|
||||
);
|
||||
|
||||
uint256 minAmountOut = 2580 * 1e6;
|
||||
uint256 amountOut =
|
||||
tychoRouter.splitSwap(amount_in, 4, pleEncode(swaps));
|
||||
|
||||
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||
assertEq(usdcBalance, 2581503157);
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 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 amount_in = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amount_in);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(WETH_ADDR, amount_in);
|
||||
|
||||
bytes memory protocolData =
|
||||
encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSwap(uint8(0), uint8(1), uint24(0), protocolData);
|
||||
bytes[] memory swaps = new bytes[](1);
|
||||
swaps[0] = swap;
|
||||
|
||||
uint256 minAmountOut = 2600 * 1e18;
|
||||
uint256 amountOut = tychoRouter.swap(
|
||||
amount_in,
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
true,
|
||||
minAmountOut,
|
||||
false,
|
||||
false,
|
||||
2,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
|
||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
assertEq(daiBalance, 2630432278145144658455);
|
||||
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 amount_in = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amount_in);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(WETH_ADDR, amount_in);
|
||||
|
||||
bytes memory protocolData =
|
||||
encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSwap(uint8(0), uint8(1), uint24(0), protocolData);
|
||||
bytes[] memory swaps = new bytes[](1);
|
||||
swaps[0] = swap;
|
||||
|
||||
uint256 minAmountOut = 3000 * 1e18;
|
||||
vm.expectRevert(
|
||||
abi.encodeWithSelector(
|
||||
TychoRouter__NegativeSlippage.selector,
|
||||
2630432278145144658455, // actual amountOut
|
||||
minAmountOut
|
||||
)
|
||||
);
|
||||
uint256 amountOut = tychoRouter.swap(
|
||||
amount_in,
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
true,
|
||||
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 amount_in = 1 ether;
|
||||
deal(WETH_ADDR, ALICE, amount_in);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(WETH_ADDR, amount_in);
|
||||
|
||||
bytes memory protocolData = encodeUniswapV2Swap(
|
||||
WETH_ADDR, WETH_DAI_POOL, address(tychoRouter), false
|
||||
);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSwap(uint8(0), uint8(1), uint24(0), protocolData);
|
||||
bytes[] memory swaps = new bytes[](1);
|
||||
swaps[0] = swap;
|
||||
|
||||
uint256 amountOut = tychoRouter.swap(
|
||||
amount_in,
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
2,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
|
||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
assertEq(daiBalance, 2604127955363693211871);
|
||||
assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26304322781451446584);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSwapWrapETH() public {
|
||||
// Trade 1 ETH (and wrap it) for DAI with 1 swap on Uniswap V2
|
||||
|
||||
uint256 amount_in = 1 ether;
|
||||
deal(ALICE, amount_in);
|
||||
|
||||
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);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSwap(uint8(0), uint8(1), uint24(0), protocolData);
|
||||
bytes[] memory swaps = new bytes[](1);
|
||||
swaps[0] = swap;
|
||||
|
||||
uint256 amountOut = tychoRouter.swap{value: amount_in}(
|
||||
amount_in,
|
||||
address(0),
|
||||
DAI_ADDR,
|
||||
false,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
2,
|
||||
ALICE,
|
||||
emptyPermitSingle,
|
||||
"",
|
||||
pleEncode(swaps)
|
||||
);
|
||||
|
||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
assertEq(daiBalance, 2630432278145144658455);
|
||||
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 amount_in = 3_000 * 10 ** 18;
|
||||
deal(DAI_ADDR, ALICE, amount_in);
|
||||
|
||||
vm.startPrank(ALICE);
|
||||
|
||||
(
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||
bytes memory signature
|
||||
) = handlePermit2Approval(DAI_ADDR, amount_in);
|
||||
|
||||
bytes memory protocolData = encodeUniswapV2Swap(
|
||||
DAI_ADDR, WETH_DAI_POOL, address(tychoRouter), true
|
||||
);
|
||||
|
||||
bytes memory swap =
|
||||
encodeSwap(uint8(0), uint8(1), uint24(0), protocolData);
|
||||
bytes[] memory swaps = new bytes[](1);
|
||||
swaps[0] = swap;
|
||||
|
||||
uint256 amountOut = tychoRouter.swap(
|
||||
amount_in,
|
||||
DAI_ADDR,
|
||||
address(0),
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
2,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
pleEncode(swaps)
|
||||
);
|
||||
|
||||
assertEq(ALICE.balance, 1132829934891544187); // 1.13 ETH
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import "@src/TychoRouter.sol";
|
||||
import "../src/executors/UniswapV2Executor.sol";
|
||||
import "./Constants.sol";
|
||||
import "./mock/MockERC20.sol";
|
||||
import "@src/TychoRouter.sol";
|
||||
import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol";
|
||||
|
||||
contract TychoRouterExposed is TychoRouter {
|
||||
@@ -16,12 +17,20 @@ contract TychoRouterExposed is TychoRouter {
|
||||
function unwrapETH(uint256 amount) external {
|
||||
return _unwrapETH(amount);
|
||||
}
|
||||
|
||||
function splitSwap(uint256 amountIn, uint256 nTokens, bytes calldata swaps)
|
||||
external
|
||||
returns (uint256)
|
||||
{
|
||||
return _splitSwap(amountIn, nTokens, swaps);
|
||||
}
|
||||
}
|
||||
|
||||
contract TychoRouterTestSetup is Test, Constants {
|
||||
TychoRouterExposed tychoRouter;
|
||||
address executorSetter;
|
||||
address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3);
|
||||
UniswapV2Executor public usv2Executor;
|
||||
MockERC20[] tokens;
|
||||
|
||||
function setUp() public {
|
||||
@@ -35,10 +44,18 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER);
|
||||
tychoRouter.grantRole(keccak256("PAUSER_ROLE"), PAUSER);
|
||||
tychoRouter.grantRole(keccak256("UNPAUSER_ROLE"), UNPAUSER);
|
||||
tychoRouter.grantRole(
|
||||
keccak256("EXECUTOR_SETTER_ROLE"), EXECUTOR_SETTER
|
||||
);
|
||||
executorSetter = BOB;
|
||||
deployDummyContract();
|
||||
vm.stopPrank();
|
||||
|
||||
usv2Executor = new UniswapV2Executor();
|
||||
vm.startPrank(EXECUTOR_SETTER);
|
||||
tychoRouter.setExecutor(address(usv2Executor));
|
||||
vm.stopPrank();
|
||||
|
||||
vm.startPrank(BOB);
|
||||
tokens.push(new MockERC20("Token A", "A"));
|
||||
tokens.push(new MockERC20("Token B", "B"));
|
||||
@@ -57,4 +74,116 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
tokens[i].mint(to, amount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Handles the Permit2 approval process for Alice, allowing the TychoRouter contract
|
||||
* to spend `amount_in` of `tokenIn` on her behalf.
|
||||
*
|
||||
* This function approves the Permit2 contract to transfer the specified token amount
|
||||
* and constructs a `PermitSingle` struct for the approval. It also generates a valid
|
||||
* EIP-712 signature for the approval using Alice's private key.
|
||||
*
|
||||
* @param tokenIn The address of the token being approved.
|
||||
* @param amount_in The amount of tokens to approve for transfer.
|
||||
* @return permitSingle The `PermitSingle` struct containing the approval details.
|
||||
* @return signature The EIP-712 signature for the approval.
|
||||
*/
|
||||
function handlePermit2Approval(address tokenIn, uint256 amount_in)
|
||||
internal
|
||||
returns (IAllowanceTransfer.PermitSingle memory, bytes memory)
|
||||
{
|
||||
IERC20(tokenIn).approve(permit2Address, amount_in);
|
||||
IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer
|
||||
.PermitSingle({
|
||||
details: IAllowanceTransfer.PermitDetails({
|
||||
token: tokenIn,
|
||||
amount: uint160(amount_in),
|
||||
expiration: uint48(block.timestamp + 1 days),
|
||||
nonce: 0
|
||||
}),
|
||||
spender: address(tychoRouter),
|
||||
sigDeadline: block.timestamp + 1 days
|
||||
});
|
||||
|
||||
bytes memory signature = signPermit2(permitSingle, ALICE_PK);
|
||||
return (permitSingle, signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Signs a Permit2 `PermitSingle` struct with the given private key.
|
||||
* @param permit The `PermitSingle` struct to sign.
|
||||
* @param privateKey The private key of the signer.
|
||||
* @return The signature as a `bytes` array.
|
||||
*/
|
||||
function signPermit2(
|
||||
IAllowanceTransfer.PermitSingle memory permit,
|
||||
uint256 privateKey
|
||||
) internal returns (bytes memory) {
|
||||
bytes32 _PERMIT_DETAILS_TYPEHASH = keccak256(
|
||||
"PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
|
||||
);
|
||||
bytes32 _PERMIT_SINGLE_TYPEHASH = keccak256(
|
||||
"PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
|
||||
);
|
||||
bytes32 domainSeparator = keccak256(
|
||||
abi.encode(
|
||||
keccak256(
|
||||
"EIP712Domain(string name,uint256 chainId,address verifyingContract)"
|
||||
),
|
||||
keccak256("Permit2"),
|
||||
block.chainid,
|
||||
permit2Address
|
||||
)
|
||||
);
|
||||
bytes32 detailsHash =
|
||||
keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, permit.details));
|
||||
bytes32 permitHash = keccak256(
|
||||
abi.encode(
|
||||
_PERMIT_SINGLE_TYPEHASH,
|
||||
detailsHash,
|
||||
permit.spender,
|
||||
permit.sigDeadline
|
||||
)
|
||||
);
|
||||
|
||||
bytes32 digest =
|
||||
keccak256(abi.encodePacked("\x19\x01", domainSeparator, permitHash));
|
||||
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
|
||||
|
||||
return abi.encodePacked(r, s, v);
|
||||
}
|
||||
|
||||
function pleEncode(bytes[] memory data)
|
||||
public
|
||||
pure
|
||||
returns (bytes memory encoded)
|
||||
{
|
||||
for (uint256 i = 0; i < data.length; i++) {
|
||||
encoded = bytes.concat(
|
||||
encoded,
|
||||
abi.encodePacked(bytes2(uint16(data[i].length)), data[i])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeSwap(
|
||||
uint8 tokenInIndex,
|
||||
uint8 tokenOutIndex,
|
||||
uint24 split,
|
||||
bytes memory protocolData
|
||||
) internal pure returns (bytes memory) {
|
||||
return
|
||||
abi.encodePacked(tokenInIndex, tokenOutIndex, split, protocolData);
|
||||
}
|
||||
|
||||
function encodeUniswapV2Swap(
|
||||
address tokenIn,
|
||||
address target,
|
||||
address receiver,
|
||||
bool zero2one
|
||||
) internal view returns (bytes memory) {
|
||||
return abi.encodePacked(
|
||||
usv2Executor, bytes4(0), tokenIn, target, receiver, zero2one
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ contract UniswapV2ExecutorTest is UniswapV2ExecutorExposed, Test, Constants {
|
||||
UniswapV2ExecutorExposed uniswapV2Exposed;
|
||||
IERC20 WETH = IERC20(WETH_ADDR);
|
||||
IERC20 DAI = IERC20(DAI_ADDR);
|
||||
address WETH_DAI_POOL = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11;
|
||||
|
||||
function setUp() public {
|
||||
uint256 forkBlock = 17323404;
|
||||
|
||||
Reference in New Issue
Block a user