Merge branch 'main' into router/hr/ENG-4035-Balancer-V2-Executor

This commit is contained in:
Harsh Vardhan Roy
2025-01-28 22:43:02 +05:30
committed by GitHub
17 changed files with 898 additions and 107 deletions

View File

@@ -22,6 +22,7 @@ interface IExecutor {
*/ */
function swap(uint256 givenAmount, bytes calldata data) function swap(uint256 givenAmount, bytes calldata data)
external external
payable
returns (uint256 calculatedAmount); returns (uint256 calculatedAmount);
} }

9
foundry/lib/IWETH.sol Normal file
View File

@@ -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;
}

58
foundry/lib/LibSwap.sol Normal file
View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
library LibSwap {
/// 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]));
}
/// 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[29:];
}
}

View File

@@ -49,6 +49,7 @@ contract CallbackVerificationDispatcher {
// slither-disable-next-line dead-code // slither-disable-next-line dead-code
function _callVerifyCallback(bytes calldata data) function _callVerifyCallback(bytes calldata data)
internal internal
view
returns ( returns (
uint256 amountOwed, uint256 amountOwed,
uint256 amountReceived, uint256 amountReceived,

View File

@@ -50,29 +50,21 @@ contract ExecutionDispatcher {
* @dev Calls an executor, assumes swap.protocolData contains * @dev Calls an executor, assumes swap.protocolData contains
* protocol-specific data required by the executor. * protocol-specific data required by the executor.
*/ */
// slither-disable-next-line dead-code // slither-disable-next-line delegatecall-loop
function _callExecutor(uint256 amount, bytes calldata data) function _callExecutor(
internal address executor,
returns (uint256 calculatedAmount) bytes4 selector,
{ uint256 amount,
address executor; bytes calldata data
bytes4 decodedSelector; ) internal returns (uint256 calculatedAmount) {
bytes memory protocolData;
(executor, decodedSelector, protocolData) =
_decodeExecutorAndSelector(data);
if (!executors[executor]) { if (!executors[executor]) {
revert ExecutionDispatcher__UnapprovedExecutor(); revert ExecutionDispatcher__UnapprovedExecutor();
} }
bytes4 selector = decodedSelector == bytes4(0) selector = selector == bytes4(0) ? IExecutor.swap.selector : selector;
? IExecutor.swap.selector // slither-disable-next-line controlled-delegatecall,low-level-calls
: decodedSelector;
// slither-disable-next-line low-level-calls
(bool success, bytes memory result) = executor.delegatecall( (bool success, bytes memory result) = executor.delegatecall(
abi.encodeWithSelector(selector, amount, protocolData) abi.encodeWithSelector(selector, amount, data)
); );
if (!success) { if (!success) {
@@ -87,16 +79,4 @@ contract ExecutionDispatcher {
calculatedAmount = abi.decode(result, (uint256)); 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:];
}
} }

View File

@@ -1,26 +1,37 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.28;
import "../lib/IWETH.sol";
import "../lib/bytes/LibPrefixLengthEncodedByteArray.sol";
import "./CallbackVerificationDispatcher.sol";
import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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 "@permit2/src/interfaces/IAllowanceTransfer.sol";
import "./ExecutionDispatcher.sol"; import "./ExecutionDispatcher.sol";
import "./CallbackVerificationDispatcher.sol"; import "./CallbackVerificationDispatcher.sol";
import "@openzeppelin/contracts/utils/Pausable.sol"; import {LibSwap} from "../lib/LibSwap.sol";
error TychoRouter__WithdrawalFailed(); error TychoRouter__WithdrawalFailed();
error TychoRouter__AddressZero(); error TychoRouter__AddressZero();
error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount);
error TychoRouter__MessageValueMismatch(uint256 value, uint256 amount);
contract TychoRouter is contract TychoRouter is
AccessControl, AccessControl,
ExecutionDispatcher, ExecutionDispatcher,
CallbackVerificationDispatcher, CallbackVerificationDispatcher,
Pausable Pausable,
ReentrancyGuard
{ {
IAllowanceTransfer public immutable permit2; IAllowanceTransfer public immutable permit2;
IWETH private immutable _weth;
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
using LibPrefixLengthEncodedByteArray for bytes;
using LibSwap for bytes;
//keccak256("NAME_OF_ROLE") : save gas on deployment //keccak256("NAME_OF_ROLE") : save gas on deployment
bytes32 public constant EXECUTOR_SETTER_ROLE = bytes32 public constant EXECUTOR_SETTER_ROLE =
@@ -48,9 +59,10 @@ contract TychoRouter is
); );
event FeeSet(uint256 indexed oldFee, uint256 indexed newFee); event FeeSet(uint256 indexed oldFee, uint256 indexed newFee);
constructor(address _permit2) { constructor(address _permit2, address weth) {
permit2 = IAllowanceTransfer(_permit2); permit2 = IAllowanceTransfer(_permit2);
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_weth = IWETH(weth);
} }
/** /**
@@ -77,22 +89,116 @@ contract TychoRouter is
} }
/** /**
* @dev Executes a swap graph supporting internal splits token amount * @notice Executes a swap operation based on a predefined swap graph, supporting internal token amount splits.
* splits, checking that the user gets more than minUserAmount of buyToken. * 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 `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 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).
* @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( function swap(
uint256 amountIn, uint256 amountIn,
address tokenIn, address tokenIn,
uint256 minUserAmount, address tokenOut,
uint256 minAmountOut,
bool wrapEth, bool wrapEth,
bool unwrapEth, bool unwrapEth,
uint256 nTokens, uint256 nTokens,
bytes calldata swaps, address receiver,
IAllowanceTransfer.PermitSingle calldata permitSingle, IAllowanceTransfer.PermitSingle calldata permitSingle,
bytes calldata signature bytes calldata signature,
) external whenNotPaused returns (uint256 amountOut) { bytes calldata swaps
amountOut = 0; ) external payable whenNotPaused nonReentrant returns (uint256 amountOut) {
// TODO 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);
} else if (tokenIn != address(0)) {
permit2.permit(msg.sender, permitSingle, signature);
permit2.transferFrom(
msg.sender,
address(this),
uint160(amountIn),
permitSingle.details.token
);
}
amountOut = _swap(amountIn, nTokens, swaps);
if (fee > 0) {
uint256 feeAmount = (amountOut * fee) / 10000;
amountOut -= feeAmount;
IERC20(tokenOut).safeTransfer(feeReceiver, feeAmount);
}
if (minAmountOut > 0 && amountOut < minAmountOut) {
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
}
if (unwrapEth) {
_unwrapETH(amountOut);
// slither-disable-next-line arbitrary-send-eth
payable(receiver).transfer(amountOut);
} else {
IERC20(tokenOut).safeTransfer(receiver, amountOut);
}
}
function _swap(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();
tokenInIndex = swapData.tokenInIndex();
tokenOutIndex = swapData.tokenOutIndex();
split = swapData.splitPercentage();
currentAmountIn = split > 0
? (amounts[tokenInIndex] * split) / 0xffffff
: remainingAmounts[tokenInIndex];
currentAmountOut = _callExecutor(
swapData.executor(),
swapData.executorSelector(),
currentAmountIn,
swapData.protocolData()
);
amounts[tokenOutIndex] += currentAmountOut;
remainingAmounts[tokenOutIndex] += currentAmountOut;
remainingAmounts[tokenInIndex] -= currentAmountIn;
}
return amounts[tokenOutIndex];
} }
/** /**
@@ -209,6 +315,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 * @dev Allows this contract to receive native token
*/ */

View File

@@ -10,8 +10,10 @@ error UniswapV2Executor__InvalidDataLength();
contract UniswapV2Executor is IExecutor { contract UniswapV2Executor is IExecutor {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
// slither-disable-next-line locked-ether
function swap(uint256 givenAmount, bytes calldata data) function swap(uint256 givenAmount, bytes calldata data)
external external
payable
returns (uint256 calculatedAmount) returns (uint256 calculatedAmount)
{ {
address target; address target;

View File

@@ -9,6 +9,7 @@ contract CallbackVerificationDispatcherExposed is
{ {
function exposedCallVerifier(bytes calldata data) function exposedCallVerifier(bytes calldata data)
external external
view
returns ( returns (
uint256 amountOwed, uint256 amountOwed,
uint256 amountReceived, uint256 amountReceived,
@@ -176,7 +177,7 @@ contract CallbackVerificationDispatcherTest is Constants {
dispatcherExposed.exposedCallVerifier(data); dispatcherExposed.exposedCallVerifier(data);
} }
function testDecodeVerifierAndSelector() public { function testDecodeVerifierAndSelector() public view {
bytes memory data = bytes memory data =
hex"2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e876b20f8aA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; hex"2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e876b20f8aA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
(address executor, bytes4 selector, bytes memory verifierData) = (address executor, bytes4 selector, bytes memory verifierData) =

View File

@@ -9,6 +9,10 @@ contract Constants is Test {
address FUND_RESCUER = makeAddr("fundRescuer"); address FUND_RESCUER = makeAddr("fundRescuer");
address FEE_SETTER = makeAddr("feeSetter"); address FEE_SETTER = makeAddr("feeSetter");
address FEE_RECEIVER = makeAddr("feeReceiver"); address FEE_RECEIVER = makeAddr("feeReceiver");
address EXECUTOR_SETTER = makeAddr("executorSetter");
address ALICE = 0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2;
uint256 ALICE_PK =
0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234;
// Dummy contracts // Dummy contracts
address DUMMY = makeAddr("dummy"); address DUMMY = makeAddr("dummy");
@@ -19,6 +23,14 @@ contract Constants is Test {
address WETH_ADDR = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); address WETH_ADDR = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
address DAI_ADDR = address(0x6B175474E89094C44Da98b954EedeAC495271d0F); address DAI_ADDR = address(0x6B175474E89094C44Da98b954EedeAC495271d0F);
address BAL_ADDR = address(0xba100000625a3754423978a60c9317c58a424e3D); address BAL_ADDR = address(0xba100000625a3754423978a60c9317c58a424e3D);
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 * @dev Deploys a dummy contract with non-empty bytecode

View File

@@ -5,19 +5,13 @@ import "@src/ExecutionDispatcher.sol";
import "./TychoRouterTestSetup.sol"; import "./TychoRouterTestSetup.sol";
contract ExecutionDispatcherExposed is ExecutionDispatcher { contract ExecutionDispatcherExposed is ExecutionDispatcher {
function exposedCallExecutor(uint256 amount, bytes calldata data) function exposedCallExecutor(
external address executor,
returns (uint256 calculatedAmount) bytes4 selector,
{ uint256 amount,
return _callExecutor(amount, data); bytes calldata data
} ) external returns (uint256 calculatedAmount) {
return _callExecutor(executor, selector, amount, data);
function exposedDecodeExecutorAndSelector(bytes calldata data)
external
pure
returns (address executor, bytes4 selector, bytes memory protocolData)
{
return _decodeExecutorAndSelector(data);
} }
function exposedSetExecutor(address target) external { function exposedSetExecutor(address target) external {
@@ -88,10 +82,14 @@ contract ExecutionDispatcherTest is Constants {
address(0xe592557AB9F4A75D992283fD6066312FF013ba3d) address(0xe592557AB9F4A75D992283fD6066312FF013ba3d)
); );
bytes memory data = bytes memory data =
hex"e592557AB9F4A75D992283fD6066312FF013ba3dbd0625ab5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72fc8c39af7983bf329086de522229a7be5fc4e41cc51c72848c68a965f66fa7a88855f9f7784502a7f2606beffe61000613d6a25b5bfef4cd7652aa94777d4a46b39f2e206411280a12c9344b769ff1066c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000082ec8ad1b0000000000000000000000000000000000000000000000000000000066d7b65800000000000000000000000000000000000000000000000000000191ba9f843c125000064000640000d52de09955f0ffffffffffffff00225c389e595fe9000001fcc910754b349f821e4bb5d8444822a63920be943aba6f1b31ee14ef0fc6840b6d28d604e04a78834b668dba24a6c082ffb901e4fffa9600649e8d991af593c81c"; hex"5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72fc8c39af7983bf329086de522229a7be5fc4e41cc51c72848c68a965f66fa7a88855f9f7784502a7f2606beffe61000613d6a25b5bfef4cd7652aa94777d4a46b39f2e206411280a12c9344b769ff1066c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000082ec8ad1b0000000000000000000000000000000000000000000000000000000066d7b65800000000000000000000000000000000000000000000000000000191ba9f843c125000064000640000d52de09955f0ffffffffffffff00225c389e595fe9000001fcc910754b349f821e4bb5d8444822a63920be943aba6f1b31ee14ef0fc6840b6d28d604e04a78834b668dba24a6c082ffb901e4fffa9600649e8d991af593c81c";
uint256 givenAmount = 15 ether; uint256 givenAmount = 15 ether;
uint256 amount = uint256 amount = dispatcherExposed.exposedCallExecutor(
dispatcherExposed.exposedCallExecutor(givenAmount, data); 0xe592557AB9F4A75D992283fD6066312FF013ba3d,
IExecutor.swap.selector,
givenAmount,
data
);
assert(amount == 35144641819); assert(amount == 35144641819);
} }
@@ -111,10 +109,14 @@ contract ExecutionDispatcherTest is Constants {
address(0xe592557AB9F4A75D992283fD6066312FF013ba3d) address(0xe592557AB9F4A75D992283fD6066312FF013ba3d)
); );
bytes memory data = bytes memory data =
hex"e592557AB9F4A75D992283fD6066312FF013ba3d000000005615dEB798BB3E4dFa0139dFa1b3D433Cc23b72fc8c39af7983bf329086de522229a7be5fc4e41cc51c72848c68a965f66fa7a88855f9f7784502a7f2606beffe61000613d6a25b5bfef4cd7652aa94777d4a46b39f2e206411280a12c9344b769ff1066c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000082ec8ad1b0000000000000000000000000000000000000000000000000000000066d7b65800000000000000000000000000000000000000000000000000000191ba9f843c125000064000640000d52de09955f0ffffffffffffff00225c389e595fe9000001fcc910754b349f821e4bb5d8444822a63920be943aba6f1b31ee14ef0fc6840b6d28d604e04a78834b668dba24a6c082ffb901e4fffa9600649e8d991af593c81c"; hex"5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72fc8c39af7983bf329086de522229a7be5fc4e41cc51c72848c68a965f66fa7a88855f9f7784502a7f2606beffe61000613d6a25b5bfef4cd7652aa94777d4a46b39f2e206411280a12c9344b769ff1066c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000082ec8ad1b0000000000000000000000000000000000000000000000000000000066d7b65800000000000000000000000000000000000000000000000000000191ba9f843c125000064000640000d52de09955f0ffffffffffffff00225c389e595fe9000001fcc910754b349f821e4bb5d8444822a63920be943aba6f1b31ee14ef0fc6840b6d28d604e04a78834b668dba24a6c082ffb901e4fffa9600649e8d991af593c81c";
uint256 givenAmount = 15 ether; uint256 givenAmount = 15 ether;
uint256 amount = uint256 amount = dispatcherExposed.exposedCallExecutor(
dispatcherExposed.exposedCallExecutor(givenAmount, data); 0xe592557AB9F4A75D992283fD6066312FF013ba3d,
bytes4(0),
givenAmount,
data
);
assert(amount == 35144641819); assert(amount == 35144641819);
} }
@@ -124,26 +126,21 @@ contract ExecutionDispatcherTest is Constants {
address(0xe592557AB9F4A75D992283fD6066312FF013ba3d) address(0xe592557AB9F4A75D992283fD6066312FF013ba3d)
); );
bytes memory data = bytes memory data =
hex"e592557AB9F4A75D992283fD6066312FF013ba3dbd0625ab5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72fc8c39af7983bf329086de522229a7be5fc4e41cc51c72848c68a965f66fa7a88855f9f7784502a7f2606beffe61000613d6a25b5bfef4cd7652aa94777d4a46b39f2e206411280a12c9344b769ff1066c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000082ec8ad1b0000000000000000000000000000000000000000000000000000000066d7b65800000000000000000000000000000000000000000000000000000191ba9f843c125000064000640000d52de09955f0ffffffffffffff00225c389e595fe9000001fcc910754b349f821e4bb5d8444822a63920be943aba6f1b31ee14ef0fc6840b6d28d604e04a78834b668dba24a6c082ffb901e4fffa9600649e8d991af593"; hex"5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72fc8c39af7983bf329086de522229a7be5fc4e41cc51c72848c68a965f66fa7a88855f9f7784502a7f2606beffe61000613d6a25b5bfef4cd7652aa94777d4a46b39f2e206411280a12c9344b769ff1066c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000d02ab486cedc0000000000000000000000000000000000000000000000000000000000082ec8ad1b0000000000000000000000000000000000000000000000000000000066d7b65800000000000000000000000000000000000000000000000000000191ba9f843c125000064000640000d52de09955f0ffffffffffffff00225c389e595fe9000001fcc910754b349f821e4bb5d8444822a63920be943aba6f1b31ee14ef0fc6840b6d28d604e04a78834b668dba24a6c082ffb901e4fffa9600649e8d991af593";
vm.expectRevert(); vm.expectRevert();
dispatcherExposed.exposedCallExecutor(0, data); dispatcherExposed.exposedCallExecutor(
0xe592557AB9F4A75D992283fD6066312FF013ba3d,
IExecutor.swap.selector,
0,
data
);
} }
function testCallExecutorUnapprovedExecutor() public { function testCallExecutorUnapprovedExecutor() public {
bytes memory data = bytes memory data = hex"aabbccdd1111111111111111";
hex"5d622C9053b8FFB1B3465495C8a42E603632bA70aabbccdd1111111111111111";
vm.expectRevert(); vm.expectRevert();
dispatcherExposed.exposedCallExecutor(0, data); dispatcherExposed.exposedCallExecutor(
} 0x5d622C9053b8FFB1B3465495C8a42E603632bA70, bytes4(0), 0, data
);
function testDecodeExecutorAndSelector() public {
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"));
} }
} }

View File

@@ -8,14 +8,14 @@ import {LibPrefixLengthEncodedByteArray} from
contract LibPrefixLengthEncodedByteArrayTest is Test { contract LibPrefixLengthEncodedByteArrayTest is Test {
using LibPrefixLengthEncodedByteArray for bytes; using LibPrefixLengthEncodedByteArray for bytes;
function testNextEmpty() public { function testNextEmpty() public view {
bytes memory encoded = ""; bytes memory encoded = "";
(bytes memory elem, bytes memory remaining) = this.next(encoded); (bytes memory elem, bytes memory remaining) = this.next(encoded);
assertEq(elem.length, 0); assertEq(elem.length, 0);
assertEq(remaining.length, 0); assertEq(remaining.length, 0);
} }
function testNextSingleElement() public { function testNextSingleElement() public view {
// Create encoded data: length prefix (0003) followed by "ABC" // Create encoded data: length prefix (0003) followed by "ABC"
bytes memory encoded = hex"0003414243"; bytes memory encoded = hex"0003414243";
(bytes memory elem, bytes memory remaining) = this.next(encoded); (bytes memory elem, bytes memory remaining) = this.next(encoded);
@@ -25,7 +25,7 @@ contract LibPrefixLengthEncodedByteArrayTest is Test {
assertEq(remaining.length, 0); assertEq(remaining.length, 0);
} }
function testNextMultipleElements() public { function testNextMultipleElements() public view {
// Encoded data: [0003]ABC[0002]DE // Encoded data: [0003]ABC[0002]DE
bytes memory encoded = hex"000341424300024445"; bytes memory encoded = hex"000341424300024445";
@@ -40,7 +40,7 @@ contract LibPrefixLengthEncodedByteArrayTest is Test {
assertEq(remaining2.length, 0); assertEq(remaining2.length, 0);
} }
function testSize() public { function testSize() public view {
bytes memory empty = ""; bytes memory empty = "";
assertEq(this.size(empty), 0); assertEq(this.size(empty), 0);
@@ -51,19 +51,19 @@ contract LibPrefixLengthEncodedByteArrayTest is Test {
assertEq(this.size(multiple), 3); assertEq(this.size(multiple), 3);
} }
function testFailInvalidLength() public { function testFailInvalidLength() public view {
// Length prefix larger than remaining data // Length prefix larger than remaining data
bytes memory invalid = hex"0004414243"; 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 // Only 1 byte instead of 2 bytes prefix
bytes memory invalid = hex"01"; 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) // Test with a large but manageable size (1000 bytes)
bytes memory large = new bytes(1002); // 2 bytes prefix + 1000 bytes data bytes memory large = new bytes(1002); // 2 bytes prefix + 1000 bytes data
large[0] = bytes1(uint8(0x03)); // 03 large[0] = bytes1(uint8(0x03)); // 03
@@ -79,7 +79,7 @@ contract LibPrefixLengthEncodedByteArrayTest is Test {
assertEq(remaining.length, 0); assertEq(remaining.length, 0);
} }
function testSizeWithLargeElements() public { function testSizeWithLargeElements() public view {
// Two elements: 1000 bytes + 500 bytes // Two elements: 1000 bytes + 500 bytes
bytes memory data = new bytes(1504); // 1000 + 2 + 500 + 2 bytes memory data = new bytes(1504); // 1000 + 2 + 500 + 2

View File

@@ -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);
}
}

View File

@@ -20,14 +20,14 @@ contract TychoRouterTest is TychoRouterTestSetup {
); );
function testSetExecutorValidRole() public { function testSetExecutorValidRole() public {
vm.startPrank(executorSetter); vm.startPrank(EXECUTOR_SETTER);
tychoRouter.setExecutor(DUMMY); tychoRouter.setExecutor(DUMMY);
vm.stopPrank(); vm.stopPrank();
assert(tychoRouter.executors(DUMMY) == true); assert(tychoRouter.executors(DUMMY) == true);
} }
function testRemoveExecutorValidRole() public { function testRemoveExecutorValidRole() public {
vm.startPrank(executorSetter); vm.startPrank(EXECUTOR_SETTER);
tychoRouter.setExecutor(DUMMY); tychoRouter.setExecutor(DUMMY);
tychoRouter.removeExecutor(DUMMY); tychoRouter.removeExecutor(DUMMY);
vm.stopPrank(); vm.stopPrank();
@@ -45,14 +45,14 @@ contract TychoRouterTest is TychoRouterTestSetup {
} }
function testSetVerifierValidRole() public { function testSetVerifierValidRole() public {
vm.startPrank(executorSetter); vm.startPrank(EXECUTOR_SETTER);
tychoRouter.setCallbackVerifier(DUMMY); tychoRouter.setCallbackVerifier(DUMMY);
vm.stopPrank(); vm.stopPrank();
assert(tychoRouter.callbackVerifiers(DUMMY) == true); assert(tychoRouter.callbackVerifiers(DUMMY) == true);
} }
function testRemoveVerifierValidRole() public { function testRemoveVerifierValidRole() public {
vm.startPrank(executorSetter); vm.startPrank(EXECUTOR_SETTER);
tychoRouter.setCallbackVerifier(DUMMY); tychoRouter.setCallbackVerifier(DUMMY);
tychoRouter.removeCallbackVerifier(DUMMY); tychoRouter.removeCallbackVerifier(DUMMY);
vm.stopPrank(); vm.stopPrank();
@@ -72,19 +72,19 @@ contract TychoRouterTest is TychoRouterTestSetup {
function testWithdrawNative() public { function testWithdrawNative() public {
vm.startPrank(FUND_RESCUER); vm.startPrank(FUND_RESCUER);
// Send 100 ether to tychoRouter // Send 100 ether to tychoRouter
assertEq(address(tychoRouter).balance, 0); assertEq(tychoRouterAddr.balance, 0);
assertEq(FUND_RESCUER.balance, 0); assertEq(FUND_RESCUER.balance, 0);
vm.deal(address(tychoRouter), 100 ether); vm.deal(tychoRouterAddr, 100 ether);
vm.expectEmit(); vm.expectEmit();
emit Withdrawal(address(0), 100 ether, FUND_RESCUER); emit Withdrawal(address(0), 100 ether, FUND_RESCUER);
tychoRouter.withdrawNative(FUND_RESCUER); tychoRouter.withdrawNative(FUND_RESCUER);
assertEq(address(tychoRouter).balance, 0); assertEq(tychoRouterAddr.balance, 0);
assertEq(FUND_RESCUER.balance, 100 ether); assertEq(FUND_RESCUER.balance, 100 ether);
vm.stopPrank(); vm.stopPrank();
} }
function testWithdrawNativeFailures() public { function testWithdrawNativeFailures() public {
vm.deal(address(tychoRouter), 100 ether); vm.deal(tychoRouterAddr, 100 ether);
vm.startPrank(FUND_RESCUER); vm.startPrank(FUND_RESCUER);
vm.expectRevert(TychoRouter__AddressZero.selector); vm.expectRevert(TychoRouter__AddressZero.selector);
tychoRouter.withdrawNative(address(0)); tychoRouter.withdrawNative(address(0));
@@ -99,7 +99,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
function testWithdrawERC20Tokens() public { function testWithdrawERC20Tokens() public {
vm.startPrank(BOB); vm.startPrank(BOB);
mintTokens(100 ether, address(tychoRouter)); mintTokens(100 ether, tychoRouterAddr);
vm.stopPrank(); vm.stopPrank();
vm.startPrank(FUND_RESCUER); vm.startPrank(FUND_RESCUER);
@@ -112,7 +112,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
// Check balances after withdrawing // Check balances after withdrawing
for (uint256 i = 0; i < tokens.length; i++) { for (uint256 i = 0; i < tokens.length; i++) {
// slither-disable-next-line calls-loop // 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 // slither-disable-next-line calls-loop
assertEq(tokens[i].balanceOf(FUND_RESCUER), 100 ether); assertEq(tokens[i].balanceOf(FUND_RESCUER), 100 ether);
} }
@@ -120,7 +120,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
} }
function testWithdrawERC20TokensFailures() public { function testWithdrawERC20TokensFailures() public {
mintTokens(100 ether, address(tychoRouter)); mintTokens(100 ether, tychoRouterAddr);
IERC20[] memory tokensArray = new IERC20[](3); IERC20[] memory tokensArray = new IERC20[](3);
tokensArray[0] = IERC20(address(tokens[0])); tokensArray[0] = IERC20(address(tokens[0]));
tokensArray[1] = IERC20(address(tokens[1])); tokensArray[1] = IERC20(address(tokens[1]));
@@ -189,4 +189,421 @@ contract TychoRouterTest is TychoRouterTestSetup {
tychoRouter.pause(); tychoRouter.pause();
vm.stopPrank(); vm.stopPrank();
} }
function testWrapETH() public {
uint256 amount = 1 ether;
vm.deal(BOB, amount);
vm.startPrank(BOB);
tychoRouter.wrapETH{value: amount}(amount);
vm.stopPrank();
assertEq(tychoRouterAddr.balance, 0);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), amount);
}
function testUnwrapETH() public {
uint256 amount = 1 ether;
deal(WETH_ADDR, tychoRouterAddr, amount);
tychoRouter.unwrapETH(amount);
assertEq(tychoRouterAddr.balance, amount);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSwapSimple() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// 1 WETH -> DAI
// (univ2)
uint256 amountIn = 1 ether;
deal(WETH_ADDR, tychoRouterAddr, amountIn);
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
);
bytes memory swap = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
tychoRouter.ExposedSwap(amountIn, 2, pleEncode(swaps));
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr);
assertEq(daiBalance, 2630432278145144658455);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSwapMultipleHops() public {
// Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2
// 1 WETH -> DAI -> USDC
// (univ2) (univ2)
uint256 amountIn = 1 ether;
deal(WETH_ADDR, tychoRouterAddr, amountIn);
bytes[] memory swaps = new bytes[](2);
// WETH -> DAI
swaps[0] = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
)
);
// DAI -> USDC
swaps[1] = encodeSwap(
uint8(1),
uint8(2),
uint24(0),
address(usv2Executor),
bytes4(0),
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true)
);
tychoRouter.ExposedSwap(amountIn, 3, pleEncode(swaps));
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr);
assertEq(usdcBalance, 2610580090);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSwapSplitHops() public {
// Trade 1 WETH for USDC through DAI and WBTC with 4 swaps on Uniswap V2
// -> DAI ->
// 1 WETH USDC
// -> WBTC ->
// (univ2) (univ2)
uint256 amountIn = 1 ether;
deal(WETH_ADDR, tychoRouterAddr, amountIn);
bytes[] memory swaps = new bytes[](4);
// WETH -> WBTC (60%)
swaps[0] = encodeSwap(
uint8(0),
uint8(1),
(0xffffff * 60) / 100, // 60%
address(usv2Executor),
bytes4(0),
encodeUniswapV2Swap(
WETH_ADDR, WETH_WBTC_POOL, tychoRouterAddr, false
)
);
// WBTC -> USDC
swaps[1] = encodeSwap(
uint8(1),
uint8(2),
uint24(0),
address(usv2Executor),
bytes4(0),
encodeUniswapV2Swap(
WBTC_ADDR, USDC_WBTC_POOL, tychoRouterAddr, true
)
);
// WETH -> DAI
swaps[2] = encodeSwap(
uint8(0),
uint8(3),
uint24(0),
address(usv2Executor),
bytes4(0),
encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
)
);
// DAI -> USDC
swaps[3] = encodeSwap(
uint8(3),
uint8(2),
uint24(0),
address(usv2Executor),
bytes4(0),
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true)
);
tychoRouter.ExposedSwap(amountIn, 4, pleEncode(swaps));
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr);
assertEq(usdcBalance, 2581503157);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSwapChecked() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// Does permit2 token approval and transfer
// Checks amount out at the end
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(WETH_ADDR, amountIn);
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
);
bytes memory swap = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 minAmountOut = 2600 * 1e18;
uint256 amountOut = tychoRouter.swap(
amountIn,
WETH_ADDR,
DAI_ADDR,
minAmountOut,
false,
false,
2,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
uint256 expectedAmount = 2630432278145144658455;
assertEq(amountOut, expectedAmount);
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
assertEq(daiBalance, expectedAmount);
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0);
vm.stopPrank();
}
function testSwapCheckedFailure() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// Does permit2 token approval and transfer
// Checks amount out at the end and fails
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(WETH_ADDR, amountIn);
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
);
bytes memory swap = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 minAmountOut = 3000 * 1e18;
vm.expectRevert(
abi.encodeWithSelector(
TychoRouter__NegativeSlippage.selector,
2630432278145144658455, // actual amountOut
minAmountOut
)
);
tychoRouter.swap(
amountIn,
WETH_ADDR,
DAI_ADDR,
minAmountOut,
false,
false,
2,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
vm.stopPrank();
}
function testSwapFee() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// Does permit2 token approval and transfer
// Takes fee at the end
vm.startPrank(FEE_SETTER);
tychoRouter.setFee(100);
tychoRouter.setFeeReceiver(FEE_RECEIVER);
vm.stopPrank();
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(WETH_ADDR, amountIn);
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
);
bytes memory swap = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 amountOut = tychoRouter.swap(
amountIn,
WETH_ADDR,
DAI_ADDR,
0,
false,
false,
2,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
uint256 expectedAmount = 2604127955363693211871;
assertEq(amountOut, expectedAmount);
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
assertEq(daiBalance, expectedAmount);
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 amountIn = 1 ether;
deal(ALICE, amountIn);
vm.startPrank(ALICE);
IAllowanceTransfer.PermitSingle memory emptyPermitSingle =
IAllowanceTransfer.PermitSingle({
details: IAllowanceTransfer.PermitDetails({
token: address(0),
amount: 0,
expiration: 0,
nonce: 0
}),
spender: address(0),
sigDeadline: 0
});
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
);
bytes memory swap = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 amountOut = tychoRouter.swap{value: amountIn}(
amountIn,
address(0),
DAI_ADDR,
0,
true,
false,
2,
ALICE,
emptyPermitSingle,
"",
pleEncode(swaps)
);
uint256 expectedAmount = 2630432278145144658455;
assertEq(amountOut, expectedAmount);
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
assertEq(daiBalance, expectedAmount);
assertEq(ALICE.balance, 0);
vm.stopPrank();
}
function testSwapUnwrapETH() public {
// Trade 3k DAI for WETH with 1 swap on Uniswap V2 and unwrap it at the end
uint256 amountIn = 3_000 * 10 ** 18;
deal(DAI_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(DAI_ADDR, amountIn);
bytes memory protocolData =
encodeUniswapV2Swap(DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true);
bytes memory swap = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor),
bytes4(0),
protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 amountOut = tychoRouter.swap(
amountIn,
DAI_ADDR,
address(0),
0,
false,
true,
2,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
uint256 expectedAmount = 1132829934891544187; // 1.13 ETH
assertEq(amountOut, expectedAmount);
assertEq(ALICE.balance, expectedAmount);
vm.stopPrank();
}
} }

View File

@@ -1,28 +1,61 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13; pragma solidity ^0.8.13;
import "@src/TychoRouter.sol"; import "../src/executors/UniswapV2Executor.sol";
import "./Constants.sol"; import "./Constants.sol";
import "./mock/MockERC20.sol"; import "./mock/MockERC20.sol";
import "@src/TychoRouter.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);
}
function ExposedSwap(
uint256 amountIn,
uint256 nTokens,
bytes calldata swaps
) external returns (uint256) {
return _swap(amountIn, nTokens, swaps);
}
}
contract TychoRouterTestSetup is Test, Constants { contract TychoRouterTestSetup is Test, Constants {
TychoRouter tychoRouter; TychoRouterExposed tychoRouter;
address executorSetter; address tychoRouterAddr;
address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3); address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3);
UniswapV2Executor public usv2Executor;
MockERC20[] tokens; MockERC20[] tokens;
function setUp() public { function setUp() public {
uint256 forkBlock = 21000000;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
vm.startPrank(ADMIN); vm.startPrank(ADMIN);
tychoRouter = new TychoRouter(permit2Address); tychoRouter = new TychoRouterExposed(permit2Address, WETH_ADDR);
tychoRouter.grantRole(keccak256("EXECUTOR_SETTER_ROLE"), BOB); tychoRouterAddr = address(tychoRouter);
tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER);
tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER);
tychoRouter.grantRole(keccak256("PAUSER_ROLE"), PAUSER); tychoRouter.grantRole(keccak256("PAUSER_ROLE"), PAUSER);
tychoRouter.grantRole(keccak256("UNPAUSER_ROLE"), UNPAUSER); tychoRouter.grantRole(keccak256("UNPAUSER_ROLE"), UNPAUSER);
executorSetter = BOB; tychoRouter.grantRole(
keccak256("EXECUTOR_SETTER_ROLE"), EXECUTOR_SETTER
);
deployDummyContract(); deployDummyContract();
vm.stopPrank(); vm.stopPrank();
usv2Executor = new UniswapV2Executor();
vm.startPrank(EXECUTOR_SETTER);
tychoRouter.setExecutor(address(usv2Executor));
vm.stopPrank();
vm.startPrank(BOB); vm.startPrank(BOB);
tokens.push(new MockERC20("Token A", "A")); tokens.push(new MockERC20("Token A", "A"));
tokens.push(new MockERC20("Token B", "B")); tokens.push(new MockERC20("Token B", "B"));
@@ -41,4 +74,117 @@ contract TychoRouterTestSetup is Test, Constants {
tokens[i].mint(to, amount); 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: tychoRouterAddr,
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 view 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,
address executor,
bytes4 selector,
bytes memory protocolData
) internal pure returns (bytes memory) {
return abi.encodePacked(
tokenInIndex, tokenOutIndex, split, executor, selector, protocolData
);
}
function encodeUniswapV2Swap(
address tokenIn,
address target,
address receiver,
bool zero2one
) internal pure returns (bytes memory) {
return abi.encodePacked(tokenIn, target, receiver, zero2one);
}
} }

View File

@@ -34,7 +34,6 @@ contract UniswapV2ExecutorTest is UniswapV2ExecutorExposed, Test, Constants {
UniswapV2ExecutorExposed uniswapV2Exposed; UniswapV2ExecutorExposed uniswapV2Exposed;
IERC20 WETH = IERC20(WETH_ADDR); IERC20 WETH = IERC20(WETH_ADDR);
IERC20 DAI = IERC20(DAI_ADDR); IERC20 DAI = IERC20(DAI_ADDR);
address WETH_DAI_POOL = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11;
function setUp() public { function setUp() public {
uint256 forkBlock = 17323404; uint256 forkBlock = 17323404;