From 3b2d9fcbdff00be8015c1c70a20687677bf4b22c Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 24 Jan 2025 15:59:12 +0000 Subject: [PATCH 1/9] feat: Wrap and unwrap ETH --- don't change below this line --- ENG-4041 Took 2 hours 28 minutes Took 14 seconds Took 11 seconds Took 2 minutes Took 1 minute Took 7 minutes --- foundry/lib/IWETH.sol | 9 +++++++ foundry/src/TychoRouter.sol | 34 ++++++++++++++++++++++++++- foundry/test/TychoRouter.t.sol | 24 +++++++++++++++++++ foundry/test/TychoRouterTestSetup.sol | 20 ++++++++++++++-- 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 foundry/lib/IWETH.sol diff --git a/foundry/lib/IWETH.sol b/foundry/lib/IWETH.sol new file mode 100644 index 0000000..3b4a359 --- /dev/null +++ b/foundry/lib/IWETH.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +interface IWETH is IERC20 { + function deposit() external payable; + function withdraw(uint256) external; +} diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index f929e9b..5276748 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; +import "../lib/IWETH.sol"; +import "../lib/bytes/LibPrefixLengthEncodedByteArray.sol"; +import "./CallbackVerificationDispatcher.sol"; +import "./SwapExecutionDispatcher.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -8,9 +12,12 @@ import "@permit2/src/interfaces/IAllowanceTransfer.sol"; import "./ExecutionDispatcher.sol"; import "./CallbackVerificationDispatcher.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; +import {Swap} from "./Swap.sol"; error TychoRouter__WithdrawalFailed(); error TychoRouter__AddressZero(); +error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount); +error TychoRouter__MessageValueMismatch(uint256 value, uint256 amount); contract TychoRouter is AccessControl, @@ -19,8 +26,11 @@ contract TychoRouter is Pausable { IAllowanceTransfer public immutable permit2; + IWETH private immutable _weth; using SafeERC20 for IERC20; + using LibPrefixLengthEncodedByteArray for bytes; + using Swap for bytes; //keccak256("NAME_OF_ROLE") : save gas on deployment bytes32 public constant EXECUTOR_SETTER_ROLE = @@ -48,9 +58,10 @@ contract TychoRouter is ); event FeeSet(uint256 indexed oldFee, uint256 indexed newFee); - constructor(address _permit2) { + constructor(address _permit2, address weth) { permit2 = IAllowanceTransfer(_permit2); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + _weth = IWETH(weth); } /** @@ -209,6 +220,27 @@ contract TychoRouter is } } + /** + * @dev Wraps a defined amount of ETH. + * @param amount of native ETH to wrap. + */ + function _wrapETH(uint256 amount) internal { + if (msg.value > 0 && msg.value != amount) { + revert TychoRouter__MessageValueMismatch(msg.value, amount); + } + _weth.deposit{value: amount}(); + } + + /** + * @dev Unwraps a defined amount of WETH. + * @param amount of WETH to unwrap. + */ + function _unwrapETH(uint256 amount) internal { + uint256 unwrapAmount = + amount == 0 ? _weth.balanceOf(address(this)) : amount; + _weth.withdraw(unwrapAmount); + } + /** * @dev Allows this contract to receive native token */ diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 3d6f6d0..5458016 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -189,4 +189,28 @@ contract TychoRouterTest is TychoRouterTestSetup { 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(address(tychoRouter).balance, 0); + assertEq(IERC20(WETH_ADDR).balanceOf(address(tychoRouter)), amount); + } + + function testUnwrapETH() public { + 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); + } } diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 8958313..345f7f0 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -4,16 +4,32 @@ pragma solidity ^0.8.13; import "@src/TychoRouter.sol"; import "./Constants.sol"; import "./mock/MockERC20.sol"; +import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol"; + +contract TychoRouterExposed is TychoRouter { + constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {} + + function wrapETH(uint256 amount) external payable { + return _wrapETH(amount); + } + + function unwrapETH(uint256 amount) external { + return _unwrapETH(amount); + } +} contract TychoRouterTestSetup is Test, Constants { - TychoRouter tychoRouter; + TychoRouterExposed tychoRouter; address executorSetter; address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3); MockERC20[] tokens; function setUp() public { + uint256 forkBlock = 21000000; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + vm.startPrank(ADMIN); - tychoRouter = new TychoRouter(permit2Address); + tychoRouter = new TychoRouterExposed(permit2Address, WETH_ADDR); tychoRouter.grantRole(keccak256("EXECUTOR_SETTER_ROLE"), BOB); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER); From a8f6fc1eeca8b3fcb0a5786ea538bf3fb087c111 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 24 Jan 2025 17:13:30 +0000 Subject: [PATCH 2/9] feat: Add swap method (first attempt) Will add tests and fullproof it in a future PR --- don't change below this line --- ENG-4041 Took 8 minutes Took 42 seconds Took 5 seconds --- foundry/src/Swap.sol | 40 ++++++++++++++++++ foundry/src/TychoRouter.sol | 82 ++++++++++++++++++++++++++++++++----- 2 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 foundry/src/Swap.sol diff --git a/foundry/src/Swap.sol b/foundry/src/Swap.sol new file mode 100644 index 0000000..7c78277 --- /dev/null +++ b/foundry/src/Swap.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +library Swap { + /// Returns the InToken index into an array of tokens + function tokenInIndex(bytes calldata swap) + internal + pure + returns (uint8 res) + { + res = uint8(swap[0]); + } + + /// The OutToken index into an array of tokens + function tokenOutIndex(bytes calldata swap) + internal + pure + returns (uint8 res) + { + res = uint8(swap[1]); + } + + /// The relative amount of token quantity routed into this swap + function splitPercentage(bytes calldata swap) + internal + pure + returns (uint24 res) + { + res = uint24(bytes3(swap[2:5])); + } + + /// Remaining bytes are interpreted as protocol data + function protocolData(bytes calldata swap) + internal + pure + returns (bytes calldata res) + { + res = swap[5:]; + } +} diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 5276748..d2badbb 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.28; import "../lib/IWETH.sol"; import "../lib/bytes/LibPrefixLengthEncodedByteArray.sol"; import "./CallbackVerificationDispatcher.sol"; -import "./SwapExecutionDispatcher.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -88,22 +87,85 @@ contract TychoRouter is } /** - * @dev Executes a swap graph supporting internal splits token amount + * @dev Executes a swap graph supporting internal token amount * splits, checking that the user gets more than minUserAmount of buyToken. */ function swap( uint256 amountIn, - address tokenIn, - uint256 minUserAmount, - bool wrapEth, - bool unwrapEth, + address tokenOut, + uint256 checkAmountOut, + bool wrapEth, // This means ETH is the sell token + bool unwrapEth, // This means ETH is the buy token uint256 nTokens, - bytes calldata swaps, + address receiver, IAllowanceTransfer.PermitSingle calldata permitSingle, - bytes calldata signature + bytes calldata signature, + bytes calldata swaps ) external whenNotPaused returns (uint256 amountOut) { - amountOut = 0; - // TODO + // 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 + ); + } + + amountOut = _splitSwap(amountIn, nTokens, swaps); + + if (fee > 0) { + uint256 feeAmount = (amountOut * fee) / 10000; + amountOut -= feeAmount; + IERC20(tokenOut).safeTransfer(feeReceiver, feeAmount); + } + + if (amountOut < checkAmountOut) { + revert TychoRouter__NegativeSlippage(amountOut, checkAmountOut); + } + + if (unwrapEth) { + _unwrapETH(amountOut); + payable(receiver).transfer(amountOut); + } + } + + function _splitSwap( + uint256 amountIn, + uint256 nTokens, + bytes calldata swaps_ + ) internal returns (uint256) { + uint256 currentAmountIn; + uint256 currentAmountOut; + uint8 tokenInIndex; + uint8 tokenOutIndex; + uint24 split; + bytes calldata swapData; + + uint256[] memory remainingAmounts = new uint256[](nTokens); + uint256[] memory amounts = new uint256[](nTokens); + amounts[0] = amountIn; + remainingAmounts[0] = amountIn; + + while (swaps_.length > 0) { + (swapData, swaps_) = swaps_.next(); + split = swapData.splitPercentage(); + tokenInIndex = swapData.tokenInIndex(); + tokenOutIndex = swapData.tokenOutIndex(); + currentAmountIn = split > 0 + ? (amounts[tokenInIndex] * split) / 0xffffff + : remainingAmounts[tokenInIndex]; + currentAmountOut = + _callExecutor(currentAmountIn, swapData.protocolData()); + + amounts[tokenOutIndex] += currentAmountOut; + remainingAmounts[tokenOutIndex] += currentAmountOut; + remainingAmounts[tokenInIndex] -= currentAmountIn; + } + return amounts[tokenOutIndex]; } /** From 50429ad05cc86bf3816fe2e4b67725cec72519f8 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Mon, 27 Jan 2025 17:10:47 +0000 Subject: [PATCH 3/9] 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 --- foundry/interfaces/IExecutor.sol | 1 + foundry/src/TychoRouter.sol | 64 +++- foundry/src/executors/UniswapV2Executor.sol | 1 + foundry/test/Constants.sol | 12 + foundry/test/TychoRouter.t.sol | 348 +++++++++++++++++- foundry/test/TychoRouterTestSetup.sol | 131 ++++++- .../test/executors/UniswapV2Executor.t.sol | 1 - 7 files changed, 537 insertions(+), 21 deletions(-) diff --git a/foundry/interfaces/IExecutor.sol b/foundry/interfaces/IExecutor.sol index 764623a..9c5a55c 100644 --- a/foundry/interfaces/IExecutor.sol +++ b/foundry/interfaces/IExecutor.sol @@ -22,6 +22,7 @@ interface IExecutor { */ function swap(uint256 givenAmount, bytes calldata data) external + payable returns (uint256 calculatedAmount); } diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index d2badbb..24a1497 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -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; diff --git a/foundry/src/executors/UniswapV2Executor.sol b/foundry/src/executors/UniswapV2Executor.sol index 01a507e..d892122 100644 --- a/foundry/src/executors/UniswapV2Executor.sol +++ b/foundry/src/executors/UniswapV2Executor.sol @@ -12,6 +12,7 @@ contract UniswapV2Executor is IExecutor { function swap(uint256 givenAmount, bytes calldata data) external + payable returns (uint256 calculatedAmount) { address target; diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index ed1b66f..0feba9d 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -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 diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 5458016..37ab4fd 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -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(); + } } diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 345f7f0..d1bb5ea 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -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 + ); + } } diff --git a/foundry/test/executors/UniswapV2Executor.t.sol b/foundry/test/executors/UniswapV2Executor.t.sol index 1598a6a..cdc0c89 100644 --- a/foundry/test/executors/UniswapV2Executor.t.sol +++ b/foundry/test/executors/UniswapV2Executor.t.sol @@ -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; From d8de65aedf459e34911f80da4dc6e44da93aa807 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 28 Jan 2025 10:26:38 +0000 Subject: [PATCH 4/9] fix: Remove checkMinAmount If minAmountOut == 0, then skip the check --- don't change below this line --- ENG-4041 Took 35 minutes --- foundry/src/TychoRouter.sol | 12 ++++-------- foundry/test/TychoRouter.t.sol | 5 ----- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 24a1497..2d43030 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -97,13 +97,12 @@ contract TychoRouter is * - 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. + * - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is bigger than 0. * * @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 minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. If it's 0, no check is performed. * @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). @@ -118,7 +117,6 @@ contract TychoRouter is uint256 amountIn, address tokenIn, address tokenOut, - bool checkMinAmount, uint256 minAmountOut, bool wrapEth, bool unwrapEth, @@ -131,8 +129,7 @@ contract TychoRouter is // For native ETH, assume funds already in our router. Else, transfer and handle approval. if (wrapEth) { _wrapETH(amountIn); - } else { - if (tokenIn != address(0)) { + } else if (tokenIn != address(0)) { permit2.permit(msg.sender, permitSingle, signature); permit2.transferFrom( msg.sender, @@ -140,7 +137,6 @@ contract TychoRouter is uint160(amountIn), permitSingle.details.token ); - } } amountOut = _splitSwap(amountIn, nTokens, swaps); @@ -154,7 +150,7 @@ contract TychoRouter is } } - if (checkMinAmount && amountOut < minAmountOut) { + if (minAmountOut > 0 && amountOut < minAmountOut) { revert TychoRouter__NegativeSlippage(amountOut, minAmountOut); } diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 37ab4fd..7df01bf 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -351,7 +351,6 @@ contract TychoRouterTest is TychoRouterTestSetup { amount_in, WETH_ADDR, DAI_ADDR, - true, minAmountOut, false, false, @@ -403,7 +402,6 @@ contract TychoRouterTest is TychoRouterTestSetup { amount_in, WETH_ADDR, DAI_ADDR, - true, minAmountOut, false, false, @@ -449,7 +447,6 @@ contract TychoRouterTest is TychoRouterTestSetup { amount_in, WETH_ADDR, DAI_ADDR, - false, 0, false, false, @@ -498,7 +495,6 @@ contract TychoRouterTest is TychoRouterTestSetup { amount_in, address(0), DAI_ADDR, - false, 0, true, false, @@ -542,7 +538,6 @@ contract TychoRouterTest is TychoRouterTestSetup { amount_in, DAI_ADDR, address(0), - false, 0, false, true, From dfa0f7d1764c755f4a072fdc26957e1933881384 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 28 Jan 2025 10:27:28 +0000 Subject: [PATCH 5/9] chore: Address compilers warnings and use EXECUTOR_SETTER instead of Bob in tests --- don't change below this line --- ENG-4041 Took 48 seconds Took 18 seconds --- .../src/CallbackVerificationDispatcher.sol | 1 + foundry/src/TychoRouter.sol | 14 +-- .../test/CallbackVerificationDispatcher.t.sol | 3 +- foundry/test/ExecutionDispatcher.t.sol | 2 +- .../LibPrefixLengthEncodedByteArray.t.sol | 20 ++-- foundry/test/TychoRouter.t.sol | 91 ++++++++++--------- foundry/test/TychoRouterTestSetup.sol | 5 +- 7 files changed, 68 insertions(+), 68 deletions(-) diff --git a/foundry/src/CallbackVerificationDispatcher.sol b/foundry/src/CallbackVerificationDispatcher.sol index 0e2a058..48d76d9 100644 --- a/foundry/src/CallbackVerificationDispatcher.sol +++ b/foundry/src/CallbackVerificationDispatcher.sol @@ -49,6 +49,7 @@ contract CallbackVerificationDispatcher { // slither-disable-next-line dead-code function _callVerifyCallback(bytes calldata data) internal + view returns ( uint256 amountOwed, uint256 amountReceived, diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 2d43030..1308868 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -130,13 +130,13 @@ contract TychoRouter is if (wrapEth) { _wrapETH(amountIn); } else if (tokenIn != address(0)) { - permit2.permit(msg.sender, permitSingle, signature); - permit2.transferFrom( - msg.sender, - address(this), - uint160(amountIn), - permitSingle.details.token - ); + permit2.permit(msg.sender, permitSingle, signature); + permit2.transferFrom( + msg.sender, + address(this), + uint160(amountIn), + permitSingle.details.token + ); } amountOut = _splitSwap(amountIn, nTokens, swaps); diff --git a/foundry/test/CallbackVerificationDispatcher.t.sol b/foundry/test/CallbackVerificationDispatcher.t.sol index 8480a96..e672642 100644 --- a/foundry/test/CallbackVerificationDispatcher.t.sol +++ b/foundry/test/CallbackVerificationDispatcher.t.sol @@ -9,6 +9,7 @@ contract CallbackVerificationDispatcherExposed is { function exposedCallVerifier(bytes calldata data) external + view returns ( uint256 amountOwed, uint256 amountReceived, @@ -176,7 +177,7 @@ contract CallbackVerificationDispatcherTest is Constants { dispatcherExposed.exposedCallVerifier(data); } - function testDecodeVerifierAndSelector() public { + function testDecodeVerifierAndSelector() public view { bytes memory data = hex"2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e876b20f8aA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; (address executor, bytes4 selector, bytes memory verifierData) = diff --git a/foundry/test/ExecutionDispatcher.t.sol b/foundry/test/ExecutionDispatcher.t.sol index 4895d03..2e49482 100644 --- a/foundry/test/ExecutionDispatcher.t.sol +++ b/foundry/test/ExecutionDispatcher.t.sol @@ -136,7 +136,7 @@ contract ExecutionDispatcherTest is Constants { dispatcherExposed.exposedCallExecutor(0, data); } - function testDecodeExecutorAndSelector() public { + function testDecodeExecutorAndSelector() public view { bytes memory data = hex"6611e616d2db3244244a54c754a16dd3ac7ca7a2aabbccdd1111111111111111"; (address executor, bytes4 selector, bytes memory protocolData) = diff --git a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol index c06eb47..d56b298 100644 --- a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol +++ b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol @@ -8,14 +8,14 @@ import {LibPrefixLengthEncodedByteArray} from contract LibPrefixLengthEncodedByteArrayTest is Test { using LibPrefixLengthEncodedByteArray for bytes; - function testNextEmpty() public { + function testNextEmpty() public view { bytes memory encoded = ""; (bytes memory elem, bytes memory remaining) = this.next(encoded); assertEq(elem.length, 0); assertEq(remaining.length, 0); } - function testNextSingleElement() public { + function testNextSingleElement() public view { // Create encoded data: length prefix (0003) followed by "ABC" bytes memory encoded = hex"0003414243"; (bytes memory elem, bytes memory remaining) = this.next(encoded); @@ -25,7 +25,7 @@ contract LibPrefixLengthEncodedByteArrayTest is Test { assertEq(remaining.length, 0); } - function testNextMultipleElements() public { + function testNextMultipleElements() public view { // Encoded data: [0003]ABC[0002]DE bytes memory encoded = hex"000341424300024445"; @@ -40,7 +40,7 @@ contract LibPrefixLengthEncodedByteArrayTest is Test { assertEq(remaining2.length, 0); } - function testSize() public { + function testSize() public view { bytes memory empty = ""; assertEq(this.size(empty), 0); @@ -51,19 +51,19 @@ contract LibPrefixLengthEncodedByteArrayTest is Test { assertEq(this.size(multiple), 3); } - function testFailInvalidLength() public { + function testFailInvalidLength() public view { // Length prefix larger than remaining data bytes memory invalid = hex"0004414243"; - (bytes memory elem, bytes memory remaining) = this.next(invalid); + this.next(invalid); } - function testFailIncompletePrefix() public { + function testFailIncompletePrefix() public view { // Only 1 byte instead of 2 bytes prefix bytes memory invalid = hex"01"; - (bytes memory elem, bytes memory remaining) = this.next(invalid); + this.next(invalid); } - function testLargeElement() public { + function testLargeElement() public view { // Test with a large but manageable size (1000 bytes) bytes memory large = new bytes(1002); // 2 bytes prefix + 1000 bytes data large[0] = bytes1(uint8(0x03)); // 03 @@ -79,7 +79,7 @@ contract LibPrefixLengthEncodedByteArrayTest is Test { assertEq(remaining.length, 0); } - function testSizeWithLargeElements() public { + function testSizeWithLargeElements() public view { // Two elements: 1000 bytes + 500 bytes bytes memory data = new bytes(1504); // 1000 + 2 + 500 + 2 diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 7df01bf..9f3b1b9 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -20,14 +20,14 @@ contract TychoRouterTest is TychoRouterTestSetup { ); function testSetExecutorValidRole() public { - vm.startPrank(executorSetter); + vm.startPrank(EXECUTOR_SETTER); tychoRouter.setExecutor(DUMMY); vm.stopPrank(); assert(tychoRouter.executors(DUMMY) == true); } function testRemoveExecutorValidRole() public { - vm.startPrank(executorSetter); + vm.startPrank(EXECUTOR_SETTER); tychoRouter.setExecutor(DUMMY); tychoRouter.removeExecutor(DUMMY); vm.stopPrank(); @@ -45,14 +45,14 @@ contract TychoRouterTest is TychoRouterTestSetup { } function testSetVerifierValidRole() public { - vm.startPrank(executorSetter); + vm.startPrank(EXECUTOR_SETTER); tychoRouter.setCallbackVerifier(DUMMY); vm.stopPrank(); assert(tychoRouter.callbackVerifiers(DUMMY) == true); } function testRemoveVerifierValidRole() public { - vm.startPrank(executorSetter); + vm.startPrank(EXECUTOR_SETTER); tychoRouter.setCallbackVerifier(DUMMY); tychoRouter.removeCallbackVerifier(DUMMY); vm.stopPrank(); @@ -216,8 +216,8 @@ contract TychoRouterTest is TychoRouterTestSetup { // 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); + uint256 amountIn = 1 ether; + deal(WETH_ADDR, address(tychoRouter), amountIn); bytes memory protocolData = encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false); @@ -227,9 +227,7 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - uint256 minAmountOut = 2600 * 1e18; - uint256 amountOut = - tychoRouter.splitSwap(amount_in, 2, pleEncode(swaps)); + tychoRouter.splitSwap(amountIn, 2, pleEncode(swaps)); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); assertEq(daiBalance, 2630432278145144658455); @@ -240,8 +238,8 @@ contract TychoRouterTest is TychoRouterTestSetup { // 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); + uint256 amountIn = 1 ether; + deal(WETH_ADDR, address(tychoRouter), amountIn); bytes[] memory swaps = new bytes[](2); // WETH -> DAI @@ -262,9 +260,7 @@ contract TychoRouterTest is TychoRouterTestSetup { encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, ALICE, true) ); - uint256 minAmountOut = 2600 * 1e6; - uint256 amountOut = - tychoRouter.splitSwap(amount_in, 3, pleEncode(swaps)); + tychoRouter.splitSwap(amountIn, 3, pleEncode(swaps)); uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE); assertEq(usdcBalance, 2610580090); @@ -277,8 +273,8 @@ contract TychoRouterTest is TychoRouterTestSetup { // 1 WETH USDC // -> WBTC -> // (univ2) (univ2) - uint256 amount_in = 1 ether; - deal(WETH_ADDR, address(tychoRouter), amount_in); + uint256 amountIn = 1 ether; + deal(WETH_ADDR, address(tychoRouter), amountIn); bytes[] memory swaps = new bytes[](4); // WETH -> WBTC (60%) @@ -315,9 +311,7 @@ contract TychoRouterTest is TychoRouterTestSetup { encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, ALICE, true) ); - uint256 minAmountOut = 2580 * 1e6; - uint256 amountOut = - tychoRouter.splitSwap(amount_in, 4, pleEncode(swaps)); + tychoRouter.splitSwap(amountIn, 4, pleEncode(swaps)); uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE); assertEq(usdcBalance, 2581503157); @@ -328,15 +322,15 @@ contract TychoRouterTest is TychoRouterTestSetup { // 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); + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); vm.startPrank(ALICE); ( IAllowanceTransfer.PermitSingle memory permitSingle, bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amount_in); + ) = handlePermit2Approval(WETH_ADDR, amountIn); bytes memory protocolData = encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false); @@ -348,7 +342,7 @@ contract TychoRouterTest is TychoRouterTestSetup { uint256 minAmountOut = 2600 * 1e18; uint256 amountOut = tychoRouter.swap( - amount_in, + amountIn, WETH_ADDR, DAI_ADDR, minAmountOut, @@ -361,8 +355,10 @@ contract TychoRouterTest is TychoRouterTestSetup { pleEncode(swaps) ); + uint256 expectedAmount = 2630432278145144658455; + assertEq(amountOut, expectedAmount); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, 2630432278145144658455); + assertEq(daiBalance, expectedAmount); assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); vm.stopPrank(); @@ -372,15 +368,15 @@ contract TychoRouterTest is TychoRouterTestSetup { // 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); + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); vm.startPrank(ALICE); ( IAllowanceTransfer.PermitSingle memory permitSingle, bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amount_in); + ) = handlePermit2Approval(WETH_ADDR, amountIn); bytes memory protocolData = encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false); @@ -398,8 +394,8 @@ contract TychoRouterTest is TychoRouterTestSetup { minAmountOut ) ); - uint256 amountOut = tychoRouter.swap( - amount_in, + tychoRouter.swap( + amountIn, WETH_ADDR, DAI_ADDR, minAmountOut, @@ -424,15 +420,15 @@ contract TychoRouterTest is TychoRouterTestSetup { tychoRouter.setFeeReceiver(FEE_RECEIVER); vm.stopPrank(); - uint256 amount_in = 1 ether; - deal(WETH_ADDR, ALICE, amount_in); + uint256 amountIn = 1 ether; + deal(WETH_ADDR, ALICE, amountIn); vm.startPrank(ALICE); ( IAllowanceTransfer.PermitSingle memory permitSingle, bytes memory signature - ) = handlePermit2Approval(WETH_ADDR, amount_in); + ) = handlePermit2Approval(WETH_ADDR, amountIn); bytes memory protocolData = encodeUniswapV2Swap( WETH_ADDR, WETH_DAI_POOL, address(tychoRouter), false @@ -444,7 +440,7 @@ contract TychoRouterTest is TychoRouterTestSetup { swaps[0] = swap; uint256 amountOut = tychoRouter.swap( - amount_in, + amountIn, WETH_ADDR, DAI_ADDR, 0, @@ -457,8 +453,10 @@ contract TychoRouterTest is TychoRouterTestSetup { pleEncode(swaps) ); + uint256 expectedAmount = 2604127955363693211871; + assertEq(amountOut, expectedAmount); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, 2604127955363693211871); + assertEq(daiBalance, expectedAmount); assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26304322781451446584); vm.stopPrank(); @@ -467,8 +465,8 @@ contract TychoRouterTest is TychoRouterTestSetup { 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); + uint256 amountIn = 1 ether; + deal(ALICE, amountIn); vm.startPrank(ALICE); @@ -491,8 +489,8 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - uint256 amountOut = tychoRouter.swap{value: amount_in}( - amount_in, + uint256 amountOut = tychoRouter.swap{value: amountIn}( + amountIn, address(0), DAI_ADDR, 0, @@ -504,9 +502,10 @@ contract TychoRouterTest is TychoRouterTestSetup { "", pleEncode(swaps) ); - + uint256 expectedAmount = 2630432278145144658455; + assertEq(amountOut, expectedAmount); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); - assertEq(daiBalance, 2630432278145144658455); + assertEq(daiBalance, expectedAmount); assertEq(ALICE.balance, 0); vm.stopPrank(); @@ -515,15 +514,15 @@ contract TychoRouterTest is TychoRouterTestSetup { 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); + uint256 amountIn = 3_000 * 10 ** 18; + deal(DAI_ADDR, ALICE, amountIn); vm.startPrank(ALICE); ( IAllowanceTransfer.PermitSingle memory permitSingle, bytes memory signature - ) = handlePermit2Approval(DAI_ADDR, amount_in); + ) = handlePermit2Approval(DAI_ADDR, amountIn); bytes memory protocolData = encodeUniswapV2Swap( DAI_ADDR, WETH_DAI_POOL, address(tychoRouter), true @@ -535,7 +534,7 @@ contract TychoRouterTest is TychoRouterTestSetup { swaps[0] = swap; uint256 amountOut = tychoRouter.swap( - amount_in, + amountIn, DAI_ADDR, address(0), 0, @@ -548,7 +547,9 @@ contract TychoRouterTest is TychoRouterTestSetup { pleEncode(swaps) ); - assertEq(ALICE.balance, 1132829934891544187); // 1.13 ETH + uint256 expectedAmount = 1132829934891544187; // 1.13 ETH + assertEq(amountOut, expectedAmount); + assertEq(ALICE.balance, expectedAmount); vm.stopPrank(); } diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index d1bb5ea..45212f8 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -28,7 +28,6 @@ contract TychoRouterExposed is TychoRouter { contract TychoRouterTestSetup is Test, Constants { TychoRouterExposed tychoRouter; - address executorSetter; address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3); UniswapV2Executor public usv2Executor; MockERC20[] tokens; @@ -39,7 +38,6 @@ contract TychoRouterTestSetup is Test, Constants { vm.startPrank(ADMIN); tychoRouter = new TychoRouterExposed(permit2Address, WETH_ADDR); - tychoRouter.grantRole(keccak256("EXECUTOR_SETTER_ROLE"), BOB); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER); tychoRouter.grantRole(keccak256("PAUSER_ROLE"), PAUSER); @@ -47,7 +45,6 @@ contract TychoRouterTestSetup is Test, Constants { tychoRouter.grantRole( keccak256("EXECUTOR_SETTER_ROLE"), EXECUTOR_SETTER ); - executorSetter = BOB; deployDummyContract(); vm.stopPrank(); @@ -118,7 +115,7 @@ contract TychoRouterTestSetup is Test, Constants { function signPermit2( IAllowanceTransfer.PermitSingle memory permit, uint256 privateKey - ) internal returns (bytes memory) { + ) internal view returns (bytes memory) { bytes32 _PERMIT_DETAILS_TYPEHASH = keccak256( "PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" ); From c2347ac79ec670615de5f6b90982670d9bb739ed Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 28 Jan 2025 12:19:07 +0000 Subject: [PATCH 6/9] feat: Add executor and selector to Swap Add tests to Swap Modify ExecutionDispatcher and TychoRouter to account for these changes --- don't change below this line --- ENG-4041 Took 57 minutes Took 10 seconds --- foundry/{src/Swap.sol => lib/LibSwap.sol} | 22 ++++++- foundry/src/ExecutionDispatcher.sol | 35 +++-------- foundry/src/TychoRouter.sol | 12 ++-- foundry/test/ExecutionDispatcher.t.sol | 67 ++++++++++----------- foundry/test/LibSwap.t.sol | 41 +++++++++++++ foundry/test/TychoRouter.t.sol | 72 +++++++++++++++++++---- foundry/test/TychoRouterTestSetup.sol | 13 ++-- 7 files changed, 176 insertions(+), 86 deletions(-) rename foundry/{src/Swap.sol => lib/LibSwap.sol} (65%) create mode 100644 foundry/test/LibSwap.t.sol diff --git a/foundry/src/Swap.sol b/foundry/lib/LibSwap.sol similarity index 65% rename from foundry/src/Swap.sol rename to foundry/lib/LibSwap.sol index 7c78277..f581d8c 100644 --- a/foundry/src/Swap.sol +++ b/foundry/lib/LibSwap.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; -library Swap { +library LibSwap { /// Returns the InToken index into an array of tokens function tokenInIndex(bytes calldata swap) internal @@ -29,12 +29,30 @@ library Swap { res = uint24(bytes3(swap[2:5])); } + /// The address of the executor contract + function executor(bytes calldata swap) + internal + pure + returns (address res) + { + res = address(uint160(bytes20(swap[5:25]))); + } + + /// The selector to be used of the executor contract + function executorSelector(bytes calldata swap) + internal + pure + returns (bytes4 res) + { + res = bytes4(swap[25:29]); + } + /// Remaining bytes are interpreted as protocol data function protocolData(bytes calldata swap) internal pure returns (bytes calldata res) { - res = swap[5:]; + res = swap[29:]; } } diff --git a/foundry/src/ExecutionDispatcher.sol b/foundry/src/ExecutionDispatcher.sol index 090368e..8a80d74 100644 --- a/foundry/src/ExecutionDispatcher.sol +++ b/foundry/src/ExecutionDispatcher.sol @@ -51,28 +51,21 @@ contract ExecutionDispatcher { * protocol-specific data required by the executor. */ // slither-disable-next-line dead-code - function _callExecutor(uint256 amount, bytes calldata data) - internal - returns (uint256 calculatedAmount) - { - address executor; - bytes4 decodedSelector; - bytes memory protocolData; - - (executor, decodedSelector, protocolData) = - _decodeExecutorAndSelector(data); - + function _callExecutor( + address executor, + bytes4 selector, + uint256 amount, + bytes calldata data + ) internal returns (uint256 calculatedAmount) { if (!executors[executor]) { revert ExecutionDispatcher__UnapprovedExecutor(); } - bytes4 selector = decodedSelector == bytes4(0) - ? IExecutor.swap.selector - : decodedSelector; + selector = selector == bytes4(0) ? IExecutor.swap.selector : selector; // slither-disable-next-line low-level-calls (bool success, bytes memory result) = executor.delegatecall( - abi.encodeWithSelector(selector, amount, protocolData) + abi.encodeWithSelector(selector, amount, data) ); if (!success) { @@ -87,16 +80,4 @@ contract ExecutionDispatcher { calculatedAmount = abi.decode(result, (uint256)); } - - // slither-disable-next-line dead-code - function _decodeExecutorAndSelector(bytes calldata data) - internal - pure - returns (address executor, bytes4 selector, bytes memory protocolData) - { - require(data.length >= 24, "Invalid data length"); - executor = address(uint160(bytes20(data[:20]))); - selector = bytes4(data[20:24]); - protocolData = data[24:]; - } } diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 1308868..880aa77 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -11,7 +11,7 @@ import "@permit2/src/interfaces/IAllowanceTransfer.sol"; import "./ExecutionDispatcher.sol"; import "./CallbackVerificationDispatcher.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; -import {Swap} from "./Swap.sol"; +import {LibSwap} from "../lib/LibSwap.sol"; error TychoRouter__WithdrawalFailed(); error TychoRouter__AddressZero(); @@ -29,7 +29,7 @@ contract TychoRouter is using SafeERC20 for IERC20; using LibPrefixLengthEncodedByteArray for bytes; - using Swap for bytes; + using LibSwap for bytes; //keccak256("NAME_OF_ROLE") : save gas on deployment bytes32 public constant EXECUTOR_SETTER_ROLE = @@ -185,8 +185,12 @@ contract TychoRouter is currentAmountIn = split > 0 ? (amounts[tokenInIndex] * split) / 0xffffff : remainingAmounts[tokenInIndex]; - currentAmountOut = - _callExecutor(currentAmountIn, swapData.protocolData()); + currentAmountOut = _callExecutor( + swapData.executor(), + swapData.executorSelector(), + currentAmountIn, + swapData.protocolData() + ); amounts[tokenOutIndex] += currentAmountOut; remainingAmounts[tokenOutIndex] += currentAmountOut; remainingAmounts[tokenInIndex] -= currentAmountIn; diff --git a/foundry/test/ExecutionDispatcher.t.sol b/foundry/test/ExecutionDispatcher.t.sol index 2e49482..a88dc44 100644 --- a/foundry/test/ExecutionDispatcher.t.sol +++ b/foundry/test/ExecutionDispatcher.t.sol @@ -5,19 +5,13 @@ import "@src/ExecutionDispatcher.sol"; import "./TychoRouterTestSetup.sol"; contract ExecutionDispatcherExposed is ExecutionDispatcher { - function exposedCallExecutor(uint256 amount, bytes calldata data) - external - returns (uint256 calculatedAmount) - { - return _callExecutor(amount, data); - } - - function exposedDecodeExecutorAndSelector(bytes calldata data) - external - pure - returns (address executor, bytes4 selector, bytes memory protocolData) - { - return _decodeExecutorAndSelector(data); + function exposedCallExecutor( + address executor, + bytes4 selector, + uint256 amount, + bytes calldata data + ) external returns (uint256 calculatedAmount) { + return _callExecutor(executor, selector, amount, data); } function exposedSetExecutor(address target) external { @@ -88,10 +82,14 @@ contract ExecutionDispatcherTest is Constants { address(0xe592557AB9F4A75D992283fD6066312FF013ba3d) ); bytes memory data = - hex"e592557AB9F4A75D992283fD6066312FF013ba3dbd0625ab5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72fc8c39af7983bf329086de522229a7be5fc4e41cc51c72848c68a965f66fa7a88855f9f7784502a7f2606beffe61000613d6a25b5bfef4cd7652aa94777d4a46b39f2e206411280a12c9344b769ff1066c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000082ec8ad1b0000000000000000000000000000000000000000000000000000000066d7b65800000000000000000000000000000000000000000000000000000191ba9f843c125000064000640000d52de09955f0ffffffffffffff00225c389e595fe9000001fcc910754b349f821e4bb5d8444822a63920be943aba6f1b31ee14ef0fc6840b6d28d604e04a78834b668dba24a6c082ffb901e4fffa9600649e8d991af593c81c"; + hex"5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72fc8c39af7983bf329086de522229a7be5fc4e41cc51c72848c68a965f66fa7a88855f9f7784502a7f2606beffe61000613d6a25b5bfef4cd7652aa94777d4a46b39f2e206411280a12c9344b769ff1066c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000082ec8ad1b0000000000000000000000000000000000000000000000000000000066d7b65800000000000000000000000000000000000000000000000000000191ba9f843c125000064000640000d52de09955f0ffffffffffffff00225c389e595fe9000001fcc910754b349f821e4bb5d8444822a63920be943aba6f1b31ee14ef0fc6840b6d28d604e04a78834b668dba24a6c082ffb901e4fffa9600649e8d991af593c81c"; uint256 givenAmount = 15 ether; - uint256 amount = - dispatcherExposed.exposedCallExecutor(givenAmount, data); + uint256 amount = dispatcherExposed.exposedCallExecutor( + 0xe592557AB9F4A75D992283fD6066312FF013ba3d, + IExecutor.swap.selector, + givenAmount, + data + ); assert(amount == 35144641819); } @@ -111,10 +109,14 @@ contract ExecutionDispatcherTest is Constants { address(0xe592557AB9F4A75D992283fD6066312FF013ba3d) ); bytes memory data = - hex"e592557AB9F4A75D992283fD6066312FF013ba3d000000005615dEB798BB3E4dFa0139dFa1b3D433Cc23b72fc8c39af7983bf329086de522229a7be5fc4e41cc51c72848c68a965f66fa7a88855f9f7784502a7f2606beffe61000613d6a25b5bfef4cd7652aa94777d4a46b39f2e206411280a12c9344b769ff1066c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000082ec8ad1b0000000000000000000000000000000000000000000000000000000066d7b65800000000000000000000000000000000000000000000000000000191ba9f843c125000064000640000d52de09955f0ffffffffffffff00225c389e595fe9000001fcc910754b349f821e4bb5d8444822a63920be943aba6f1b31ee14ef0fc6840b6d28d604e04a78834b668dba24a6c082ffb901e4fffa9600649e8d991af593c81c"; + hex"5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72fc8c39af7983bf329086de522229a7be5fc4e41cc51c72848c68a965f66fa7a88855f9f7784502a7f2606beffe61000613d6a25b5bfef4cd7652aa94777d4a46b39f2e206411280a12c9344b769ff1066c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000082ec8ad1b0000000000000000000000000000000000000000000000000000000066d7b65800000000000000000000000000000000000000000000000000000191ba9f843c125000064000640000d52de09955f0ffffffffffffff00225c389e595fe9000001fcc910754b349f821e4bb5d8444822a63920be943aba6f1b31ee14ef0fc6840b6d28d604e04a78834b668dba24a6c082ffb901e4fffa9600649e8d991af593c81c"; uint256 givenAmount = 15 ether; - uint256 amount = - dispatcherExposed.exposedCallExecutor(givenAmount, data); + uint256 amount = dispatcherExposed.exposedCallExecutor( + 0xe592557AB9F4A75D992283fD6066312FF013ba3d, + bytes4(0), + givenAmount, + data + ); assert(amount == 35144641819); } @@ -124,26 +126,21 @@ contract ExecutionDispatcherTest is Constants { address(0xe592557AB9F4A75D992283fD6066312FF013ba3d) ); bytes memory data = - hex"e592557AB9F4A75D992283fD6066312FF013ba3dbd0625ab5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72fc8c39af7983bf329086de522229a7be5fc4e41cc51c72848c68a965f66fa7a88855f9f7784502a7f2606beffe61000613d6a25b5bfef4cd7652aa94777d4a46b39f2e206411280a12c9344b769ff1066c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000082ec8ad1b0000000000000000000000000000000000000000000000000000000066d7b65800000000000000000000000000000000000000000000000000000191ba9f843c125000064000640000d52de09955f0ffffffffffffff00225c389e595fe9000001fcc910754b349f821e4bb5d8444822a63920be943aba6f1b31ee14ef0fc6840b6d28d604e04a78834b668dba24a6c082ffb901e4fffa9600649e8d991af593"; + hex"5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72fc8c39af7983bf329086de522229a7be5fc4e41cc51c72848c68a965f66fa7a88855f9f7784502a7f2606beffe61000613d6a25b5bfef4cd7652aa94777d4a46b39f2e206411280a12c9344b769ff1066c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000082ec8ad1b0000000000000000000000000000000000000000000000000000000066d7b65800000000000000000000000000000000000000000000000000000191ba9f843c125000064000640000d52de09955f0ffffffffffffff00225c389e595fe9000001fcc910754b349f821e4bb5d8444822a63920be943aba6f1b31ee14ef0fc6840b6d28d604e04a78834b668dba24a6c082ffb901e4fffa9600649e8d991af593"; vm.expectRevert(); - dispatcherExposed.exposedCallExecutor(0, data); + dispatcherExposed.exposedCallExecutor( + 0xe592557AB9F4A75D992283fD6066312FF013ba3d, + IExecutor.swap.selector, + 0, + data + ); } function testCallExecutorUnapprovedExecutor() public { - bytes memory data = - hex"5d622C9053b8FFB1B3465495C8a42E603632bA70aabbccdd1111111111111111"; + bytes memory data = hex"aabbccdd1111111111111111"; vm.expectRevert(); - dispatcherExposed.exposedCallExecutor(0, data); - } - - function testDecodeExecutorAndSelector() public view { - bytes memory data = - hex"6611e616d2db3244244a54c754a16dd3ac7ca7a2aabbccdd1111111111111111"; - (address executor, bytes4 selector, bytes memory protocolData) = - dispatcherExposed.exposedDecodeExecutorAndSelector(data); - assert(executor == address(0x6611e616d2db3244244A54c754A16dd3ac7cA7a2)); - assert(selector == bytes4(0xaabbccdd)); - // Direct bytes comparison not supported - must use keccak - assert(keccak256(protocolData) == keccak256(hex"1111111111111111")); + dispatcherExposed.exposedCallExecutor( + 0x5d622C9053b8FFB1B3465495C8a42E603632bA70, bytes4(0), 0, data + ); } } diff --git a/foundry/test/LibSwap.t.sol b/foundry/test/LibSwap.t.sol new file mode 100644 index 0000000..eedc931 --- /dev/null +++ b/foundry/test/LibSwap.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import "forge-std/Test.sol"; +import "../lib/LibSwap.sol"; + +contract LibSwapTest is Test { + using LibSwap for bytes; + + function testSwap() public view { + uint8 tokenInIndex = 1; + uint8 tokenOutIndex = 2; + uint24 split = 3; + address executor = 0x1234567890123456789012345678901234567890; + bytes4 selector = 0x12345678; + bytes memory protocolData = abi.encodePacked(uint256(456)); + + bytes memory swap = abi.encodePacked( + tokenInIndex, tokenOutIndex, split, executor, selector, protocolData + ); + this.assertSwap( + swap, tokenInIndex, tokenOutIndex, split, executor, selector + ); + } + + // This is necessary so that the compiler accepts bytes as a LibSwap.sol + function assertSwap( + bytes calldata swap, + uint8 tokenInIndex, + uint8 tokenOutIndex, + uint24 split, + address executor, + bytes4 selector + ) public pure { + assert(swap.tokenInIndex() == tokenInIndex); + assert(swap.tokenOutIndex() == tokenOutIndex); + assert(swap.splitPercentage() == split); + assert(swap.executor() == executor); + assert(swap.executorSelector() == selector); + } +} diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 9f3b1b9..caad604 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -222,8 +222,14 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes memory protocolData = encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false); - bytes memory swap = - encodeSwap(uint8(0), uint8(1), uint24(0), protocolData); + bytes memory swap = encodeSwap( + uint8(0), + uint8(1), + uint24(0), + address(usv2Executor), + bytes4(0), + protocolData + ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; @@ -247,6 +253,8 @@ contract TychoRouterTest is TychoRouterTestSetup { uint8(0), uint8(1), uint24(0), + address(usv2Executor), + bytes4(0), encodeUniswapV2Swap( WETH_ADDR, WETH_DAI_POOL, address(tychoRouter), false ) @@ -257,6 +265,8 @@ contract TychoRouterTest is TychoRouterTestSetup { uint8(1), uint8(2), uint24(0), + address(usv2Executor), + bytes4(0), encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, ALICE, true) ); @@ -282,6 +292,8 @@ contract TychoRouterTest is TychoRouterTestSetup { uint8(0), uint8(1), (0xffffff * 60) / 100, // 60% + address(usv2Executor), + bytes4(0), encodeUniswapV2Swap( WETH_ADDR, WETH_WBTC_POOL, address(tychoRouter), false ) @@ -291,6 +303,8 @@ contract TychoRouterTest is TychoRouterTestSetup { uint8(1), uint8(2), uint24(0), + address(usv2Executor), + bytes4(0), encodeUniswapV2Swap(WBTC_ADDR, USDC_WBTC_POOL, ALICE, true) ); // WETH -> DAI @@ -298,6 +312,8 @@ contract TychoRouterTest is TychoRouterTestSetup { uint8(0), uint8(3), uint24(0), + address(usv2Executor), + bytes4(0), encodeUniswapV2Swap( WETH_ADDR, WETH_DAI_POOL, address(tychoRouter), false ) @@ -308,6 +324,8 @@ contract TychoRouterTest is TychoRouterTestSetup { uint8(3), uint8(2), uint24(0), + address(usv2Executor), + bytes4(0), encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, ALICE, true) ); @@ -335,8 +353,14 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes memory protocolData = encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false); - bytes memory swap = - encodeSwap(uint8(0), uint8(1), uint24(0), protocolData); + bytes memory swap = encodeSwap( + uint8(0), + uint8(1), + uint24(0), + address(usv2Executor), + bytes4(0), + protocolData + ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; @@ -381,8 +405,14 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes memory protocolData = encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false); - bytes memory swap = - encodeSwap(uint8(0), uint8(1), uint24(0), protocolData); + bytes memory swap = encodeSwap( + uint8(0), + uint8(1), + uint24(0), + address(usv2Executor), + bytes4(0), + protocolData + ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; @@ -434,8 +464,14 @@ contract TychoRouterTest is TychoRouterTestSetup { WETH_ADDR, WETH_DAI_POOL, address(tychoRouter), false ); - bytes memory swap = - encodeSwap(uint8(0), uint8(1), uint24(0), protocolData); + bytes memory swap = encodeSwap( + uint8(0), + uint8(1), + uint24(0), + address(usv2Executor), + bytes4(0), + protocolData + ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; @@ -484,8 +520,14 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes memory protocolData = encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false); - bytes memory swap = - encodeSwap(uint8(0), uint8(1), uint24(0), protocolData); + bytes memory swap = encodeSwap( + uint8(0), + uint8(1), + uint24(0), + address(usv2Executor), + bytes4(0), + protocolData + ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; @@ -528,8 +570,14 @@ contract TychoRouterTest is TychoRouterTestSetup { DAI_ADDR, WETH_DAI_POOL, address(tychoRouter), true ); - bytes memory swap = - encodeSwap(uint8(0), uint8(1), uint24(0), protocolData); + bytes memory swap = encodeSwap( + uint8(0), + uint8(1), + uint24(0), + address(usv2Executor), + bytes4(0), + protocolData + ); bytes[] memory swaps = new bytes[](1); swaps[0] = swap; diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 45212f8..f80ea11 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -167,10 +167,13 @@ contract TychoRouterTestSetup is Test, Constants { uint8 tokenInIndex, uint8 tokenOutIndex, uint24 split, + address executor, + bytes4 selector, bytes memory protocolData ) internal pure returns (bytes memory) { - return - abi.encodePacked(tokenInIndex, tokenOutIndex, split, protocolData); + return abi.encodePacked( + tokenInIndex, tokenOutIndex, split, executor, selector, protocolData + ); } function encodeUniswapV2Swap( @@ -178,9 +181,7 @@ contract TychoRouterTestSetup is Test, Constants { address target, address receiver, bool zero2one - ) internal view returns (bytes memory) { - return abi.encodePacked( - usv2Executor, bytes4(0), tokenIn, target, receiver, zero2one - ); + ) internal pure returns (bytes memory) { + return abi.encodePacked(tokenIn, target, receiver, zero2one); } } From dfa7033d2e1aa2f2845335d29d6142cc9a7ac5f1 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 28 Jan 2025 13:03:04 +0000 Subject: [PATCH 7/9] feat: Smother slither and add a reentrancy guard in swap() --- don't change below this line --- ENG-4041 Took 34 minutes --- foundry/src/ExecutionDispatcher.sol | 5 ++--- foundry/src/TychoRouter.sol | 13 +++++++++---- foundry/src/executors/UniswapV2Executor.sol | 1 + 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/foundry/src/ExecutionDispatcher.sol b/foundry/src/ExecutionDispatcher.sol index 8a80d74..e8b36e8 100644 --- a/foundry/src/ExecutionDispatcher.sol +++ b/foundry/src/ExecutionDispatcher.sol @@ -50,7 +50,7 @@ contract ExecutionDispatcher { * @dev Calls an executor, assumes swap.protocolData contains * protocol-specific data required by the executor. */ - // slither-disable-next-line dead-code + // slither-disable-next-line delegatecall-loop function _callExecutor( address executor, bytes4 selector, @@ -62,8 +62,7 @@ contract ExecutionDispatcher { } selector = selector == bytes4(0) ? IExecutor.swap.selector : selector; - - // slither-disable-next-line low-level-calls + // slither-disable-next-line controlled-delegatecall,low-level-calls (bool success, bytes memory result) = executor.delegatecall( abi.encodeWithSelector(selector, amount, data) ); diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 880aa77..616d5c0 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -7,10 +7,11 @@ import "./CallbackVerificationDispatcher.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; import "@permit2/src/interfaces/IAllowanceTransfer.sol"; import "./ExecutionDispatcher.sol"; import "./CallbackVerificationDispatcher.sol"; -import "@openzeppelin/contracts/utils/Pausable.sol"; import {LibSwap} from "../lib/LibSwap.sol"; error TychoRouter__WithdrawalFailed(); @@ -22,7 +23,8 @@ contract TychoRouter is AccessControl, ExecutionDispatcher, CallbackVerificationDispatcher, - Pausable + Pausable, + ReentrancyGuard { IAllowanceTransfer public immutable permit2; IWETH private immutable _weth; @@ -125,7 +127,9 @@ contract TychoRouter is IAllowanceTransfer.PermitSingle calldata permitSingle, bytes calldata signature, bytes calldata swaps - ) external payable whenNotPaused returns (uint256 amountOut) { + ) external payable whenNotPaused nonReentrant returns (uint256 amountOut) { + require(receiver != address(0), "Invalid receiver address"); + // For native ETH, assume funds already in our router. Else, transfer and handle approval. if (wrapEth) { _wrapETH(amountIn); @@ -145,7 +149,7 @@ contract TychoRouter is uint256 feeAmount = (amountOut * fee) / 10000; amountOut -= feeAmount; IERC20(tokenOut).safeTransfer(feeReceiver, feeAmount); - if (unwrapEth == false) { + if (!unwrapEth) { IERC20(tokenOut).safeTransfer(receiver, amountOut); } } @@ -156,6 +160,7 @@ contract TychoRouter is if (unwrapEth) { _unwrapETH(amountOut); + // slither-disable-next-line arbitrary-send-eth payable(receiver).transfer(amountOut); } } diff --git a/foundry/src/executors/UniswapV2Executor.sol b/foundry/src/executors/UniswapV2Executor.sol index d892122..9f447f1 100644 --- a/foundry/src/executors/UniswapV2Executor.sol +++ b/foundry/src/executors/UniswapV2Executor.sol @@ -10,6 +10,7 @@ error UniswapV2Executor__InvalidDataLength(); contract UniswapV2Executor is IExecutor { using SafeERC20 for IERC20; + // slither-disable-next-line locked-ether function swap(uint256 givenAmount, bytes calldata data) external payable From 655cf91984fb568c5ff02efd498d093155c4e33d Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 28 Jan 2025 16:13:24 +0000 Subject: [PATCH 8/9] feat: Assume that funds will never go straight from a pool to the receiver - The funds will always go through the router - Rename splitSwap for Swap - Improve tests --- don't change below this line --- ENG-4041 Took 24 minutes Took 23 seconds Took 42 seconds --- foundry/lib/openzeppelin-contracts | 2 +- foundry/lib/permit2 | 2 +- foundry/lib/v2-core | 2 +- foundry/src/TychoRouter.sol | 16 ++--- foundry/test/TychoRouter.t.sol | 95 ++++++++++++++------------- foundry/test/TychoRouterTestSetup.sol | 15 +++-- 6 files changed, 69 insertions(+), 63 deletions(-) diff --git a/foundry/lib/openzeppelin-contracts b/foundry/lib/openzeppelin-contracts index acd4ff7..293a3e2 160000 --- a/foundry/lib/openzeppelin-contracts +++ b/foundry/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit acd4ff74de833399287ed6b31b4debf6b2b35527 +Subproject commit 293a3e264ef2d33526bcc5e8a304887ec4966fdc diff --git a/foundry/lib/permit2 b/foundry/lib/permit2 index cc56ad0..5834a2d 160000 --- a/foundry/lib/permit2 +++ b/foundry/lib/permit2 @@ -1 +1 @@ -Subproject commit cc56ad0f3439c502c246fc5cfcc3db92bb8b7219 +Subproject commit 5834a2d12cc85df5b706eaaea02ef74e89738231 diff --git a/foundry/lib/v2-core b/foundry/lib/v2-core index 4dd5906..ee547b1 160000 --- a/foundry/lib/v2-core +++ b/foundry/lib/v2-core @@ -1 +1 @@ -Subproject commit 4dd59067c76dea4a0e8e4bfdda41877a6b16dedc +Subproject commit ee547b17853e71ed4e0101ccfd52e70d5acded58 diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 616d5c0..bac2f47 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -143,15 +143,12 @@ contract TychoRouter is ); } - amountOut = _splitSwap(amountIn, nTokens, swaps); + amountOut = _swap(amountIn, nTokens, swaps); if (fee > 0) { uint256 feeAmount = (amountOut * fee) / 10000; amountOut -= feeAmount; IERC20(tokenOut).safeTransfer(feeReceiver, feeAmount); - if (!unwrapEth) { - IERC20(tokenOut).safeTransfer(receiver, amountOut); - } } if (minAmountOut > 0 && amountOut < minAmountOut) { @@ -162,14 +159,15 @@ contract TychoRouter is _unwrapETH(amountOut); // slither-disable-next-line arbitrary-send-eth payable(receiver).transfer(amountOut); + } else { + IERC20(tokenOut).safeTransfer(receiver, amountOut); } } - function _splitSwap( - uint256 amountIn, - uint256 nTokens, - bytes calldata swaps_ - ) internal returns (uint256) { + function _swap(uint256 amountIn, uint256 nTokens, bytes calldata swaps_) + internal + returns (uint256) + { uint256 currentAmountIn; uint256 currentAmountOut; uint8 tokenInIndex; diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index caad604..d736494 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -72,19 +72,19 @@ contract TychoRouterTest is TychoRouterTestSetup { function testWithdrawNative() public { vm.startPrank(FUND_RESCUER); // Send 100 ether to tychoRouter - assertEq(address(tychoRouter).balance, 0); + assertEq(tychoRouterAddr.balance, 0); assertEq(FUND_RESCUER.balance, 0); - vm.deal(address(tychoRouter), 100 ether); + vm.deal(tychoRouterAddr, 100 ether); vm.expectEmit(); emit Withdrawal(address(0), 100 ether, FUND_RESCUER); tychoRouter.withdrawNative(FUND_RESCUER); - assertEq(address(tychoRouter).balance, 0); + assertEq(tychoRouterAddr.balance, 0); assertEq(FUND_RESCUER.balance, 100 ether); vm.stopPrank(); } function testWithdrawNativeFailures() public { - vm.deal(address(tychoRouter), 100 ether); + vm.deal(tychoRouterAddr, 100 ether); vm.startPrank(FUND_RESCUER); vm.expectRevert(TychoRouter__AddressZero.selector); tychoRouter.withdrawNative(address(0)); @@ -99,7 +99,7 @@ contract TychoRouterTest is TychoRouterTestSetup { function testWithdrawERC20Tokens() public { vm.startPrank(BOB); - mintTokens(100 ether, address(tychoRouter)); + mintTokens(100 ether, tychoRouterAddr); vm.stopPrank(); vm.startPrank(FUND_RESCUER); @@ -112,7 +112,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // Check balances after withdrawing for (uint256 i = 0; i < tokens.length; i++) { // slither-disable-next-line calls-loop - assertEq(tokens[i].balanceOf(address(tychoRouter)), 0); + assertEq(tokens[i].balanceOf(tychoRouterAddr), 0); // slither-disable-next-line calls-loop assertEq(tokens[i].balanceOf(FUND_RESCUER), 100 ether); } @@ -120,7 +120,7 @@ contract TychoRouterTest is TychoRouterTestSetup { } function testWithdrawERC20TokensFailures() public { - mintTokens(100 ether, address(tychoRouter)); + mintTokens(100 ether, tychoRouterAddr); IERC20[] memory tokensArray = new IERC20[](3); tokensArray[0] = IERC20(address(tokens[0])); tokensArray[1] = IERC20(address(tokens[1])); @@ -198,29 +198,30 @@ contract TychoRouterTest is TychoRouterTestSetup { tychoRouter.wrapETH{value: amount}(amount); vm.stopPrank(); - assertEq(address(tychoRouter).balance, 0); - assertEq(IERC20(WETH_ADDR).balanceOf(address(tychoRouter)), amount); + assertEq(tychoRouterAddr.balance, 0); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), amount); } function testUnwrapETH() public { uint256 amount = 1 ether; - deal(WETH_ADDR, address(tychoRouter), amount); + deal(WETH_ADDR, tychoRouterAddr, amount); tychoRouter.unwrapETH(amount); - assertEq(address(tychoRouter).balance, amount); - assertEq(IERC20(WETH_ADDR).balanceOf(address(tychoRouter)), 0); + assertEq(tychoRouterAddr.balance, amount); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSplitSwapSimple() public { + function testSwapSimple() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V2 // 1 WETH -> DAI // (univ2) uint256 amountIn = 1 ether; - deal(WETH_ADDR, address(tychoRouter), amountIn); + deal(WETH_ADDR, tychoRouterAddr, amountIn); - bytes memory protocolData = - encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false); + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); bytes memory swap = encodeSwap( uint8(0), @@ -233,19 +234,19 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes[] memory swaps = new bytes[](1); swaps[0] = swap; - tychoRouter.splitSwap(amountIn, 2, pleEncode(swaps)); + tychoRouter.ExposedSwap(amountIn, 2, pleEncode(swaps)); - uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); + uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr); assertEq(daiBalance, 2630432278145144658455); - assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSplitSwapMultipleHops() public { + 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, address(tychoRouter), amountIn); + deal(WETH_ADDR, tychoRouterAddr, amountIn); bytes[] memory swaps = new bytes[](2); // WETH -> DAI @@ -256,7 +257,7 @@ contract TychoRouterTest is TychoRouterTestSetup { address(usv2Executor), bytes4(0), encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, address(tychoRouter), false + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ) ); @@ -267,24 +268,24 @@ contract TychoRouterTest is TychoRouterTestSetup { uint24(0), address(usv2Executor), bytes4(0), - encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, ALICE, true) + encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) ); - tychoRouter.splitSwap(amountIn, 3, pleEncode(swaps)); + tychoRouter.ExposedSwap(amountIn, 3, pleEncode(swaps)); - uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE); + uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); assertEq(usdcBalance, 2610580090); - assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } - function testSplitSwapSplitHops() public { + 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, address(tychoRouter), amountIn); + deal(WETH_ADDR, tychoRouterAddr, amountIn); bytes[] memory swaps = new bytes[](4); // WETH -> WBTC (60%) @@ -295,7 +296,7 @@ contract TychoRouterTest is TychoRouterTestSetup { address(usv2Executor), bytes4(0), encodeUniswapV2Swap( - WETH_ADDR, WETH_WBTC_POOL, address(tychoRouter), false + WETH_ADDR, WETH_WBTC_POOL, tychoRouterAddr, false ) ); // WBTC -> USDC @@ -305,7 +306,9 @@ contract TychoRouterTest is TychoRouterTestSetup { uint24(0), address(usv2Executor), bytes4(0), - encodeUniswapV2Swap(WBTC_ADDR, USDC_WBTC_POOL, ALICE, true) + encodeUniswapV2Swap( + WBTC_ADDR, USDC_WBTC_POOL, tychoRouterAddr, true + ) ); // WETH -> DAI swaps[2] = encodeSwap( @@ -315,7 +318,7 @@ contract TychoRouterTest is TychoRouterTestSetup { address(usv2Executor), bytes4(0), encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, address(tychoRouter), false + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ) ); @@ -326,14 +329,14 @@ contract TychoRouterTest is TychoRouterTestSetup { uint24(0), address(usv2Executor), bytes4(0), - encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, ALICE, true) + encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) ); - tychoRouter.splitSwap(amountIn, 4, pleEncode(swaps)); + tychoRouter.ExposedSwap(amountIn, 4, pleEncode(swaps)); - uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE); + uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); assertEq(usdcBalance, 2581503157); - assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0); + assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } function testSwapChecked() public { @@ -350,8 +353,9 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes memory signature ) = handlePermit2Approval(WETH_ADDR, amountIn); - bytes memory protocolData = - encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false); + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); bytes memory swap = encodeSwap( uint8(0), @@ -402,8 +406,9 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes memory signature ) = handlePermit2Approval(WETH_ADDR, amountIn); - bytes memory protocolData = - encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false); + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); bytes memory swap = encodeSwap( uint8(0), @@ -461,7 +466,7 @@ contract TychoRouterTest is TychoRouterTestSetup { ) = handlePermit2Approval(WETH_ADDR, amountIn); bytes memory protocolData = encodeUniswapV2Swap( - WETH_ADDR, WETH_DAI_POOL, address(tychoRouter), false + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false ); bytes memory swap = encodeSwap( @@ -517,8 +522,9 @@ contract TychoRouterTest is TychoRouterTestSetup { spender: address(0), sigDeadline: 0 }); - bytes memory protocolData = - encodeUniswapV2Swap(WETH_ADDR, WETH_DAI_POOL, ALICE, false); + bytes memory protocolData = encodeUniswapV2Swap( + WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false + ); bytes memory swap = encodeSwap( uint8(0), @@ -566,9 +572,8 @@ contract TychoRouterTest is TychoRouterTestSetup { bytes memory signature ) = handlePermit2Approval(DAI_ADDR, amountIn); - bytes memory protocolData = encodeUniswapV2Swap( - DAI_ADDR, WETH_DAI_POOL, address(tychoRouter), true - ); + bytes memory protocolData = + encodeUniswapV2Swap(DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true); bytes memory swap = encodeSwap( uint8(0), diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index f80ea11..91edbf1 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -18,16 +18,18 @@ contract TychoRouterExposed is TychoRouter { return _unwrapETH(amount); } - function splitSwap(uint256 amountIn, uint256 nTokens, bytes calldata swaps) - external - returns (uint256) - { - return _splitSwap(amountIn, nTokens, swaps); + function ExposedSwap( + uint256 amountIn, + uint256 nTokens, + bytes calldata swaps + ) external returns (uint256) { + return _swap(amountIn, nTokens, swaps); } } contract TychoRouterTestSetup is Test, Constants { TychoRouterExposed tychoRouter; + address tychoRouterAddr; address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3); UniswapV2Executor public usv2Executor; MockERC20[] tokens; @@ -38,6 +40,7 @@ contract TychoRouterTestSetup is Test, Constants { vm.startPrank(ADMIN); tychoRouter = new TychoRouterExposed(permit2Address, WETH_ADDR); + tychoRouterAddr = address(tychoRouter); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER); tychoRouter.grantRole(keccak256("PAUSER_ROLE"), PAUSER); @@ -98,7 +101,7 @@ contract TychoRouterTestSetup is Test, Constants { expiration: uint48(block.timestamp + 1 days), nonce: 0 }), - spender: address(tychoRouter), + spender: tychoRouterAddr, sigDeadline: block.timestamp + 1 days }); From 0a1f5222076f20496b520aee64999507a343b0b3 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 28 Jan 2025 17:06:24 +0000 Subject: [PATCH 9/9] fix: fix submodules --- don't change below this line --- ENG-4041 Took 6 minutes --- foundry/lib/openzeppelin-contracts | 2 +- foundry/lib/permit2 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foundry/lib/openzeppelin-contracts b/foundry/lib/openzeppelin-contracts index 293a3e2..840c974 160000 --- a/foundry/lib/openzeppelin-contracts +++ b/foundry/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 293a3e264ef2d33526bcc5e8a304887ec4966fdc +Subproject commit 840c974028316f3c8172c1b8e5ed67ad95e255ca diff --git a/foundry/lib/permit2 b/foundry/lib/permit2 index 5834a2d..cc56ad0 160000 --- a/foundry/lib/permit2 +++ b/foundry/lib/permit2 @@ -1 +1 @@ -Subproject commit 5834a2d12cc85df5b706eaaea02ef74e89738231 +Subproject commit cc56ad0f3439c502c246fc5cfcc3db92bb8b7219