Merge branch 'main' into feat/maverick-v2-executor

This commit is contained in:
Tamara
2025-04-29 12:01:20 -04:00
committed by GitHub
57 changed files with 9339 additions and 5667 deletions

View File

@@ -7,6 +7,7 @@ evm_version = 'cancun'
optimizer = true
optimizer_runs = 200
via_ir = true
fs_permissions = [{ access = "read", path = "./test/assets" }]
[profile.production]
src = 'src'
@@ -21,6 +22,5 @@ via_ir = true
[rpc_endpoints]
mainnet = "${RPC_URL}"
[fmt]
line_length = 80

View File

@@ -2,36 +2,42 @@
pragma solidity ^0.8.26;
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]);
/**
* @dev Returns arguments required to perform a single swap
*/
function decodeSingleSwap(bytes calldata swap)
internal
pure
returns (address executor, bytes calldata protocolData)
{
executor = address(uint160(bytes20(swap[0:20])));
protocolData = swap[20:];
}
/// The OutToken index into an array of tokens
function tokenOutIndex(
bytes calldata swap
) internal pure returns (uint8 res) {
res = uint8(swap[1]);
/**
* @dev Returns arguments required to perform a sequential swap
*/
function decodeSequentialSwap(bytes calldata swap)
internal
pure
returns (address executor, bytes calldata protocolData)
{
executor = address(uint160(bytes20(swap[0:20])));
protocolData = swap[20:];
}
/// 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])));
}
/// Remaining bytes are interpreted as protocol data
function protocolData(
bytes calldata swap
) internal pure returns (bytes calldata res) {
res = swap[25:];
/**
* @dev Returns arguments required to perform a split swap
*/
function decodeSplitSwap(bytes calldata swap)
internal
pure
returns (uint8 tokenInIndex, uint8 tokenOutIndex, uint24 split, address executor, bytes calldata protocolData)
{
tokenInIndex = uint8(swap[0]);
tokenOutIndex = uint8(swap[1]);
split = uint24(bytes3(swap[2:5]));
executor = address(uint160(bytes20(swap[5:25])));
protocolData = swap[25:];
}
}

View File

@@ -4,84 +4,123 @@ const hre = require("hardhat");
// Comment out the executors you don't want to deploy
const executors_to_deploy = {
"ethereum":[
// USV2 - Args: Factory, Pool Init Code Hash
{exchange: "UniswapV2Executor", args: [
"0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"
]},
// SUSHISWAP - Args: Factory, Pool Init Code Hash
{exchange: "UniswapV2Executor", args: [
"0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac",
"0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303"
]},
// PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash
{exchange: "UniswapV2Executor", args: [
"0x1097053Fd2ea711dad45caCcc45EfF7548fCB362",
"0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d"
]},
// USV3 -Args: Factory, Pool Init Code Hash
{exchange: "UniswapV3Executor", args: [
"0x1F98431c8aD98523631AE4a59f267346ea31F984",
"0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"
]},
// PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash
{exchange: "UniswapV3Executor", args: [
"0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9",
"0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2"
]},
// Args: Pool manager
{exchange: "UniswapV4Executor", args: ["0x000000000004444c5dc75cB358380D2e3dE08A90"]},
{exchange: "BalancerV2Executor", args: []},
// Args: Ekubo core contract
{exchange: "EkuboExecutor", args: [
"0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444"
]}
],
"base":[
// Args: Factory, Pool Init Code Hash
{exchange: "UniswapV2Executor", args: [
"0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6",
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"
]},
// SUSHISWAP V2 - Args: Factory, Pool Init Code Hash
{exchange: "UniswapV2Executor", args: [
"0x71524B4f93c58fcbF659783284E38825f0622859",
"0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303"
]},
// PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash
{exchange: "UniswapV2Executor", args: [
"0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E",
"0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d"
]},
// USV3 - Args: Factory, Pool Init Code Hash
{exchange: "UniswapV3Executor", args: [
"0x33128a8fC17869897dcE68Ed026d694621f6FDfD",
"0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"
]},
// PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash
{exchange: "UniswapV3Executor", args: [
"0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9",
"0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2"
]},
// Args: Pool manager
{exchange: "UniswapV4Executor", args: ["0x498581ff718922c3f8e6a244956af099b2652b2b"]},
{exchange: "BalancerV2Executor", args: []},
],
"unichain":[
// Args: Factory, Pool Init Code Hash
{exchange: "UniswapV2Executor", args: [
"0x1f98400000000000000000000000000000000002",
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"
]},
// USV3 - Args: Factory, Pool Init Code Hash
{exchange: "UniswapV3Executor", args: [
"0x1f98400000000000000000000000000000000003",
"0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"
]},
// Args: Pool manager
{exchange: "UniswapV4Executor", args: ["0x1f98400000000000000000000000000000000004"]},
],
"ethereum": [
// USV2 - Args: Factory, Pool Init Code Hash, Fee BPS
{
exchange: "UniswapV2Executor", args: [
"0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
30
]
},
// SUSHISWAP - Args: Factory, Pool Init Code Hash, Fee BPS, Fee BPS
{
exchange: "UniswapV2Executor", args: [
"0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac",
"0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303",
30
]
},
// PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash, Fee BPS
{
exchange: "UniswapV2Executor", args: [
"0x1097053Fd2ea711dad45caCcc45EfF7548fCB362",
"0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d",
25
]
},
// USV3 -Args: Factory, Pool Init Code Hash
{
exchange: "UniswapV3Executor", args: [
"0x1F98431c8aD98523631AE4a59f267346ea31F984",
"0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"
]
},
// PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash
{
exchange: "UniswapV3Executor", args: [
"0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9",
"0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2"
]
},
// Args: Pool manager
{exchange: "UniswapV4Executor", args: ["0x000000000004444c5dc75cB358380D2e3dE08A90"]},
{exchange: "BalancerV2Executor", args: []},
// Args: Ekubo core contract
{
exchange: "EkuboExecutor", args: [
"0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444"
]
},
// Args: ETH address in curve pools
{
exchange: "CurveExecutor", args: [
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
]
}
],
"base": [
// Args: Factory, Pool Init Code Hash, Fee BPS
{
exchange: "UniswapV2Executor", args: [
"0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6",
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
30
]
},
// SUSHISWAP V2 - Args: Factory, Pool Init Code Hash, Fee BPS
{
exchange: "UniswapV2Executor", args: [
"0x71524B4f93c58fcbF659783284E38825f0622859",
"0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303",
30
]
},
// PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash, Fee BPS
{
exchange: "UniswapV2Executor", args: [
"0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E",
"0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d",
25
]
},
// USV3 - Args: Factory, Pool Init Code Hash
{
exchange: "UniswapV3Executor", args: [
"0x33128a8fC17869897dcE68Ed026d694621f6FDfD",
"0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"
]
},
// PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash
{
exchange: "UniswapV3Executor", args: [
"0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9",
"0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2"
]
},
// Args: Pool manager
{exchange: "UniswapV4Executor", args: ["0x498581ff718922c3f8e6a244956af099b2652b2b"]},
{exchange: "BalancerV2Executor", args: []},
],
"unichain": [
// Args: Factory, Pool Init Code Hash, Fee BPS
{
exchange: "UniswapV2Executor", args: [
"0x1f98400000000000000000000000000000000002",
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
30
]
},
// USV3 - Args: Factory, Pool Init Code Hash
{
exchange: "UniswapV3Executor", args: [
"0x1f98400000000000000000000000000000000003",
"0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"
]
},
// Args: Pool manager
{exchange: "UniswapV4Executor", args: ["0x1f98400000000000000000000000000000000004"]},
],
}
async function main() {

View File

@@ -4,7 +4,7 @@ pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol";
import "@interfaces/ICallback.sol";
error Dispatcher__UnapprovedExecutor();
error Dispatcher__UnapprovedExecutor(address executor);
error Dispatcher__NonContractExecutor();
error Dispatcher__InvalidDataLength();
@@ -23,6 +23,10 @@ error Dispatcher__InvalidDataLength();
contract Dispatcher {
mapping(address => bool) public executors;
// keccak256("Dispatcher#CURRENTLY_SWAPPING_EXECUTOR_SLOT")
uint256 private constant _CURRENTLY_SWAPPING_EXECUTOR_SLOT =
0x098a7a3b47801589e8cdf9ec791b93ad44273246946c32ef1fc4dbe45390c80e;
event ExecutorSet(address indexed executor);
event ExecutorRemoved(address indexed executor);
@@ -52,21 +56,30 @@ contract Dispatcher {
* @dev Calls an executor, assumes swap.protocolData contains
* protocol-specific data required by the executor.
*/
// slither-disable-next-line delegatecall-loop
function _callExecutor(
// slither-disable-next-line delegatecall-loop,assembly
function _callSwapOnExecutor(
address executor,
uint256 amount,
bytes calldata data
) internal returns (uint256 calculatedAmount) {
if (!executors[executor]) {
revert Dispatcher__UnapprovedExecutor();
revert Dispatcher__UnapprovedExecutor(executor);
}
// slither-disable-next-line controlled-delegatecall,low-level-calls
assembly {
tstore(_CURRENTLY_SWAPPING_EXECUTOR_SLOT, executor)
}
// slither-disable-next-line controlled-delegatecall,low-level-calls,calls-loop
(bool success, bytes memory result) = executor.delegatecall(
abi.encodeWithSelector(IExecutor.swap.selector, amount, data)
);
// Clear transient storage in case no callback was performed
assembly {
tstore(_CURRENTLY_SWAPPING_EXECUTOR_SLOT, 0)
}
if (!success) {
revert(
string(
@@ -80,11 +93,18 @@ contract Dispatcher {
calculatedAmount = abi.decode(result, (uint256));
}
function _handleCallback(bytes calldata data) internal {
address executor = address(uint160(bytes20(data[data.length - 20:])));
// slither-disable-next-line assembly
function _callHandleCallbackOnExecutor(bytes calldata data)
internal
returns (bytes memory)
{
address executor;
assembly {
executor := tload(_CURRENTLY_SWAPPING_EXECUTOR_SLOT)
}
if (!executors[executor]) {
revert Dispatcher__UnapprovedExecutor();
revert Dispatcher__UnapprovedExecutor(executor);
}
// slither-disable-next-line controlled-delegatecall,low-level-calls
@@ -101,5 +121,14 @@ contract Dispatcher {
)
);
}
// to prevent multiple callbacks
assembly {
tstore(_CURRENTLY_SWAPPING_EXECUTOR_SLOT, 0)
}
// this is necessary because the delegatecall will prepend extra bytes we don't want like the length and prefix
bytes memory decodedResult = abi.decode(result, (bytes));
return decodedResult;
}
}

View File

@@ -58,7 +58,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
error TychoRouter__AddressZero();
error TychoRouter__EmptySwaps();
error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount);
error TychoRouter__AmountInDiffersFromConsumed(
error TychoRouter__AmountOutNotFullyReceived(
uint256 amountIn, uint256 amountConsumed
);
error TychoRouter__MessageValueMismatch(uint256 value, uint256 amount);
@@ -76,8 +76,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
//keccak256("NAME_OF_ROLE") : save gas on deployment
bytes32 public constant EXECUTOR_SETTER_ROLE =
0x6a1dd52dcad5bd732e45b6af4e7344fa284e2d7d4b23b5b09cb55d36b0685c87;
bytes32 public constant FEE_SETTER_ROLE =
0xe6ad9a47fbda1dc18de1eb5eeb7d935e5e81b4748f3cfc61e233e64f88182060;
bytes32 public constant PAUSER_ROLE =
0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a;
bytes32 public constant UNPAUSER_ROLE =
@@ -85,19 +83,9 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
bytes32 public constant FUND_RESCUER_ROLE =
0x912e45d663a6f4cc1d0491d8f046e06c616f40352565ea1cdb86a0e1aaefa41b;
address public feeReceiver;
// Fee should be expressed in basis points (1/100th of a percent)
// For example, 100 = 1%, 500 = 5%, 1000 = 10%
uint256 public fee;
event Withdrawal(
address indexed token, uint256 amount, address indexed receiver
);
event FeeReceiverSet(
address indexed oldFeeReceiver, address indexed newFeeReceiver
);
event FeeSet(uint256 indexed oldFee, uint256 indexed newFee);
constructor(address _permit2, address weth) {
if (_permit2 == address(0) || weth == address(0)) {
@@ -111,13 +99,12 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
/**
* @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. This function performs a transferFrom to retrieve tokens from the caller.
* 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.
* - Swaps are executed sequentially using the `_swap` 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 greater than 0.
*
* @param amountIn The input token amount to be swapped.
@@ -130,9 +117,9 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
* @param receiver The address to receive the output tokens.
* @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.
* @return amountOut The total amount of the output token received by the receiver.
*/
function swap(
function splitSwap(
uint256 amountIn,
address tokenIn,
address tokenOut,
@@ -143,12 +130,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
address receiver,
bytes calldata swaps
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
if (address(tokenIn) != address(0)) {
IERC20(tokenIn).safeTransferFrom(
msg.sender, address(this), amountIn
);
}
return _swapChecked(
return _splitSwapChecked(
amountIn,
tokenIn,
tokenOut,
@@ -171,7 +153,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
* - 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 `_swap` 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 greater than 0.
*
* @param amountIn The input token amount to be swapped.
@@ -186,9 +167,9 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
* @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.
* @return amountOut The total amount of the output token received by the receiver.
*/
function swapPermit2(
function splitSwapPermit2(
uint256 amountIn,
address tokenIn,
address tokenOut,
@@ -201,18 +182,12 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
bytes calldata signature,
bytes calldata swaps
) external payable whenNotPaused nonReentrant returns (uint256 amountOut) {
// For native ETH, assume funds already in our router. Else, transfer and handle approval.
// For native ETH, assume funds already in our router. Else, handle approval.
if (tokenIn != address(0)) {
permit2.permit(msg.sender, permitSingle, signature);
permit2.transferFrom(
msg.sender,
address(this),
uint160(amountIn),
permitSingle.details.token
);
}
return _swapChecked(
return _splitSwapChecked(
amountIn,
tokenIn,
tokenOut,
@@ -226,14 +201,206 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
}
/**
* @notice Internal implementation of the core swap logic shared between swap() and swapPermit2().
* @notice Executes a swap operation based on a predefined swap graph with no split routes.
* 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.
* - Swaps are executed sequentially using the `_swap` function.
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater 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
* @param tokenOut The address of the output token. Use `address(0)` for native ETH
* @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage.
* @param wrapEth If true, wraps the input token (native ETH) into WETH.
* @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver.
* @param receiver The address to receive the output tokens.
* @param swaps Encoded swap graph data containing details of each swap.
*
* @return amountOut The total amount of the output token received by the receiver.
*/
function sequentialSwap(
uint256 amountIn,
address tokenIn,
address tokenOut,
uint256 minAmountOut,
bool wrapEth,
bool unwrapEth,
address receiver,
bytes calldata swaps
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
return _sequentialSwapChecked(
amountIn,
tokenIn,
tokenOut,
minAmountOut,
wrapEth,
unwrapEth,
receiver,
swaps
);
}
/**
* @notice Executes a swap operation based on a predefined swap graph with no split routes.
* 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.
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater 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
* @param tokenOut The address of the output token. Use `address(0)` for native ETH
* @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage.
* @param wrapEth If true, wraps the input token (native ETH) into WETH.
* @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver.
* @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.
*/
function sequentialSwapPermit2(
uint256 amountIn,
address tokenIn,
address tokenOut,
uint256 minAmountOut,
bool wrapEth,
bool unwrapEth,
address receiver,
IAllowanceTransfer.PermitSingle calldata permitSingle,
bytes calldata signature,
bytes calldata swaps
) external payable whenNotPaused nonReentrant returns (uint256 amountOut) {
// For native ETH, assume funds already in our router. Else, handle approval.
if (tokenIn != address(0)) {
permit2.permit(msg.sender, permitSingle, signature);
}
return _sequentialSwapChecked(
amountIn,
tokenIn,
tokenOut,
minAmountOut,
wrapEth,
unwrapEth,
receiver,
swaps
);
}
/**
* @notice Executes a single swap operation.
* This function enables 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.
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater 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
* @param tokenOut The address of the output token. Use `address(0)` for native ETH
* @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage.
* @param wrapEth If true, wraps the input token (native ETH) into WETH.
* @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver.
* @param receiver The address to receive the output tokens.
* @param swapData Encoded swap details.
*
* @return amountOut The total amount of the output token received by the receiver.
*/
function singleSwap(
uint256 amountIn,
address tokenIn,
address tokenOut,
uint256 minAmountOut,
bool wrapEth,
bool unwrapEth,
address receiver,
bytes calldata swapData
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
return _singleSwap(
amountIn,
tokenIn,
tokenOut,
minAmountOut,
wrapEth,
unwrapEth,
receiver,
swapData
);
}
/**
* @notice Executes a single swap operation.
* This function enables 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.
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater 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
* @param tokenOut The address of the output token. Use `address(0)` for native ETH
* @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage.
* @param wrapEth If true, wraps the input token (native ETH) into WETH.
* @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver.
* @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 swapData Encoded swap details.
*
* @return amountOut The total amount of the output token received by the receiver.
*/
function singleSwapPermit2(
uint256 amountIn,
address tokenIn,
address tokenOut,
uint256 minAmountOut,
bool wrapEth,
bool unwrapEth,
address receiver,
IAllowanceTransfer.PermitSingle calldata permitSingle,
bytes calldata signature,
bytes calldata swapData
) external payable whenNotPaused nonReentrant returns (uint256 amountOut) {
// For native ETH, assume funds already in our router. Else, handle approval.
if (tokenIn != address(0)) {
permit2.permit(msg.sender, permitSingle, signature);
}
return _singleSwap(
amountIn,
tokenIn,
tokenOut,
minAmountOut,
wrapEth,
unwrapEth,
receiver,
swapData
);
}
/**
* @notice Internal implementation of the core swap logic shared between splitSwap() and splitSwapPermit2().
*
* @notice This function centralizes the swap execution logic.
* @notice For detailed documentation on parameters and behavior, see the documentation for
* swap() and swapPermit2() functions.
* splitSwap() and splitSwapPermit2() functions.
*
*/
function _swapChecked(
function _splitSwapChecked(
uint256 amountIn,
address tokenIn,
address tokenOut,
@@ -257,28 +424,8 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
tokenIn = address(_weth);
}
uint256 initialBalance = tokenIn == address(0)
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
amountOut = _swap(amountIn, nTokens, swaps);
uint256 currentBalance = tokenIn == address(0)
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
uint256 amountConsumed = initialBalance - currentBalance;
if (tokenIn != tokenOut && amountConsumed != amountIn) {
revert TychoRouter__AmountInDiffersFromConsumed(
amountIn, amountConsumed
);
}
if (fee > 0) {
uint256 feeAmount = (amountOut * fee) / 10000;
amountOut -= feeAmount;
IERC20(tokenOut).safeTransfer(feeReceiver, feeAmount);
}
uint256 initialBalanceTokenOut = _balanceOf(tokenOut, receiver);
amountOut = _splitSwap(amountIn, nTokens, swaps);
if (amountOut < minAmountOut) {
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
@@ -286,11 +433,128 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
if (unwrapEth) {
_unwrapETH(amountOut);
}
if (tokenOut == address(0)) {
Address.sendValue(payable(receiver), amountOut);
} else {
IERC20(tokenOut).safeTransfer(receiver, amountOut);
}
if (tokenIn != tokenOut) {
uint256 currentBalanceTokenOut = _balanceOf(tokenOut, receiver);
uint256 userAmount = currentBalanceTokenOut - initialBalanceTokenOut;
if (userAmount != amountOut) {
revert TychoRouter__AmountOutNotFullyReceived(
userAmount, amountOut
);
}
}
}
/**
* @notice Internal implementation of the core swap logic shared between singleSwap() and singleSwapPermit2().
*
* @notice This function centralizes the swap execution logic.
* @notice For detailed documentation on parameters and behavior, see the documentation for
* singleSwap() and singleSwapPermit2() functions.
*
*/
function _singleSwap(
uint256 amountIn,
address tokenIn,
address tokenOut,
uint256 minAmountOut,
bool wrapEth,
bool unwrapEth,
address receiver,
bytes calldata swap_
) internal returns (uint256 amountOut) {
if (receiver == address(0)) {
revert TychoRouter__AddressZero();
}
if (minAmountOut == 0) {
revert TychoRouter__UndefinedMinAmountOut();
}
// Assume funds are already in the router.
if (wrapEth) {
_wrapETH(amountIn);
tokenIn = address(_weth);
}
(address executor, bytes calldata protocolData) =
swap_.decodeSingleSwap();
uint256 initialBalanceTokenOut = _balanceOf(tokenOut, receiver);
amountOut = _callSwapOnExecutor(executor, amountIn, protocolData);
if (amountOut < minAmountOut) {
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
}
if (unwrapEth) {
_unwrapETH(amountOut);
Address.sendValue(payable(receiver), amountOut);
}
if (tokenIn != tokenOut) {
uint256 currentBalanceTokenOut = _balanceOf(tokenOut, receiver);
uint256 userAmount = currentBalanceTokenOut - initialBalanceTokenOut;
if (userAmount != amountOut) {
revert TychoRouter__AmountOutNotFullyReceived(
userAmount, amountOut
);
}
}
}
/**
* @notice Internal implementation of the core swap logic shared between sequentialSwap() and sequentialSwapPermit2().
*
* @notice This function centralizes the swap execution logic.
* @notice For detailed documentation on parameters and behavior, see the documentation for
* sequentialSwap() and sequentialSwapPermit2() functions.
*
*/
function _sequentialSwapChecked(
uint256 amountIn,
address tokenIn,
address tokenOut,
uint256 minAmountOut,
bool wrapEth,
bool unwrapEth,
address receiver,
bytes calldata swaps
) internal returns (uint256 amountOut) {
if (receiver == address(0)) {
revert TychoRouter__AddressZero();
}
if (minAmountOut == 0) {
revert TychoRouter__UndefinedMinAmountOut();
}
// Assume funds are already in the router.
if (wrapEth) {
_wrapETH(amountIn);
tokenIn = address(_weth);
}
uint256 initialBalanceTokenOut = _balanceOf(tokenOut, receiver);
amountOut = _sequentialSwap(amountIn, swaps);
if (amountOut < minAmountOut) {
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
}
if (unwrapEth) {
_unwrapETH(amountOut);
Address.sendValue(payable(receiver), amountOut);
}
if (tokenIn != tokenOut) {
uint256 currentBalanceTokenOut = _balanceOf(tokenOut, receiver);
uint256 userAmount = currentBalanceTokenOut - initialBalanceTokenOut;
if (userAmount != amountOut) {
revert TychoRouter__AmountOutNotFullyReceived(
userAmount, amountOut
);
}
}
}
@@ -317,10 +581,11 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
*
* @return The total amount of the buy token obtained after all swaps have been executed.
*/
function _swap(uint256 amountIn, uint256 nTokens, bytes calldata swaps_)
internal
returns (uint256)
{
function _splitSwap(
uint256 amountIn,
uint256 nTokens,
bytes calldata swaps_
) internal returns (uint256) {
if (swaps_.length == 0) {
revert TychoRouter__EmptySwaps();
}
@@ -330,6 +595,8 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
uint8 tokenInIndex = 0;
uint8 tokenOutIndex = 0;
uint24 split;
address executor;
bytes calldata protocolData;
bytes calldata swapData;
uint256[] memory remainingAmounts = new uint256[](nTokens);
@@ -340,17 +607,16 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
while (swaps_.length > 0) {
(swapData, swaps_) = swaps_.next();
tokenInIndex = swapData.tokenInIndex();
tokenOutIndex = swapData.tokenOutIndex();
split = swapData.splitPercentage();
(tokenInIndex, tokenOutIndex, split, executor, protocolData) =
swapData.decodeSplitSwap();
currentAmountIn = split > 0
? (amounts[tokenInIndex] * split) / 0xffffff
: remainingAmounts[tokenInIndex];
currentAmountOut = _callExecutor(
swapData.executor(), currentAmountIn, swapData.protocolData()
);
currentAmountOut =
_callSwapOnExecutor(executor, currentAmountIn, protocolData);
// Checks if the output token is the same as the input token
if (tokenOutIndex == 0) {
cyclicSwapAmountOut += currentAmountOut;
@@ -363,11 +629,41 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
return tokenOutIndex == 0 ? cyclicSwapAmountOut : amounts[tokenOutIndex];
}
/**
* @dev Executes sequential swaps as defined by the provided swap graph.
*
* @param amountIn The initial amount of the sell token to be swapped.
* @param swaps_ Encoded swap graph data containing the details of each swap operation.
*
* @return calculatedAmount The total amount of the buy token obtained after all swaps have been executed.
*/
function _sequentialSwap(uint256 amountIn, bytes calldata swaps_)
internal
returns (uint256 calculatedAmount)
{
bytes calldata swap;
calculatedAmount = amountIn;
while (swaps_.length > 0) {
(swap, swaps_) = swaps_.next();
(address executor, bytes calldata protocolData) =
swap.decodeSingleSwap();
calculatedAmount =
_callSwapOnExecutor(executor, calculatedAmount, protocolData);
}
}
/**
* @dev We use the fallback function to allow flexibility on callback.
*/
fallback() external {
_handleCallback(msg.data);
bytes memory result = _callHandleCallbackOnExecutor(msg.data);
// slither-disable-next-line assembly
assembly ("memory-safe") {
// Propagate the calculatedAmount
return(add(result, 32), 16)
}
}
/**
@@ -420,26 +716,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
_removeExecutor(target);
}
/**
* @dev Allows setting the fee receiver.
*/
function setFeeReceiver(address newfeeReceiver)
external
onlyRole(FEE_SETTER_ROLE)
{
if (newfeeReceiver == address(0)) revert TychoRouter__AddressZero();
emit FeeReceiverSet(feeReceiver, newfeeReceiver);
feeReceiver = newfeeReceiver;
}
/**
* @dev Allows setting the fee.
*/
function setFee(uint256 newFee) external onlyRole(FEE_SETTER_ROLE) {
emit FeeSet(fee, newFee);
fee = newFee;
}
/**
* @dev Allows withdrawing any ERC20 funds if funds get stuck in case of a bug.
*/
@@ -502,45 +778,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
require(msg.sender.code.length != 0);
}
/**
* @dev Called by UniswapV3 pool when swapping on it.
* See in IUniswapV3SwapCallback for documentation.
*/
function uniswapV3SwapCallback(
int256, /* amount0Delta */
int256, /* amount1Delta */
bytes calldata data
) external {
if (data.length < 24) revert TychoRouter__InvalidDataLength();
// We are taking advantage of the fact that the data we need is already encoded in the correct format inside msg.data
// This way we preserve the bytes calldata (and don't need to convert it to bytes memory)
uint256 dataOffset = 4 + 32 + 32 + 32; // Skip selector + 2 ints + data_offset
uint256 dataLength =
uint256(bytes32(msg.data[dataOffset:dataOffset + 32]));
bytes calldata fullData = msg.data[4:dataOffset + 32 + dataLength];
_handleCallback(fullData);
}
/**
* @dev Called by PancakeV3 pool when swapping on it.
*/
function pancakeV3SwapCallback(
int256, /* amount0Delta */
int256, /* amount1Delta */
bytes calldata data
) external {
if (data.length < 24) revert TychoRouter__InvalidDataLength();
// We are taking advantage of the fact that the data we need is already encoded in the correct format inside msg.data
// This way we preserve the bytes calldata (and don't need to convert it to bytes memory)
uint256 dataOffset = 4 + 32 + 32 + 32; // Skip selector + 2 ints + data_offset
uint256 dataLength =
uint256(bytes32(msg.data[dataOffset:dataOffset + 32]));
bytes calldata fullData = msg.data[4:dataOffset + 32 + dataLength];
_handleCallback(fullData);
}
/**
* @dev Called by UniswapV4 pool manager after achieving unlock state.
*/
@@ -549,47 +786,19 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
returns (bytes memory)
{
if (data.length < 24) revert TychoRouter__InvalidDataLength();
_handleCallback(data);
return "";
bytes memory result = _callHandleCallbackOnExecutor(data);
return result;
}
function locked(uint256) external {
address executor = address(0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D);
// slither-disable-next-line controlled-delegatecall,low-level-calls
(bool success, bytes memory result) = executor.delegatecall(msg.data);
if (!success) {
revert(
string(
result.length > 0
? result
: abi.encodePacked("Callback failed")
)
);
}
// slither-disable-next-line assembly
assembly ("memory-safe") {
// Propagate the swappedAmount
return(add(result, 32), 16)
}
}
function payCallback(uint256, address /*token*/ ) external {
address executor = address(0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D);
// slither-disable-next-line controlled-delegatecall,low-level-calls
(bool success, bytes memory result) = executor.delegatecall(msg.data);
if (!success) {
revert(
string(
result.length > 0
? result
: abi.encodePacked("Callback failed")
)
);
}
/**
* @dev Gets balance of a token for a given address. Supports both native ETH and ERC20 tokens.
*/
function _balanceOf(address token, address owner)
internal
view
returns (uint256)
{
return
token == address(0) ? owner.balance : IERC20(token).balanceOf(owner);
}
}

View File

@@ -10,14 +10,17 @@ import {
import {IAsset} from "@balancer-labs/v2-interfaces/contracts/vault/IAsset.sol";
// slither-disable-next-line solc-version
import {IVault} from "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol";
import {TokenTransfer} from "./TokenTransfer.sol";
error BalancerV2Executor__InvalidDataLength();
contract BalancerV2Executor is IExecutor {
contract BalancerV2Executor is IExecutor, TokenTransfer {
using SafeERC20 for IERC20;
address private constant VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
constructor(address _permit2) TokenTransfer(_permit2) {}
// slither-disable-next-line locked-ether
function swap(uint256 givenAmount, bytes calldata data)
external
@@ -29,12 +32,23 @@ contract BalancerV2Executor is IExecutor {
IERC20 tokenOut,
bytes32 poolId,
address receiver,
bool needsApproval
bool needsApproval,
TransferType transferType
) = _decodeData(data);
_transfer(
address(tokenIn),
msg.sender,
// Receiver can never be the pool, since the pool expects funds in the router contract
// Thus, this call will only ever be used to transfer funds from the user into the router.
address(this),
givenAmount,
transferType
);
if (needsApproval) {
// slither-disable-next-line unused-return
tokenIn.approve(VAULT, type(uint256).max);
tokenIn.forceApprove(VAULT, type(uint256).max);
}
IVault.SingleSwap memory singleSwap = IVault.SingleSwap({
@@ -67,10 +81,11 @@ contract BalancerV2Executor is IExecutor {
IERC20 tokenOut,
bytes32 poolId,
address receiver,
bool needsApproval
bool needsApproval,
TransferType transferType
)
{
if (data.length != 93) {
if (data.length != 94) {
revert BalancerV2Executor__InvalidDataLength();
}
@@ -79,5 +94,6 @@ contract BalancerV2Executor is IExecutor {
poolId = bytes32(data[40:72]);
receiver = address(bytes20(data[72:92]));
needsApproval = uint8(data[92]) > 0;
transferType = TransferType(uint8(data[93]));
}
}

View File

@@ -3,8 +3,11 @@ pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./TokenTransfer.sol";
import "@openzeppelin/contracts/utils/Address.sol";
error CurveExecutor__AddressZero();
error CurveExecutor__InvalidDataLength();
interface CryptoPool {
// slither-disable-next-line naming-convention
@@ -32,12 +35,14 @@ interface CryptoPoolETH {
// slither-disable-end naming-convention
}
contract CurveExecutor is IExecutor {
contract CurveExecutor is IExecutor, TokenTransfer {
using SafeERC20 for IERC20;
address public immutable nativeToken;
constructor(address _nativeToken) {
constructor(address _nativeToken, address _permit2)
TokenTransfer(_permit2)
{
if (_nativeToken == address(0)) {
revert CurveExecutor__AddressZero();
}
@@ -50,6 +55,8 @@ contract CurveExecutor is IExecutor {
payable
returns (uint256)
{
if (data.length != 85) revert CurveExecutor__InvalidDataLength();
(
address tokenIn,
address tokenOut,
@@ -57,12 +64,24 @@ contract CurveExecutor is IExecutor {
uint8 poolType,
int128 i,
int128 j,
bool tokenApprovalNeeded
bool tokenApprovalNeeded,
TransferType transferType,
address receiver
) = _decodeData(data);
_transfer(
tokenIn,
msg.sender,
// Receiver can never be the pool, since the pool expects funds in the router contract
// Thus, this call will only ever be used to transfer funds from the user into the router.
address(this),
amountIn,
transferType
);
if (tokenApprovalNeeded && tokenIn != nativeToken) {
// slither-disable-next-line unused-return
IERC20(tokenIn).approve(address(pool), type(uint256).max);
IERC20(tokenIn).forceApprove(address(pool), type(uint256).max);
}
/// Inspired by Curve's router contract: https://github.com/curvefi/curve-router-ng/blob/9ab006ca848fc7f1995b6fbbecfecc1e0eb29e2a/contracts/Router.vy#L44
@@ -92,7 +111,16 @@ contract CurveExecutor is IExecutor {
}
uint256 balanceAfter = _balanceOf(tokenOut);
return balanceAfter - balanceBefore;
uint256 amountOut = balanceAfter - balanceBefore;
if (receiver != address(this)) {
if (tokenOut == nativeToken) {
Address.sendValue(payable(receiver), amountOut);
} else {
IERC20(tokenOut).safeTransfer(receiver, amountOut);
}
}
return amountOut;
}
function _decodeData(bytes calldata data)
@@ -105,7 +133,9 @@ contract CurveExecutor is IExecutor {
uint8 poolType,
int128 i,
int128 j,
bool tokenApprovalNeeded
bool tokenApprovalNeeded,
TransferType transferType,
address receiver
)
{
tokenIn = address(bytes20(data[0:20]));
@@ -115,6 +145,8 @@ contract CurveExecutor is IExecutor {
i = int128(uint128(uint8(data[61])));
j = int128(uint128(uint8(data[62])));
tokenApprovalNeeded = data[63] != 0;
transferType = TransferType(uint8(data[64]));
receiver = address(bytes20(data[65:85]));
}
receive() external payable {

View File

@@ -3,6 +3,7 @@ pragma solidity ^0.8.26;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IExecutor} from "@interfaces/IExecutor.sol";
import {ICallback} from "@interfaces/ICallback.sol";
import {ICore} from "@ekubo/interfaces/ICore.sol";
import {ILocker, IPayer} from "@ekubo/interfaces/IFlashAccountant.sol";
import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol";
@@ -10,18 +11,28 @@ import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol";
import {LibBytes} from "@solady/utils/LibBytes.sol";
import {Config, EkuboPoolKey} from "@ekubo/types/poolKey.sol";
import {MAX_SQRT_RATIO, MIN_SQRT_RATIO} from "@ekubo/types/sqrtRatio.sol";
import {TokenTransfer} from "./TokenTransfer.sol";
contract EkuboExecutor is IExecutor, ILocker, IPayer {
contract EkuboExecutor is
IExecutor,
ILocker,
IPayer,
ICallback,
TokenTransfer
{
error EkuboExecutor__InvalidDataLength();
error EkuboExecutor__CoreOnly();
error EkuboExecutor__UnknownCallback();
ICore immutable core;
uint256 constant POOL_DATA_OFFSET = 92;
uint256 constant POOL_DATA_OFFSET = 77;
uint256 constant HOP_BYTE_LEN = 52;
constructor(address _core) {
bytes4 constant LOCKED_SELECTOR = 0xb45a3c0e; // locked(uint256)
bytes4 constant PAY_CALLBACK_SELECTOR = 0x599d0714; // payCallback(uint256,address)
constructor(address _core, address _permit2) TokenTransfer(_permit2) {
core = ICore(_core);
}
@@ -30,67 +41,57 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer {
payable
returns (uint256 calculatedAmount)
{
if (data.length < 92) revert EkuboExecutor__InvalidDataLength();
if (data.length < 93) revert EkuboExecutor__InvalidDataLength();
// amountIn must be at most type(int128).MAX
calculatedAmount =
uint256(_lock(bytes.concat(bytes16(uint128(amountIn)), data)));
calculatedAmount = uint256(
_lock(
bytes.concat(
bytes16(uint128(amountIn)), bytes20(msg.sender), data
)
)
);
}
function locked(uint256) external coreOnly {
int128 nextAmountIn = int128(uint128(bytes16(msg.data[36:52])));
uint128 tokenInDebtAmount = uint128(nextAmountIn);
function handleCallback(bytes calldata raw)
external
returns (bytes memory)
{
verifyCallback(raw);
address receiver = address(bytes20(msg.data[52:72]));
address tokenIn = address(bytes20(msg.data[72:POOL_DATA_OFFSET]));
// Without selector and locker id
bytes calldata stripped = raw[36:];
address nextTokenIn = tokenIn;
bytes4 selector = bytes4(raw[:4]);
uint256 hopsLength = (msg.data.length - POOL_DATA_OFFSET) / HOP_BYTE_LEN;
uint256 offset = POOL_DATA_OFFSET;
for (uint256 i = 0; i < hopsLength; i++) {
address nextTokenOut =
address(bytes20(LibBytes.loadCalldata(msg.data, offset)));
Config poolConfig =
Config.wrap(LibBytes.loadCalldata(msg.data, offset + 20));
(address token0, address token1, bool isToken1) = nextTokenIn
> nextTokenOut
? (nextTokenOut, nextTokenIn, true)
: (nextTokenIn, nextTokenOut, false);
// slither-disable-next-line calls-loop
(int128 delta0, int128 delta1) = core.swap_611415377(
EkuboPoolKey(token0, token1, poolConfig),
nextAmountIn,
isToken1,
isToken1 ? MAX_SQRT_RATIO : MIN_SQRT_RATIO,
0
);
nextTokenIn = nextTokenOut;
nextAmountIn = -(isToken1 ? delta0 : delta1);
offset += HOP_BYTE_LEN;
bytes memory result = "";
if (selector == LOCKED_SELECTOR) {
int128 calculatedAmount = _locked(stripped);
result = abi.encodePacked(calculatedAmount);
} else if (selector == PAY_CALLBACK_SELECTOR) {
_payCallback(stripped);
} else {
revert EkuboExecutor__UnknownCallback();
}
_pay(tokenIn, tokenInDebtAmount);
return result;
}
core.withdraw(nextTokenIn, receiver, uint128(nextAmountIn));
function verifyCallback(bytes calldata) public view coreOnly {}
function locked(uint256) external coreOnly {
// Without selector and locker id
int128 calculatedAmount = _locked(msg.data[36:]);
// slither-disable-next-line assembly
assembly ("memory-safe") {
mstore(0, nextAmountIn)
mstore(0, calculatedAmount)
return(0x10, 16)
}
}
function payCallback(uint256, address token) external coreOnly {
uint128 amount = uint128(bytes16(msg.data[68:84]));
SafeTransferLib.safeTransfer(token, address(core), amount);
function payCallback(uint256, address /*token*/ ) external coreOnly {
// Without selector and locker id
_payCallback(msg.data[36:]);
}
function _lock(bytes memory data)
@@ -121,7 +122,58 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer {
}
}
function _pay(address token, uint128 amount) internal {
function _locked(bytes calldata swapData) internal returns (int128) {
int128 nextAmountIn = int128(uint128(bytes16(swapData[0:16])));
uint128 tokenInDebtAmount = uint128(nextAmountIn);
address sender = address(bytes20(swapData[16:36]));
uint8 transferType = uint8(swapData[36]);
address receiver = address(bytes20(swapData[37:57]));
address tokenIn = address(bytes20(swapData[57:77]));
address nextTokenIn = tokenIn;
uint256 hopsLength = (swapData.length - POOL_DATA_OFFSET) / HOP_BYTE_LEN;
uint256 offset = POOL_DATA_OFFSET;
for (uint256 i = 0; i < hopsLength; i++) {
address nextTokenOut =
address(bytes20(LibBytes.loadCalldata(swapData, offset)));
Config poolConfig =
Config.wrap(LibBytes.loadCalldata(swapData, offset + 20));
(address token0, address token1, bool isToken1) = nextTokenIn
> nextTokenOut
? (nextTokenOut, nextTokenIn, true)
: (nextTokenIn, nextTokenOut, false);
// slither-disable-next-line calls-loop
(int128 delta0, int128 delta1) = core.swap_611415377(
EkuboPoolKey(token0, token1, poolConfig),
nextAmountIn,
isToken1,
isToken1 ? MAX_SQRT_RATIO : MIN_SQRT_RATIO,
0
);
nextTokenIn = nextTokenOut;
nextAmountIn = -(isToken1 ? delta0 : delta1);
offset += HOP_BYTE_LEN;
}
_pay(tokenIn, tokenInDebtAmount, sender, transferType);
core.withdraw(nextTokenIn, receiver, uint128(nextAmountIn));
return nextAmountIn;
}
function _pay(
address token,
uint128 amount,
address sender,
uint8 transferType
) internal {
address target = address(core);
if (token == NATIVE_TOKEN_ADDRESS) {
@@ -134,9 +186,11 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer {
mstore(free, shl(224, 0x0c11dedd))
mstore(add(free, 4), token)
mstore(add(free, 36), shl(128, amount))
mstore(add(free, 52), shl(96, sender))
mstore(add(free, 72), shl(248, transferType))
// if it failed, pass through revert
if iszero(call(gas(), target, 0, free, 52, 0, 0)) {
// 4 (selector) + 32 (token) + 16 (amount) + 20 (sender) + 1 (transferType) = 73
if iszero(call(gas(), target, 0, free, 73, 0, 0)) {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
@@ -144,6 +198,14 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer {
}
}
function _payCallback(bytes calldata payData) internal {
address token = address(bytes20(payData[12:32])); // This arg is abi-encoded
uint128 amount = uint128(bytes16(payData[32:48]));
address sender = address(bytes20(payData[48:68]));
TransferType transferType = TransferType(uint8(payData[68]));
_transfer(token, sender, address(core), amount, transferType);
}
// To receive withdrawals from Core
receive() external payable {}

View File

@@ -0,0 +1,70 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@permit2/src/interfaces/IAllowanceTransfer.sol";
error TokenTransfer__AddressZero();
contract TokenTransfer {
using SafeERC20 for IERC20;
IAllowanceTransfer public immutable permit2;
enum TransferType {
// Assume funds are in the TychoRouter - transfer into the pool
TRANSFER_TO_PROTOCOL,
// Assume funds are in msg.sender's wallet - transferFrom into the pool
TRANSFER_FROM_TO_PROTOCOL,
// Assume funds are in msg.sender's wallet - permit2TransferFrom into the pool
TRANSFER_PERMIT2_TO_PROTOCOL,
// Assume funds are in msg.sender's wallet - but the pool requires it to be
// in the router contract when calling swap - transferFrom into the router
// contract
TRANSFER_FROM_TO_ROUTER,
// Assume funds are in msg.sender's wallet - but the pool requires it to be
// in the router contract when calling swap - transferFrom into the router
// contract using permit2
TRANSFER_PERMIT2_TO_ROUTER,
// Assume funds have already been transferred into the pool. Do nothing.
NONE
}
constructor(address _permit2) {
if (_permit2 == address(0)) {
revert TokenTransfer__AddressZero();
}
permit2 = IAllowanceTransfer(_permit2);
}
function _transfer(
address tokenIn,
address sender,
address receiver,
uint256 amount,
TransferType transferType
) internal {
if (transferType == TransferType.TRANSFER_TO_PROTOCOL) {
if (tokenIn == address(0)) {
payable(receiver).transfer(amount);
} else {
IERC20(tokenIn).safeTransfer(receiver, amount);
}
} else if (transferType == TransferType.TRANSFER_FROM_TO_PROTOCOL) {
// slither-disable-next-line arbitrary-send-erc20
IERC20(tokenIn).safeTransferFrom(sender, receiver, amount);
} else if (transferType == TransferType.TRANSFER_PERMIT2_TO_PROTOCOL) {
// Permit2.permit is already called from the TychoRouter
permit2.transferFrom(sender, receiver, uint160(amount), tokenIn);
} else if (transferType == TransferType.TRANSFER_FROM_TO_ROUTER) {
// slither-disable-next-line arbitrary-send-erc20
IERC20(tokenIn).safeTransferFrom(sender, address(this), amount);
} else if (transferType == TransferType.TRANSFER_PERMIT2_TO_ROUTER) {
// Permit2.permit is already called from the TychoRouter
permit2.transferFrom(
sender, address(this), uint160(amount), tokenIn
);
}
}
}

View File

@@ -4,20 +4,28 @@ pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@uniswap-v2/contracts/interfaces/IUniswapV2Pair.sol";
import "./TokenTransfer.sol";
error UniswapV2Executor__InvalidDataLength();
error UniswapV2Executor__InvalidTarget();
error UniswapV2Executor__InvalidFactory();
error UniswapV2Executor__InvalidInitCode();
error UniswapV2Executor__InvalidFee();
contract UniswapV2Executor is IExecutor {
contract UniswapV2Executor is IExecutor, TokenTransfer {
using SafeERC20 for IERC20;
address public immutable factory;
bytes32 public immutable initCode;
address private immutable self;
uint256 public immutable feeBps;
constructor(address _factory, bytes32 _initCode) {
constructor(
address _factory,
bytes32 _initCode,
address _permit2,
uint256 _feeBps
) TokenTransfer(_permit2) {
if (_factory == address(0)) {
revert UniswapV2Executor__InvalidFactory();
}
@@ -26,6 +34,10 @@ contract UniswapV2Executor is IExecutor {
}
factory = _factory;
initCode = _initCode;
if (_feeBps > 30) {
revert UniswapV2Executor__InvalidFee();
}
feeBps = _feeBps;
self = address(this);
}
@@ -35,17 +47,21 @@ contract UniswapV2Executor is IExecutor {
payable
returns (uint256 calculatedAmount)
{
IERC20 tokenIn;
address target;
address receiver;
bool zeroForOne;
IERC20 tokenIn;
TransferType transferType;
(tokenIn, target, receiver, zeroForOne) = _decodeData(data);
(tokenIn, target, receiver, zeroForOne, transferType) =
_decodeData(data);
_verifyPairAddress(target);
calculatedAmount = _getAmountOut(target, givenAmount, zeroForOne);
tokenIn.safeTransfer(target, givenAmount);
_transfer(
address(tokenIn), msg.sender, target, givenAmount, transferType
);
IUniswapV2Pair pool = IUniswapV2Pair(target);
if (zeroForOne) {
@@ -62,16 +78,18 @@ contract UniswapV2Executor is IExecutor {
IERC20 inToken,
address target,
address receiver,
bool zeroForOne
bool zeroForOne,
TransferType transferType
)
{
if (data.length != 61) {
if (data.length != 62) {
revert UniswapV2Executor__InvalidDataLength();
}
inToken = IERC20(address(bytes20(data[0:20])));
target = address(bytes20(data[20:40]));
receiver = address(bytes20(data[40:60]));
zeroForOne = uint8(data[60]) > 0;
transferType = TransferType(uint8(data[61]));
}
function _getAmountOut(address target, uint256 amountIn, bool zeroForOne)
@@ -91,9 +109,9 @@ contract UniswapV2Executor is IExecutor {
}
require(reserveIn > 0 && reserveOut > 0, "L");
uint256 amountInWithFee = amountIn * 997;
uint256 amountInWithFee = amountIn * (10000 - feeBps);
uint256 numerator = amountInWithFee * uint256(reserveOut);
uint256 denominator = (uint256(reserveIn) * 1000) + amountInWithFee;
uint256 denominator = (uint256(reserveIn) * 10000) + amountInWithFee;
amount = numerator / denominator;
}

View File

@@ -5,13 +5,15 @@ import "@interfaces/IExecutor.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import "@interfaces/ICallback.sol";
import {TokenTransfer} from "./TokenTransfer.sol";
error UniswapV3Executor__InvalidDataLength();
error UniswapV3Executor__InvalidFactory();
error UniswapV3Executor__InvalidTarget();
error UniswapV3Executor__InvalidInitCode();
error UniswapV3Executor__InvalidTransferType(uint8 transferType);
contract UniswapV3Executor is IExecutor, ICallback {
contract UniswapV3Executor is IExecutor, ICallback, TokenTransfer {
using SafeERC20 for IERC20;
uint160 private constant MIN_SQRT_RATIO = 4295128739;
@@ -22,7 +24,9 @@ contract UniswapV3Executor is IExecutor, ICallback {
bytes32 public immutable initCode;
address private immutable self;
constructor(address _factory, bytes32 _initCode) {
constructor(address _factory, bytes32 _initCode, address _permit2)
TokenTransfer(_permit2)
{
if (_factory == address(0)) {
revert UniswapV3Executor__InvalidFactory();
}
@@ -46,7 +50,8 @@ contract UniswapV3Executor is IExecutor, ICallback {
uint24 fee,
address receiver,
address target,
bool zeroForOne
bool zeroForOne,
TransferType transferType
) = _decodeData(data);
_verifyPairAddress(tokenIn, tokenOut, fee, target);
@@ -55,7 +60,8 @@ contract UniswapV3Executor is IExecutor, ICallback {
int256 amount1;
IUniswapV3Pool pool = IUniswapV3Pool(target);
bytes memory callbackData = _makeV3CallbackData(tokenIn, tokenOut, fee);
bytes memory callbackData =
_makeV3CallbackData(tokenIn, tokenOut, fee, transferType);
{
(amount0, amount1) = pool.swap(
@@ -80,6 +86,7 @@ contract UniswapV3Executor is IExecutor, ICallback {
returns (bytes memory result)
{
// The data has the following layout:
// - selector (4 bytes)
// - amount0Delta (32 bytes)
// - amount1Delta (32 bytes)
// - dataOffset (32 bytes)
@@ -87,16 +94,25 @@ contract UniswapV3Executor is IExecutor, ICallback {
// - protocolData (variable length)
(int256 amount0Delta, int256 amount1Delta) =
abi.decode(msgData[:64], (int256, int256));
abi.decode(msgData[4:68], (int256, int256));
address tokenIn = address(bytes20(msgData[128:148]));
address tokenIn = address(bytes20(msgData[132:152]));
verifyCallback(msgData[128:]);
// Transfer type does not exist
if (uint8(msgData[175]) > uint8(TransferType.NONE)) {
revert UniswapV3Executor__InvalidTransferType(uint8(msgData[175]));
}
TransferType transferType = TransferType(uint8(msgData[175]));
address sender = address(bytes20(msgData[176:196]));
verifyCallback(msgData[132:]);
uint256 amountOwed =
amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);
IERC20(tokenIn).safeTransfer(msg.sender, amountOwed);
_transfer(tokenIn, sender, msg.sender, amountOwed, transferType);
return abi.encode(amountOwed, tokenIn);
}
@@ -113,13 +129,7 @@ contract UniswapV3Executor is IExecutor, ICallback {
int256, /* amount1Delta */
bytes calldata /* data */
) external {
uint256 dataOffset = 4 + 32 + 32 + 32; // Skip selector + 2 ints + data_offset
uint256 dataLength =
uint256(bytes32(msg.data[dataOffset:dataOffset + 32]));
bytes calldata fullData = msg.data[4:dataOffset + 32 + dataLength];
handleCallback(fullData);
handleCallback(msg.data);
}
function _decodeData(bytes calldata data)
@@ -131,10 +141,11 @@ contract UniswapV3Executor is IExecutor, ICallback {
uint24 fee,
address receiver,
address target,
bool zeroForOne
bool zeroForOne,
TransferType transferType
)
{
if (data.length != 84) {
if (data.length != 85) {
revert UniswapV3Executor__InvalidDataLength();
}
tokenIn = address(bytes20(data[0:20]));
@@ -143,14 +154,18 @@ contract UniswapV3Executor is IExecutor, ICallback {
receiver = address(bytes20(data[43:63]));
target = address(bytes20(data[63:83]));
zeroForOne = uint8(data[83]) > 0;
transferType = TransferType(uint8(data[84]));
}
function _makeV3CallbackData(address tokenIn, address tokenOut, uint24 fee)
internal
view
returns (bytes memory)
{
return abi.encodePacked(tokenIn, tokenOut, fee, self);
function _makeV3CallbackData(
address tokenIn,
address tokenOut,
uint24 fee,
TransferType transferType
) internal view returns (bytes memory) {
return abi.encodePacked(
tokenIn, tokenOut, fee, uint8(transferType), msg.sender
);
}
function _verifyPairAddress(

View File

@@ -2,27 +2,53 @@
pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol";
import {ICallback} from "@interfaces/ICallback.sol";
import {TokenTransfer} from "./TokenTransfer.sol";
import {
IERC20,
SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {
Currency, CurrencyLibrary
} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {V4Router} from "@uniswap/v4-periphery/src/V4Router.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol";
import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
import {ICallback} from "@interfaces/ICallback.sol";
import {IUnlockCallback} from
"@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
import {TransientStateLibrary} from
"@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
error UniswapV4Executor__InvalidDataLength();
error UniswapV4Executor__NotPoolManager();
error UniswapV4Executor__UnknownCallback(bytes4 selector);
error UniswapV4Executor__DeltaNotPositive(Currency currency);
error UniswapV4Executor__DeltaNotNegative(Currency currency);
error UniswapV4Executor__V4TooMuchRequested(
uint256 maxAmountInRequested, uint256 amountRequested
);
contract UniswapV4Executor is IExecutor, V4Router, ICallback {
contract UniswapV4Executor is
IExecutor,
IUnlockCallback,
ICallback,
TokenTransfer
{
using SafeERC20 for IERC20;
using CurrencyLibrary for Currency;
using SafeCast for *;
using TransientStateLibrary for IPoolManager;
IPoolManager public immutable poolManager;
address private immutable _self;
bytes4 constant SWAP_EXACT_INPUT_SINGLE_SELECTOR = 0x8bc6d0d7;
bytes4 constant SWAP_EXACT_INPUT_SELECTOR = 0xaf90aeb1;
struct UniswapV4Pool {
address intermediaryToken;
@@ -30,7 +56,22 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
int24 tickSpacing;
}
constructor(IPoolManager _poolManager) V4Router(_poolManager) {}
constructor(IPoolManager _poolManager, address _permit2)
TokenTransfer(_permit2)
{
poolManager = _poolManager;
_self = address(this);
}
/**
* @dev Modifier to restrict access to only the pool manager.
*/
modifier poolManagerOnly() virtual {
if (msg.sender != address(poolManager)) {
revert UniswapV4Executor__NotPoolManager();
}
_;
}
function swap(uint256 amountIn, bytes calldata data)
external
@@ -41,10 +82,10 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
address tokenIn,
address tokenOut,
bool zeroForOne,
address callbackExecutor,
TransferType transferType,
address receiver,
UniswapV4Executor.UniswapV4Pool[] memory pools
) = _decodeData(data);
bytes memory swapData;
if (pools.length == 1) {
PoolKey memory key = PoolKey({
@@ -54,26 +95,16 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
tickSpacing: pools[0].tickSpacing,
hooks: IHooks(address(0))
});
bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_IN_SINGLE),
uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL)
swapData = abi.encodeWithSelector(
this.swapExactInputSingle.selector,
key,
zeroForOne,
amountIn,
msg.sender,
transferType,
receiver,
bytes("")
);
bytes[] memory params = new bytes[](3);
params[0] = abi.encode(
IV4Router.ExactInputSingleParams({
poolKey: key,
zeroForOne: zeroForOne,
amountIn: uint128(amountIn),
amountOutMinimum: uint128(0),
hookData: bytes("")
})
);
params[1] = abi.encode(tokenIn, amountIn); // currency to settle
params[2] = abi.encode(tokenOut, uint256(0)); // currency to take
swapData = abi.encode(actions, params);
} else {
PathKey[] memory path = new PathKey[](pools.length);
for (uint256 i = 0; i < pools.length; i++) {
@@ -86,51 +117,22 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
});
}
bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_IN),
uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL)
);
bytes[] memory params = new bytes[](3);
Currency currencyIn = Currency.wrap(tokenIn);
params[0] = abi.encode(
IV4Router.ExactInputParams({
currencyIn: currencyIn,
path: path,
amountIn: uint128(amountIn),
amountOutMinimum: uint128(0)
})
swapData = abi.encodeWithSelector(
this.swapExactInput.selector,
currencyIn,
path,
amountIn,
msg.sender,
transferType,
receiver
);
params[1] = abi.encode(currencyIn, amountIn);
params[2] = abi.encode(Currency.wrap(tokenOut), uint256(0));
swapData = abi.encode(actions, params);
}
bytes memory fullData = abi.encodePacked(swapData, callbackExecutor);
uint256 tokenOutBalanceBefore;
tokenOutBalanceBefore = tokenOut == address(0)
? address(this).balance
: IERC20(tokenOut).balanceOf(address(this));
bytes memory result = poolManager.unlock(swapData);
uint128 amountOut = abi.decode(result, (uint128));
executeActions(fullData);
uint256 tokenOutBalanceAfter;
tokenOutBalanceAfter = tokenOut == address(0)
? address(this).balance
: IERC20(tokenOut).balanceOf(address(this));
calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore;
return calculatedAmount;
}
// necessary to convert bytes memory to bytes calldata
function executeActions(bytes memory unlockData) public {
// slither-disable-next-line unused-return
poolManager.unlock(unlockData);
return amountOut;
}
function _decodeData(bytes calldata data)
@@ -140,22 +142,24 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
address tokenIn,
address tokenOut,
bool zeroForOne,
address callbackExecutor,
TransferType transferType,
address receiver,
UniswapV4Pool[] memory pools
)
{
if (data.length < 87) {
if (data.length < 88) {
revert UniswapV4Executor__InvalidDataLength();
}
tokenIn = address(bytes20(data[0:20]));
tokenOut = address(bytes20(data[20:40]));
zeroForOne = (data[40] != 0);
callbackExecutor = address(bytes20(data[41:61]));
transferType = TransferType(uint8(data[41]));
receiver = address(bytes20(data[42:62]));
uint256 poolsLength = (data.length - 61) / 26; // 26 bytes per pool object
uint256 poolsLength = (data.length - 62) / 26; // 26 bytes per pool object
pools = new UniswapV4Pool[](poolsLength);
bytes memory poolsData = data[61:];
bytes memory poolsData = data[62:];
uint256 offset = 0;
for (uint256 i = 0; i < poolsLength; i++) {
address intermediaryToken;
@@ -173,6 +177,9 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
}
}
/**
* @notice Handles the callback from the pool manager. This is used for callbacks from the router.
*/
function handleCallback(bytes calldata data)
external
returns (bytes memory)
@@ -181,15 +188,253 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
return _unlockCallback(data);
}
function verifyCallback(bytes calldata) public view onlyPoolManager {}
function verifyCallback(bytes calldata) public view poolManagerOnly {}
function _pay(Currency token, address, uint256 amount) internal override {
IERC20(Currency.unwrap(token)).safeTransfer(
address(poolManager), amount
);
/**
* @notice Handles the unlock callback from the pool manager. This is used for swaps against the executor directly (bypassing the router).
*/
function unlockCallback(bytes calldata data)
external
poolManagerOnly
returns (bytes memory)
{
return _unlockCallback(data);
}
function msgSender() public view override returns (address) {
return address(this);
/**
* @dev Internal function to handle the unlock callback.
*/
function _unlockCallback(bytes calldata data)
internal
returns (bytes memory)
{
bytes4 selector = bytes4(data[:4]);
if (
selector != SWAP_EXACT_INPUT_SELECTOR
&& selector != SWAP_EXACT_INPUT_SINGLE_SELECTOR
) {
revert UniswapV4Executor__UnknownCallback(selector);
}
// here we expect to call either `swapExactInputSingle` or `swapExactInput`. See `swap` to see how we encode the selector and the calldata
// slither-disable-next-line low-level-calls
(bool success, bytes memory returnData) = _self.delegatecall(data);
if (!success) {
revert(
string(
returnData.length > 0
? returnData
: abi.encodePacked("Uniswap v4 Callback failed")
)
);
}
return returnData;
}
/**
* @notice Performs an exact input single swap. It settles and takes the tokens after the swap.
* @param poolKey The key of the pool to swap in.
* @param zeroForOne Whether the swap is from token0 to token1 (true) or vice versa (false).
* @param amountIn The amount of tokens to swap in.
* @param sender The address of the sender.
* @param transferType The type of transfer in to use.
* @param receiver The address of the receiver.
* @param hookData Additional data for hook contracts.
*/
function swapExactInputSingle(
PoolKey memory poolKey,
bool zeroForOne,
uint128 amountIn,
address sender,
TransferType transferType,
address receiver,
bytes calldata hookData
) external returns (uint128) {
uint128 amountOut = _swap(
poolKey, zeroForOne, -int256(uint256(amountIn)), hookData
).toUint128();
Currency currencyIn = zeroForOne ? poolKey.currency0 : poolKey.currency1;
uint256 amount = _getFullDebt(currencyIn);
if (amount > amountIn) {
revert UniswapV4Executor__V4TooMuchRequested(amountIn, amount);
}
_settle(currencyIn, amount, sender, transferType);
Currency currencyOut =
zeroForOne ? poolKey.currency1 : poolKey.currency0;
_take(currencyOut, receiver, _mapTakeAmount(amountOut, currencyOut));
return amountOut;
}
/**
* @notice Performs an exact input swap along a path. It settles and takes the tokens after the swap.
* @param currencyIn The currency of the input token.
* @param path The path to swap along.
* @param amountIn The amount of tokens to swap in.
* @param sender The address of the sender.
* @param transferType The type of transfer in to use.
* @param receiver The address of the receiver.
*/
function swapExactInput(
Currency currencyIn,
PathKey[] calldata path,
uint128 amountIn,
address sender,
TransferType transferType,
address receiver
) external returns (uint128) {
uint128 amountOut = 0;
Currency swapCurrencyIn = currencyIn;
uint256 swapAmountIn = amountIn;
unchecked {
uint256 pathLength = path.length;
PathKey calldata pathKey;
for (uint256 i = 0; i < pathLength; i++) {
pathKey = path[i];
(PoolKey memory poolKey, bool zeroForOne) =
pathKey.getPoolAndSwapDirection(swapCurrencyIn);
// The output delta will always be positive, except for when interacting with certain hook pools
amountOut = _swap(
poolKey,
zeroForOne,
-int256(uint256(swapAmountIn)),
pathKey.hookData
).toUint128();
swapAmountIn = amountOut;
swapCurrencyIn = pathKey.intermediateCurrency;
}
}
uint256 amount = _getFullDebt(currencyIn);
if (amount > amountIn) {
revert UniswapV4Executor__V4TooMuchRequested(amountIn, amount);
}
_settle(currencyIn, amount, sender, transferType);
_take(
swapCurrencyIn, // at the end of the loop this is actually currency out
receiver,
_mapTakeAmount(amountOut, swapCurrencyIn)
);
return amountOut;
}
function _swap(
PoolKey memory poolKey,
bool zeroForOne,
int256 amountSpecified,
bytes calldata hookData
) private returns (int128 reciprocalAmount) {
unchecked {
// slither-disable-next-line calls-loop
BalanceDelta delta = poolManager.swap(
poolKey,
IPoolManager.SwapParams(
zeroForOne,
amountSpecified,
zeroForOne
? TickMath.MIN_SQRT_PRICE + 1
: TickMath.MAX_SQRT_PRICE - 1
),
hookData
);
reciprocalAmount = (zeroForOne == amountSpecified < 0)
? delta.amount1()
: delta.amount0();
}
}
/**
* @notice Obtains the full amount owed by this contract (negative delta).
* @param currency The currency to get the delta for.
* @return amount The amount owed by this contract.
*/
function _getFullCredit(Currency currency)
internal
view
returns (uint256 amount)
{
int256 _amount = poolManager.currencyDelta(address(this), currency);
// If the amount is negative, it should be settled not taken.
if (_amount < 0) revert UniswapV4Executor__DeltaNotPositive(currency);
amount = uint256(_amount);
}
/// @notice Obtain the full amount owed by this contract (negative delta)
/// @param currency Currency to get the delta for
/// @return amount The amount owed by this contract as a uint256
function _getFullDebt(Currency currency)
internal
view
returns (uint256 amount)
{
int256 _amount = poolManager.currencyDelta(address(this), currency);
// If the amount is positive, it should be taken not settled.
if (_amount > 0) revert UniswapV4Executor__DeltaNotNegative(currency);
// Casting is safe due to limits on the total supply of a pool
amount = uint256(-_amount);
}
/**
* @notice Pays and settles a currency to the pool manager.
* @dev The implementing contract must ensure that the `payer` is a secure address.
* @param currency The currency to settle.
* @param amount The amount to send.
* @param sender The address of the payer.
* @param transferType The type of transfer to use.
* @dev Returns early if the amount is 0.
*/
function _settle(
Currency currency,
uint256 amount,
address sender,
TransferType transferType
) internal {
if (amount == 0) return;
poolManager.sync(currency);
if (currency.isAddressZero()) {
// slither-disable-next-line unused-return
poolManager.settle{value: amount}();
} else {
_transfer(
Currency.unwrap(currency),
sender,
address(poolManager),
amount,
transferType
);
// slither-disable-next-line unused-return
poolManager.settle();
}
}
/**
* @notice Takes an amount of currency out of the pool manager.
* @param currency The currency to take.
* @param recipient The address to receive the currency.
* @param amount The amount to take.
* @dev Returns early if the amount is 0.
*/
function _take(Currency currency, address recipient, uint256 amount)
internal
{
if (amount == 0) return;
poolManager.take(currency, recipient, amount);
}
function _mapTakeAmount(uint256 amount, Currency currency)
internal
view
returns (uint256)
{
if (amount == 0) {
return _getFullCredit(currency);
} else {
return amount;
}
}
}

View File

@@ -15,8 +15,6 @@ contract Constants is Test, BaseConstants {
address ADMIN = makeAddr("admin"); //admin=us
address BOB = makeAddr("bob"); //bob=someone!=us
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 =

View File

@@ -10,7 +10,7 @@ contract DispatcherExposed is Dispatcher {
uint256 amount,
bytes calldata data
) external returns (uint256 calculatedAmount) {
return _callExecutor(executor, amount, data);
return _callSwapOnExecutor(executor, amount, data);
}
function exposedSetExecutor(address target) external {

View File

@@ -7,7 +7,45 @@ import "../lib/LibSwap.sol";
contract LibSwapTest is Test {
using LibSwap for bytes;
function testSwap() public view {
function testSingleSwap() public view {
address executor = 0x1234567890123456789012345678901234567890;
bytes memory protocolData = abi.encodePacked(uint256(123));
bytes memory swap = abi.encodePacked(executor, protocolData);
this.assertSingleSwap(swap, executor, protocolData);
}
function assertSingleSwap(
bytes calldata swap,
address executor,
bytes calldata protocolData
) public pure {
(address decodedExecutor, bytes memory decodedProtocolData) =
swap.decodeSingleSwap();
assertEq(decodedExecutor, executor);
assertEq(decodedProtocolData, protocolData);
}
function testSequentialSwap() public view {
address executor = 0x1234567890123456789012345678901234567890;
bytes memory protocolData = abi.encodePacked(uint256(234));
bytes memory swap = abi.encodePacked(executor, protocolData);
this.assertSequentialSwap(swap, executor, protocolData);
}
function assertSequentialSwap(
bytes calldata swap,
address executor,
bytes calldata protocolData
) public pure {
(address decodedExecutor, bytes memory decodedProtocolData) =
swap.decodeSequentialSwap();
assertEq(decodedExecutor, executor);
assertEq(decodedProtocolData, protocolData);
}
function testSplitSwap() public view {
uint8 tokenInIndex = 1;
uint8 tokenOutIndex = 2;
uint24 split = 3;
@@ -17,20 +55,32 @@ contract LibSwapTest is Test {
bytes memory swap = abi.encodePacked(
tokenInIndex, tokenOutIndex, split, executor, protocolData
);
this.assertSwap(swap, tokenInIndex, tokenOutIndex, split, executor);
this.assertSplitSwap(
swap, tokenInIndex, tokenOutIndex, split, executor, protocolData
);
}
// This is necessary so that the compiler accepts bytes as a LibSwap.sol
function assertSwap(
// This is necessary so that the compiler accepts bytes as a LibSwap.sol for testing
// This is because this function takes calldata as input
function assertSplitSwap(
bytes calldata swap,
uint8 tokenInIndex,
uint8 tokenOutIndex,
uint24 split,
address executor
address executor,
bytes calldata protocolData
) public pure {
assert(swap.tokenInIndex() == tokenInIndex);
assert(swap.tokenOutIndex() == tokenOutIndex);
assert(swap.splitPercentage() == split);
assert(swap.executor() == executor);
(
uint8 decodedTokenInIndex,
uint8 decodedTokenOutIndex,
uint24 decodedSplit,
address decodedExecutor,
bytes memory decodedProtocolData
) = swap.decodeSplitSwap();
assertEq(decodedTokenInIndex, tokenInIndex);
assertEq(decodedTokenOutIndex, tokenOutIndex);
assertEq(decodedSplit, split);
assertEq(decodedExecutor, executor);
assertEq(decodedProtocolData, protocolData);
}
}

View File

@@ -0,0 +1,87 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "./Constants.sol";
import "@permit2/src/interfaces/IAllowanceTransfer.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract Permit2TestHelper is Constants {
/**
* @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,
address spender,
uint256 amount_in
) internal returns (IAllowanceTransfer.PermitSingle memory, bytes memory) {
IERC20(tokenIn).approve(PERMIT2_ADDRESS, 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: spender,
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,
PERMIT2_ADDRESS
)
);
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);
}
}

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "forge-std/Test.sol";
contract TestUtils is Test {
constructor() {}
function loadCallDataFromFile(string memory testName)
internal
view
returns (bytes memory)
{
string memory fileContent = vm.readFile("./test/assets/calldata.txt");
string[] memory lines = vm.split(fileContent, "\n");
for (uint256 i = 0; i < lines.length; i++) {
string[] memory parts = vm.split(lines[i], ":");
if (
parts.length >= 2
&& keccak256(bytes(parts[0])) == keccak256(bytes(testName))
) {
return vm.parseBytes(parts[1]);
}
}
revert("Test calldata not found");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,267 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "./TychoRouterTestSetup.sol";
import "./executors/UniswapV4Utils.sol";
contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup {
function testSingleSwapUSV4CallbackPermit2() public {
vm.startPrank(ALICE);
uint256 amountIn = 100 ether;
deal(USDE_ADDR, ALICE, amountIn);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(USDE_ADDR, tychoRouterAddr, amountIn);
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](1);
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(100),
tickSpacing: int24(1)
});
bytes memory protocolData = UniswapV4Utils.encodeExactInput(
USDE_ADDR,
USDT_ADDR,
true,
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL,
ALICE,
pools
);
bytes memory swap =
encodeSingleSwap(address(usv4Executor), protocolData);
tychoRouter.singleSwapPermit2(
amountIn,
USDE_ADDR,
USDT_ADDR,
99943850,
false,
false,
ALICE,
permitSingle,
signature,
swap
);
assertEq(IERC20(USDT_ADDR).balanceOf(ALICE), 99963618);
vm.stopPrank();
}
function testSplitSwapMultipleUSV4Callback() public {
// This test has two uniswap v4 hops that will be executed inside of the V4 pool manager
// USDE -> USDT -> WBTC
uint256 amountIn = 100 ether;
deal(USDE_ADDR, tychoRouterAddr, amountIn);
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](2);
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(100),
tickSpacing: int24(1)
});
pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: WBTC_ADDR,
fee: uint24(3000),
tickSpacing: int24(60)
});
bytes memory protocolData = UniswapV4Utils.encodeExactInput(
USDE_ADDR,
WBTC_ADDR,
true,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL,
ALICE,
pools
);
bytes memory swap =
encodeSingleSwap(address(usv4Executor), protocolData);
tychoRouter.singleSwap(
amountIn, USDE_ADDR, WBTC_ADDR, 118280, false, false, ALICE, swap
);
assertEq(IERC20(WBTC_ADDR).balanceOf(ALICE), 118281);
}
function testSequentialUSV4Integration() public {
// Test created with calldata from our router encoder.
// Performs a sequential swap from USDC to PEPE though ETH using two
// consecutive USV4 pools
//
// USDC ──(USV4)──> ETH ───(USV4)──> PEPE
//
deal(USDC_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(PEPE_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_sequential_encoding_strategy_usv4");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(PEPE_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 123172000092711286554274694);
}
function testMultiProtocolIntegration() public {
// Test created with calldata from our router encoder.
//
// DAI ─(USV2)─> WETH ─(bal)─> WBTC ─(curve)─> USDT ─(ekubo)─> ETH ─(USV4)─> USDC
deal(DAI_ADDR, ALICE, 1500 ether);
uint256 balanceBefore = address(ALICE).balance;
// Approve permit2
vm.startPrank(ALICE);
IERC20(DAI_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData = loadCallDataFromFile("test_multi_protocol");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = address(ALICE).balance;
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 732214216964381330);
}
function testSingleUSV4IntegrationInputETH() public {
// Test created with calldata from our router encoder.
// Performs a single swap from ETH to PEPE without wrapping or unwrapping
//
// ETH ───(USV4)──> PEPE
//
deal(ALICE, 1 ether);
uint256 balanceBefore = IERC20(PEPE_ADDR).balanceOf(ALICE);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_usv4_eth_in");
(bool success,) = tychoRouterAddr.call{value: 1 ether}(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(PEPE_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 235610487387677804636755778);
}
function testSingleUSV4IntegrationOutputETH() public {
// Test created with calldata from our router encoder.
// Performs a single swap from USDC to ETH without wrapping or unwrapping
//
// USDC ───(USV4)──> ETH
//
deal(USDC_ADDR, ALICE, 3000_000000);
uint256 balanceBefore = ALICE.balance;
// Approve permit2
vm.startPrank(ALICE);
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_usv4_eth_out");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = ALICE.balance;
assertTrue(success, "Call Failed");
console.logUint(balanceAfter - balanceBefore);
assertEq(balanceAfter - balanceBefore, 1474406268748155809);
}
function testSingleEkuboIntegration() public {
vm.stopPrank();
deal(ALICE, 1 ether);
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_ekubo");
(bool success,) = tychoRouterAddr.call{value: 1 ether}(callData);
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertGe(balanceAfter - balanceBefore, 26173932);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSingleCurveIntegration() public {
deal(UWU_ADDR, ALICE, 1 ether);
vm.startPrank(ALICE);
IERC20(UWU_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_curve");
(bool success,) = tychoRouterAddr.call(callData);
assertTrue(success, "Call Failed");
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 2877855391767);
vm.stopPrank();
}
function testSingleSwapUSV3Permit2() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V3 using Permit2
// Tests entire USV3 flow including callback
// 1 WETH -> DAI
// (USV3)
vm.startPrank(ALICE);
uint256 amountIn = 10 ** 18;
deal(WETH_ADDR, ALICE, amountIn);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(WETH_ADDR, tychoRouterAddr, amountIn);
uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI
bool zeroForOne = false;
bytes memory protocolData = encodeUniswapV3Swap(
WETH_ADDR,
DAI_ADDR,
ALICE,
DAI_WETH_USV3,
zeroForOne,
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
);
bytes memory swap =
encodeSingleSwap(address(usv3Executor), protocolData);
tychoRouter.singleSwapPermit2(
amountIn,
WETH_ADDR,
DAI_ADDR,
expAmountOut - 1,
false,
false,
ALICE,
permitSingle,
signature,
swap
);
uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
assertGe(finalBalance, expAmountOut);
vm.stopPrank();
}
}

View File

@@ -0,0 +1,498 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "@src/executors/UniswapV4Executor.sol";
import {TychoRouter} from "@src/TychoRouter.sol";
import "./TychoRouterTestSetup.sol";
import "./executors/UniswapV4Utils.sol";
import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol";
contract TychoRouterSequentialSwapTest is TychoRouterTestSetup {
function _getSequentialSwaps(bool permit2)
internal
view
returns (bytes[] memory)
{
// Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2
// 1 WETH -> DAI -> USDC
// (univ2) (univ2)
TokenTransfer.TransferType transferType = permit2
? TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
: TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL;
bytes[] memory swaps = new bytes[](2);
// WETH -> DAI
swaps[0] = encodeSequentialSwap(
address(usv2Executor),
encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false, transferType
)
);
// DAI -> USDC
swaps[1] = encodeSequentialSwap(
address(usv2Executor),
encodeUniswapV2Swap(
DAI_ADDR,
DAI_USDC_POOL,
ALICE,
true,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
)
);
return swaps;
}
function testSequentialSwapPermit2() public {
// Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(WETH_ADDR, tychoRouterAddr, amountIn);
bytes[] memory swaps = _getSequentialSwaps(true);
tychoRouter.sequentialSwapPermit2(
amountIn,
WETH_ADDR,
USDC_ADDR,
1000_000000, // min amount
false,
false,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
assertEq(usdcBalance, 2005810530);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSequentialSwapNoPermit2() public {
// Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn);
bytes[] memory swaps = _getSequentialSwaps(false);
tychoRouter.sequentialSwap(
amountIn,
WETH_ADDR,
USDC_ADDR,
1000_000000, // min amount
false,
false,
ALICE,
pleEncode(swaps)
);
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
assertEq(usdcBalance, 2005810530);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSequentialSwapUndefinedMinAmount() public {
// Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn);
bytes[] memory swaps = _getSequentialSwaps(false);
vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector);
tychoRouter.sequentialSwap(
amountIn,
WETH_ADDR,
USDC_ADDR,
0, // min amount
false,
false,
ALICE,
pleEncode(swaps)
);
}
function testSequentialSwapInsufficientApproval() public {
// Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn - 1);
bytes[] memory swaps = _getSequentialSwaps(false);
vm.expectRevert();
tychoRouter.sequentialSwap(
amountIn,
WETH_ADDR,
USDC_ADDR,
0, // min amount
false,
false,
ALICE,
pleEncode(swaps)
);
}
function testSequentialSwapNegativeSlippageFailure() public {
// Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(WETH_ADDR, tychoRouterAddr, amountIn);
bytes[] memory swaps = _getSequentialSwaps(true);
uint256 minAmountOut = 3000 * 1e18;
vm.expectRevert(
abi.encodeWithSelector(
TychoRouter__NegativeSlippage.selector,
2005810530, // actual amountOut
minAmountOut
)
);
tychoRouter.sequentialSwapPermit2(
amountIn,
WETH_ADDR,
DAI_ADDR,
minAmountOut,
false,
false,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
vm.stopPrank();
}
function testSequentialSwapWrapETH() public {
uint256 amountIn = 1 ether;
deal(ALICE, amountIn);
vm.startPrank(ALICE);
IAllowanceTransfer.PermitSingle memory emptyPermitSingle =
IAllowanceTransfer.PermitSingle({
details: IAllowanceTransfer.PermitDetails({
token: address(0),
amount: 0,
expiration: 0,
nonce: 0
}),
spender: address(0),
sigDeadline: 0
});
bytes[] memory swaps = new bytes[](2);
// WETH -> DAI
swaps[0] = encodeSequentialSwap(
address(usv2Executor),
encodeUniswapV2Swap(
WETH_ADDR,
WETH_DAI_POOL,
tychoRouterAddr,
false,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
)
);
// DAI -> USDC
swaps[1] = encodeSequentialSwap(
address(usv2Executor),
encodeUniswapV2Swap(
DAI_ADDR,
DAI_USDC_POOL,
ALICE,
true,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
)
);
uint256 amountOut = tychoRouter.sequentialSwapPermit2{value: amountIn}(
amountIn,
address(0),
USDC_ADDR,
1000_000000,
true,
false,
ALICE,
emptyPermitSingle,
"",
pleEncode(swaps)
);
uint256 expectedAmount = 2005810530;
assertEq(amountOut, expectedAmount);
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
assertEq(usdcBalance, expectedAmount);
assertEq(ALICE.balance, 0);
vm.stopPrank();
}
function testSequentialSwapUnwrapETH() public {
// Trade 3k DAI for WETH with 1 swap on Uniswap V2 and unwrap it at the end
uint256 amountIn = 3_000 * 10 ** 6;
deal(USDC_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(USDC_ADDR, tychoRouterAddr, amountIn);
bytes[] memory swaps = new bytes[](2);
// USDC -> DAI
swaps[0] = encodeSequentialSwap(
address(usv2Executor),
encodeUniswapV2Swap(
USDC_ADDR,
DAI_USDC_POOL,
tychoRouterAddr,
false,
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
)
);
// DAI -> WETH
swaps[1] = encodeSequentialSwap(
address(usv2Executor),
encodeUniswapV2Swap(
DAI_ADDR,
WETH_DAI_POOL,
tychoRouterAddr,
true,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
)
);
uint256 amountOut = tychoRouter.sequentialSwapPermit2(
amountIn,
USDC_ADDR,
address(0),
1 * 10 ** 18, // min amount
false,
true,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
uint256 expectedAmount = 1466332452295613768; // 1.11 ETH
assertEq(amountOut, expectedAmount);
assertEq(ALICE.balance, expectedAmount);
vm.stopPrank();
}
function testCyclicSequentialSwap() public {
// This test has start and end tokens that are the same
// The flow is:
// USDC --(USV3)--> WETH --(USV3)--> USDC
uint256 amountIn = 100 * 10 ** 6;
deal(USDC_ADDR, tychoRouterAddr, amountIn);
bytes memory usdcWethV3Pool1ZeroOneData = encodeUniswapV3Swap(
USDC_ADDR,
WETH_ADDR,
tychoRouterAddr,
USDC_WETH_USV3,
true,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap(
WETH_ADDR,
USDC_ADDR,
tychoRouterAddr,
USDC_WETH_USV3_2,
false,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
bytes[] memory swaps = new bytes[](2);
// USDC -> WETH
swaps[0] = encodeSequentialSwap(
address(usv3Executor), usdcWethV3Pool1ZeroOneData
);
// WETH -> USDC
swaps[1] = encodeSequentialSwap(
address(usv3Executor), usdcWethV3Pool2OneZeroData
);
tychoRouter.exposedSequentialSwap(amountIn, pleEncode(swaps));
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99792554);
}
function testSequentialSwapIntegrationPermit2() public {
// Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools
//
// WETH ──(USV2)──> WBTC ───(USV2)──> USDC
deal(WETH_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_sequential_swap_strategy_encoder");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 1951856272);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSequentialSwapIntegration() public {
// Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools
//
// WETH ──(USV2)──> WBTC ───(USV2)──> USDC
deal(WETH_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData = loadCallDataFromFile(
"test_sequential_swap_strategy_encoder_no_permit2"
);
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 1951856272);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSequentialCyclicSwapIntegration() public {
// USDC -> WETH -> USDC using two pools
deal(USDC_ADDR, ALICE, 100 * 10 ** 6);
// Approve permit2
vm.startPrank(ALICE);
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_sequential_strategy_cyclic_swap");
(bool success,) = tychoRouterAddr.call(callData);
assertTrue(success, "Call Failed");
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99792554);
vm.stopPrank();
}
function testUSV3USV2Integration() public {
// Performs a sequential swap from WETH to USDC though WBTC and DAI using USV3 and USV2 pools
//
// WETH ──(USV3)──> WBTC ───(USV2)──> USDC
deal(WETH_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_uniswap_v3_uniswap_v2");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 1952973189);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testUSV3USV3Integration() public {
// Performs a sequential swap from WETH to USDC though WBTC using USV3 pools
//
// WETH ──(USV3)──> WBTC ───(USV3)──> USDC
deal(WETH_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_uniswap_v3_uniswap_v3");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 2015740345);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testUSV3CurveIntegration() public {
// Performs a sequential swap from WETH to USDT though WBTC using USV3 and Curve pools
//
// WETH ──(USV3)──> WBTC ───(USV3)──> USDT
deal(WETH_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(USDT_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData = loadCallDataFromFile("test_uniswap_v3_curve");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(USDT_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 2018869128);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testBalancerV2USV2Integration() public {
// Performs a sequential swap from WETH to USDC though WBTC using Balancer v2 and USV2 pools
//
// WETH ──(balancer)──> WBTC ───(USV2)──> USDC
deal(WETH_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(USDT_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_balancer_v2_uniswap_v2");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 1949668893);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
}

View File

@@ -0,0 +1,384 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "@src/executors/UniswapV4Executor.sol";
import {TychoRouter} from "@src/TychoRouter.sol";
import "./TychoRouterTestSetup.sol";
import "./executors/UniswapV4Utils.sol";
import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol";
contract TychoRouterSingleSwapTest is TychoRouterTestSetup {
function testSingleSwapPermit2() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2 using Permit2
// 1 WETH -> DAI
// (USV2)
vm.startPrank(ALICE);
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(WETH_ADDR, tychoRouterAddr, amountIn);
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR,
WETH_DAI_POOL,
ALICE,
false,
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
);
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
tychoRouter.singleSwapPermit2(
amountIn,
WETH_ADDR,
DAI_ADDR,
2008817438608734439722,
false,
false,
ALICE,
permitSingle,
signature,
swap
);
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
assertEq(daiBalance, 2018817438608734439722);
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0);
vm.stopPrank();
}
function testSingleSwapNoPermit2() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// Checks amount out at the end
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
// Approve the tokenIn to be transferred to the router
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn);
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR,
WETH_DAI_POOL,
ALICE,
false,
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
);
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
uint256 minAmountOut = 2000 * 1e18;
uint256 amountOut = tychoRouter.singleSwap(
amountIn,
WETH_ADDR,
DAI_ADDR,
minAmountOut,
false,
false,
ALICE,
swap
);
uint256 expectedAmount = 2018817438608734439722;
assertEq(amountOut, expectedAmount);
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
assertEq(daiBalance, expectedAmount);
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0);
vm.stopPrank();
}
function testSingleSwapUndefinedMinAmount() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// Checks amount out at the end
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn);
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR,
WETH_DAI_POOL,
ALICE,
false,
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
);
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector);
tychoRouter.singleSwap(
amountIn, WETH_ADDR, DAI_ADDR, 0, false, false, ALICE, swap
);
}
function testSingleSwapInsufficientApproval() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// Checks amount out at the end
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn - 1);
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR,
WETH_DAI_POOL,
ALICE,
false,
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
);
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
uint256 minAmountOut = 2600 * 1e18;
vm.expectRevert();
tychoRouter.singleSwap(
amountIn,
WETH_ADDR,
DAI_ADDR,
minAmountOut,
false,
false,
ALICE,
swap
);
}
function testSingleSwapNegativeSlippageFailure() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// Checks amount out at the end
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
// Approve the tokenIn to be transferred to the router
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn);
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR,
WETH_DAI_POOL,
ALICE,
false,
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
);
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
uint256 minAmountOut = 5600 * 1e18;
vm.expectRevert(
abi.encodeWithSelector(
TychoRouter__NegativeSlippage.selector,
2018817438608734439722, // actual amountOut
minAmountOut
)
);
tychoRouter.singleSwap(
amountIn,
WETH_ADDR,
DAI_ADDR,
minAmountOut,
false,
false,
ALICE,
swap
);
}
function testSingleSwapWrapETH() public {
uint256 amountIn = 1 ether;
deal(ALICE, amountIn);
vm.startPrank(ALICE);
IAllowanceTransfer.PermitSingle memory emptyPermitSingle =
IAllowanceTransfer.PermitSingle({
details: IAllowanceTransfer.PermitDetails({
token: address(0),
amount: 0,
expiration: 0,
nonce: 0
}),
spender: address(0),
sigDeadline: 0
});
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR,
WETH_DAI_POOL,
ALICE,
false,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
uint256 amountOut = tychoRouter.singleSwapPermit2{value: amountIn}(
amountIn,
address(0),
DAI_ADDR,
1000_000000,
true,
false,
ALICE,
emptyPermitSingle,
"",
swap
);
uint256 expectedAmount = 2018817438608734439722;
assertEq(amountOut, expectedAmount);
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
assertEq(daiBalance, expectedAmount);
assertEq(ALICE.balance, 0);
vm.stopPrank();
}
function testSingleSwapUnwrapETH() public {
// DAI -> WETH with unwrapping to ETH
uint256 amountIn = 3000 ether;
deal(DAI_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(DAI_ADDR, tychoRouterAddr, amountIn);
bytes memory protocolData = encodeUniswapV2Swap(
DAI_ADDR,
WETH_DAI_POOL,
tychoRouterAddr,
true,
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
);
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
uint256 amountOut = tychoRouter.singleSwapPermit2(
amountIn,
DAI_ADDR,
address(0),
1000_000000,
false,
true,
ALICE,
permitSingle,
signature,
swap
);
uint256 expectedAmount = 1475644707225677606;
assertEq(amountOut, expectedAmount);
assertEq(ALICE.balance, expectedAmount);
vm.stopPrank();
}
function testSingleSwapIntegration() public {
// Tests swapping WETH -> DAI on a USV2 pool with regular approvals
deal(WETH_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_swap_strategy_encoder_no_permit2");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 2018817438608734439722);
}
function testSingleSwapIntegrationPermit2() public {
// Tests swapping WETH -> DAI on a USV2 pool with permit2
deal(WETH_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_swap_strategy_encoder");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 2018817438608734439722);
}
function testSingleSwapWithWrapIntegration() public {
// Tests swapping WETH -> DAI on a USV2 pool, but ETH is received from the user
// and wrapped before the swap
deal(ALICE, 1 ether);
uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
bytes memory callData =
loadCallDataFromFile("test_single_swap_strategy_encoder_wrap");
(bool success,) = tychoRouterAddr.call{value: 1 ether}(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 2018817438608734439722);
}
function testSingleSwapWithUnwrapIntegration() public {
// Tests swapping DAI -> WETH on a USV2 pool, and WETH is unwrapped to ETH
// before sending back to the user
deal(DAI_ADDR, ALICE, 3000 ether);
uint256 balanceBefore = ALICE.balance;
// Approve permit2
vm.startPrank(ALICE);
IERC20(DAI_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_swap_strategy_encoder_unwrap");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = ALICE.balance;
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 1475644707225677606);
}
function testSingleSwapIntegrationNoTransferIn() public {
// Tests swapping WETH -> DAI on a USV2 pool assuming that the tokens are already inside the router
deal(WETH_ADDR, tychoRouterAddr, 1 ether);
uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
vm.startPrank(ALICE);
bytes memory callData = loadCallDataFromFile(
"test_single_swap_strategy_encoder_no_transfer_in"
);
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 2018817438608734439722);
}
}

View File

@@ -0,0 +1,578 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "@src/executors/UniswapV4Executor.sol";
import {TychoRouter} from "@src/TychoRouter.sol";
import "./TychoRouterTestSetup.sol";
import "./executors/UniswapV4Utils.sol";
import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol";
contract TychoRouterSplitSwapTest is TychoRouterTestSetup {
function _getSplitSwaps(bool permit2)
private
view
returns (bytes[] memory)
{
// Trade 1 WETH for USDC through DAI and WBTC with 4 swaps on Uniswap V2
// -> DAI ->
// 1 WETH USDC
// -> WBTC ->
// (univ2) (univ2)
bytes[] memory swaps = new bytes[](4);
TokenTransfer.TransferType inTransferType = permit2
? TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
: TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL;
// WETH -> WBTC (60%)
swaps[0] = encodeSplitSwap(
uint8(0),
uint8(1),
(0xffffff * 60) / 100, // 60%
address(usv2Executor),
encodeUniswapV2Swap(
WETH_ADDR,
WETH_WBTC_POOL,
tychoRouterAddr,
false,
inTransferType
)
);
// WBTC -> USDC
swaps[1] = encodeSplitSwap(
uint8(1),
uint8(2),
uint24(0),
address(usv2Executor),
encodeUniswapV2Swap(
WBTC_ADDR,
USDC_WBTC_POOL,
ALICE,
true,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
)
);
// WETH -> DAI
swaps[2] = encodeSplitSwap(
uint8(0),
uint8(3),
uint24(0),
address(usv2Executor),
encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false, inTransferType
)
);
// DAI -> USDC
swaps[3] = encodeSplitSwap(
uint8(3),
uint8(2),
uint24(0),
address(usv2Executor),
encodeUniswapV2Swap(
DAI_ADDR,
DAI_USDC_POOL,
ALICE,
true,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
)
);
return swaps;
}
function testSplitSwapInternalMethod() public {
// Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn);
bytes[] memory swaps = _getSplitSwaps(false);
tychoRouter.exposedSplitSwap(amountIn, 4, pleEncode(swaps));
vm.stopPrank();
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
assertEq(usdcBalance, 1989737355);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSplitSwapPermit2() public {
// Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(WETH_ADDR, tychoRouterAddr, amountIn);
bytes[] memory swaps = _getSplitSwaps(true);
tychoRouter.splitSwapPermit2(
amountIn,
WETH_ADDR,
USDC_ADDR,
1, // min amount
false,
false,
4,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
assertEq(usdcBalance, 1989737355);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSplitSwapNoPermit2() public {
// Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn);
bytes[] memory swaps = _getSplitSwaps(false);
tychoRouter.splitSwap(
amountIn,
WETH_ADDR,
USDC_ADDR,
1000_000000, // min amount
false,
false,
4,
ALICE,
pleEncode(swaps)
);
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
assertEq(usdcBalance, 1989737355);
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0);
}
function testSplitSwapUndefinedMinAmount() public {
// Min amount should always be non-zero. If zero, swap attempt should revert.
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn);
bytes[] memory swaps = _getSplitSwaps(false);
vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector);
tychoRouter.splitSwap(
amountIn,
WETH_ADDR,
USDC_ADDR,
0, // min amount
false,
false,
4,
ALICE,
pleEncode(swaps)
);
vm.stopPrank();
}
function testSplitSwapInsufficientApproval() public {
// Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
// Approve less than the amountIn
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn - 1);
bytes[] memory swaps = _getSplitSwaps(false);
vm.expectRevert();
tychoRouter.splitSwap(
amountIn,
WETH_ADDR,
USDC_ADDR,
1000_000000, // min amount
false,
false,
2,
ALICE,
pleEncode(swaps)
);
vm.stopPrank();
}
function testSplitSwapNegativeSlippageFailure() public {
// Trade 1 WETH for USDC through DAI and WBTC - see _getSplitSwaps for more info
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(WETH_ADDR, tychoRouterAddr, amountIn);
bytes[] memory swaps = _getSplitSwaps(true);
uint256 minAmountOut = 3000 * 1e18;
vm.expectRevert(
abi.encodeWithSelector(
TychoRouter__NegativeSlippage.selector,
1989737355, // actual amountOut
minAmountOut
)
);
tychoRouter.splitSwapPermit2(
amountIn,
WETH_ADDR,
DAI_ADDR,
minAmountOut,
false,
false,
4,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
vm.stopPrank();
}
function testSplitSwapWrapETH() 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,
ALICE,
false,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
bytes memory swap = encodeSplitSwap(
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 amountOut = tychoRouter.splitSwapPermit2{value: amountIn}(
amountIn,
address(0),
DAI_ADDR,
2008817438608734439722,
true,
false,
2,
ALICE,
emptyPermitSingle,
"",
pleEncode(swaps)
);
uint256 expectedAmount = 2018817438608734439722;
assertEq(amountOut, expectedAmount);
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
assertEq(daiBalance, expectedAmount);
assertEq(ALICE.balance, 0);
vm.stopPrank();
}
function testSplitSwapUnwrapETH() 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, tychoRouterAddr, amountIn);
bytes memory protocolData = encodeUniswapV2Swap(
DAI_ADDR,
WETH_DAI_POOL,
tychoRouterAddr,
true,
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL
);
bytes memory swap = encodeSplitSwap(
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 amountOut = tychoRouter.splitSwapPermit2(
amountIn,
DAI_ADDR,
address(0),
1465644707225677606,
false,
true,
2,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
uint256 expectedAmount = 1475644707225677606; // 1.12 ETH
assertEq(amountOut, expectedAmount);
assertEq(ALICE.balance, expectedAmount);
vm.stopPrank();
}
function testEmptySwapsRevert() public {
uint256 amountIn = 10 ** 18;
bytes memory swaps = "";
vm.expectRevert(TychoRouter__EmptySwaps.selector);
tychoRouter.exposedSplitSwap(amountIn, 2, swaps);
}
function testSplitInputCyclicSwapInternalMethod() public {
// This test has start and end tokens that are the same
// The flow is:
// ┌─ (USV3, 60% split) ──> WETH ─┐
// │ │
// USDC ──────┤ ├──(USV2)──> USDC
// │ │
// └─ (USV3, 40% split) ──> WETH ─┘
uint256 amountIn = 100 * 10 ** 6;
deal(USDC_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
// Approve the TychoRouter to spend USDC
IERC20(USDC_ADDR).approve(tychoRouterAddr, amountIn);
bytes memory usdcWethV3Pool1ZeroOneData = encodeUniswapV3Swap(
USDC_ADDR,
WETH_ADDR,
tychoRouterAddr,
USDC_WETH_USV3,
true,
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
);
bytes memory usdcWethV3Pool2ZeroOneData = encodeUniswapV3Swap(
USDC_ADDR,
WETH_ADDR,
tychoRouterAddr,
USDC_WETH_USV3_2,
true,
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
);
bytes memory wethUsdcV2OneZeroData = encodeUniswapV2Swap(
WETH_ADDR,
USDC_WETH_USV2,
tychoRouterAddr,
false,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
bytes[] memory swaps = new bytes[](3);
// USDC -> WETH (60% split)
swaps[0] = encodeSplitSwap(
uint8(0),
uint8(1),
(0xffffff * 60) / 100, // 60%
address(usv3Executor),
usdcWethV3Pool1ZeroOneData
);
// USDC -> WETH (40% remainder)
swaps[1] = encodeSplitSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv3Executor),
usdcWethV3Pool2ZeroOneData
);
// WETH -> USDC
swaps[2] = encodeSplitSwap(
uint8(1),
uint8(0),
uint24(0),
address(usv2Executor),
wethUsdcV2OneZeroData
);
tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps));
vm.stopPrank();
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99654537);
}
function testSplitOutputCyclicSwapInternalMethod() public {
// This test has start and end tokens that are the same
// The flow is:
// ┌─── (USV3, 60% split) ───┐
// │ │
// USDC ──(USV2) ── WETH──| ├─> USDC
// │ │
// └─── (USV3, 40% split) ───┘
uint256 amountIn = 100 * 10 ** 6;
deal(USDC_ADDR, tychoRouterAddr, amountIn);
bytes memory usdcWethV2Data = encodeUniswapV2Swap(
USDC_ADDR,
USDC_WETH_USV2,
tychoRouterAddr,
true,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
bytes memory usdcWethV3Pool1OneZeroData = encodeUniswapV3Swap(
WETH_ADDR,
USDC_ADDR,
tychoRouterAddr,
USDC_WETH_USV3,
false,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap(
WETH_ADDR,
USDC_ADDR,
tychoRouterAddr,
USDC_WETH_USV3_2,
false,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
bytes[] memory swaps = new bytes[](3);
// USDC -> WETH
swaps[0] = encodeSplitSwap(
uint8(0), uint8(1), uint24(0), address(usv2Executor), usdcWethV2Data
);
// WETH -> USDC
swaps[1] = encodeSplitSwap(
uint8(1),
uint8(0),
(0xffffff * 60) / 100,
address(usv3Executor),
usdcWethV3Pool1OneZeroData
);
// WETH -> USDC
swaps[2] = encodeSplitSwap(
uint8(1),
uint8(0),
uint24(0),
address(usv3Executor),
usdcWethV3Pool2OneZeroData
);
tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps));
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99444510);
}
// Base Network Tests
// Make sure to set the RPC_URL to base network
function testSplitSwapInternalMethodBase() public {
vm.skip(true);
vm.rollFork(26857267);
uint256 amountIn = 10 * 10 ** 6;
deal(BASE_USDC, tychoRouterAddr, amountIn);
bytes memory protocolData = encodeUniswapV2Swap(
BASE_USDC,
USDC_MAG7_POOL,
tychoRouterAddr,
true,
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL
);
bytes memory swap = encodeSplitSwap(
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps));
assertGt(IERC20(BASE_MAG7).balanceOf(tychoRouterAddr), 1379830606);
}
function testSplitSwapIntegration() public {
// Performs a split swap from WETH to USDC though WBTC and DAI using USV2 pools
//
// ┌──(USV2)──> WBTC ───(USV2)──> USDC
// WETH ─┤
// └──(USV2)──> DAI ───(USV2)──> USDC
deal(WETH_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_split_swap_strategy_encoder");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertGe(balanceAfter - balanceBefore, 26173932);
// All input tokens are transferred to the router at first. Make sure we used
// all of it (and thus our splits are correct).
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSplitInputCyclicSwapIntegration() public {
deal(USDC_ADDR, ALICE, 100 * 10 ** 6);
// Approve permit2
vm.startPrank(ALICE);
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_split_input_cyclic_swap");
(bool success,) = tychoRouterAddr.call(callData);
assertTrue(success, "Call Failed");
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99654537);
vm.stopPrank();
}
function testSplitOutputCyclicSwapIntegration() public {
deal(USDC_ADDR, ALICE, 100 * 10 ** 6);
// Approve permit2
vm.startPrank(ALICE);
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_split_output_cyclic_swap");
(bool success,) = tychoRouterAddr.call(callData);
assertTrue(success, "Call Failed");
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99444510);
vm.stopPrank();
}
}

View File

@@ -13,6 +13,8 @@ import "@src/TychoRouter.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol";
import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol";
import {Permit2TestHelper} from "./Permit2TestHelper.sol";
import "./TestUtils.sol";
contract TychoRouterExposed is TychoRouter {
constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {}
@@ -25,16 +27,23 @@ contract TychoRouterExposed is TychoRouter {
return _unwrapETH(amount);
}
function exposedSwap(
function exposedSplitSwap(
uint256 amountIn,
uint256 nTokens,
bytes calldata swaps
) external returns (uint256) {
return _swap(amountIn, nTokens, swaps);
return _splitSwap(amountIn, nTokens, swaps);
}
function exposedSequentialSwap(uint256 amountIn, bytes calldata swaps)
external
returns (uint256)
{
return _sequentialSwap(amountIn, swaps);
}
}
contract TychoRouterTestSetup is Test, Constants {
contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
TychoRouterExposed tychoRouter;
address tychoRouterAddr;
UniswapV2Executor public usv2Executor;
@@ -47,7 +56,7 @@ contract TychoRouterTestSetup is Test, Constants {
MockERC20[] tokens;
function setUp() public {
uint256 forkBlock = 21817316;
uint256 forkBlock = 22082754;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
vm.startPrank(ADMIN);
@@ -71,7 +80,6 @@ contract TychoRouterTestSetup is Test, Constants {
tychoRouter = new TychoRouterExposed(PERMIT2_ADDRESS, 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);
tychoRouter.grantRole(keccak256("UNPAUSER_ROLE"), UNPAUSER);
tychoRouter.grantRole(
@@ -91,14 +99,17 @@ contract TychoRouterTestSetup is Test, Constants {
address ekuboCore = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444;
IPoolManager poolManager = IPoolManager(poolManagerAddress);
usv2Executor = new UniswapV2Executor(factoryV2, initCodeV2);
usv3Executor = new UniswapV3Executor(factoryV3, initCodeV3);
usv4Executor = new UniswapV4Executor(poolManager);
pancakev3Executor =
new UniswapV3Executor(factoryPancakeV3, initCodePancakeV3);
balancerv2Executor = new BalancerV2Executor();
ekuboExecutor = new EkuboExecutor(ekuboCore);
curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE);
usv2Executor =
new UniswapV2Executor(factoryV2, initCodeV2, PERMIT2_ADDRESS, 30);
usv3Executor =
new UniswapV3Executor(factoryV3, initCodeV3, PERMIT2_ADDRESS);
usv4Executor = new UniswapV4Executor(poolManager, PERMIT2_ADDRESS);
pancakev3Executor = new UniswapV3Executor(
factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS
);
balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS);
ekuboExecutor = new EkuboExecutor(ekuboCore, PERMIT2_ADDRESS);
curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS);
address[] memory executors = new address[](7);
executors[0] = address(usv2Executor);
@@ -123,84 +134,6 @@ contract TychoRouterTestSetup is Test, Constants {
}
}
/**
* @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(PERMIT2_ADDRESS, 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,
PERMIT2_ADDRESS
)
);
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
@@ -214,7 +147,23 @@ contract TychoRouterTestSetup is Test, Constants {
}
}
function encodeSwap(
function encodeSingleSwap(address executor, bytes memory protocolData)
internal
pure
returns (bytes memory)
{
return abi.encodePacked(executor, protocolData);
}
function encodeSequentialSwap(address executor, bytes memory protocolData)
internal
pure
returns (bytes memory)
{
return abi.encodePacked(executor, protocolData);
}
function encodeSplitSwap(
uint8 tokenInIndex,
uint8 tokenOutIndex,
uint24 split,
@@ -230,9 +179,11 @@ contract TychoRouterTestSetup is Test, Constants {
address tokenIn,
address target,
address receiver,
bool zero2one
bool zero2one,
TokenTransfer.TransferType transferType
) internal pure returns (bytes memory) {
return abi.encodePacked(tokenIn, target, receiver, zero2one);
return
abi.encodePacked(tokenIn, target, receiver, zero2one, transferType);
}
function encodeUniswapV3Swap(
@@ -240,11 +191,18 @@ contract TychoRouterTestSetup is Test, Constants {
address tokenOut,
address receiver,
address target,
bool zero2one
bool zero2one,
TokenTransfer.TransferType transferType
) internal view returns (bytes memory) {
IUniswapV3Pool pool = IUniswapV3Pool(target);
return abi.encodePacked(
tokenIn, tokenOut, pool.fee(), receiver, target, zero2one
tokenIn,
tokenOut,
pool.fee(),
receiver,
target,
zero2one,
transferType
);
}
}

View File

@@ -0,0 +1,26 @@
test_uniswap_v3_uniswap_v2:e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000bf00692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb8004375dff511095cc5a197a54140a24efef3a416cbcdf9626bc03e24f779434178a73a0b4bad62ed000100525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2010500
test_single_encoding_strategy_ekubo:20144a070000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000071a0cb889707d426a7a386870a03bc70d1b069759805cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000000000000000000000000000000000
test_uniswap_v3_uniswap_v3:e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb83ede3eca2a72b3aecc820e955b36f38437d01395cbcdf9626bc03e24f779434178a73a0b4bad62ed000100692e234dae75c793f67a35089c9d99245e1c58470b2260fac5e5542a773aa44fbcfedf7c193bc2c599a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc299ac8ca7087fa4a2a1fb6357269965a2014abc35010000000000000000000000
test_balancer_v2_uniswap_v2:e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c80072c7183455a4c133ae270771860664b6b7ec320bb1c02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599a6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e004375dff511095cc5a197a54140a24efef3a416010300525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20105000000000000000000000000000000000000000000000000
test_sequential_swap_strategy_encoder_no_permit2:e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d940004375dff511095cc5a197a54140a24efef3a416000100525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20105000000000000000000000000000000000000000000000000
test_sequential_encoding_strategy_usv4:51bcc7b6000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000005064ff624d54346285543f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000006838268800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004182b5da1843c415fd1304215c3049ced9c5e9b1f66463d5b035d67eb6fe8903de2564f5cb6740cdd838d7e497c61a75ccc572273a67e8c4b9e63f5f1ffc31e3bb1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000880086f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d23119330002cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c6982508145454ce325ddbe47a25d4ec3d23119330061a80001f4000000000000000000000000000000000000000000000000
test_single_encoding_strategy_usv4_eth_out:7c55384600000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f81490b4f29aade000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000000000000000000000000000000000006838268800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a0900000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000418b43338a724ecb56c3c2ba7b10f26445ef901fa89fb585ad97771b24b87b2d71627d7dcd178bce3594dff4e4c024bd34c64ca33eda7ed6419c627e9a9b089e591c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007300710001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000002cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c00000000000000000000000000
test_sequential_swap_strategy_encoder:51bcc7b60000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006838268800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000411ddba9e27afa444896a84afcbd773cd492da98731f40e28243bf0aae4f345b7d44b7a6d220f7e5345055b7fe382114f0bf9f1ddb9e97987515880311f25f001b1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d940004375dff511095cc5a197a54140a24efef3a416000200525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20105000000000000000000000000000000000000000000000000
test_single_swap_strategy_encoder_no_permit2:20144a070000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000058e7926ee858a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200010000000000000000000000000000
test_single_swap_strategy_encoder_no_transfer_in:20144a070000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000058e7926ee858a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000
test_single_encoding_strategy_usv4_eth_in:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000007e0a55d4322a6e93c2379c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006838268800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041657dbf542ed8b39f717c204413c77002d0d75fb3f81300bfef8eedf5889b55523868c42c0e2ef53fe7b82229ceceabe1b662201898043519d042e333c7492dad1c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006cf62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330105cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc26982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000000000000000
test_sequential_strategy_cyclic_swap:51bcc7b60000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ec8f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006838268800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041de5b09c674e85fef9de204fd3a03e190b1d06ebb6847543298183db02b46c0123c1433aec9951dfa391bc489c756816c4e87851882c8b4adbd21931490a820781b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f5640010200692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000000000000000000000000
test_single_encoding_strategy_curve_st_eth:20144a070000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ae7ab96520de3a18e5e111b5eaab095312d7fe84000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000691d1499e622d69689cdf9004d05ec547d650ff211eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeae7ab96520de3a18e5e111b5eaab095312d7fe84dc24316b9ae028f1497c275eb9192a3ea0f670220100010005cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000
test_single_swap_strategy_encoder:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000006b56051582a970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006838268900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09100000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041df0cad7d6d1228221ff55af2259877bbc38b6ef4af79f90757eab0576601a2f522e27881b929a5053eccb0d2fddc18db0bf3e905e407b5f17461e1bf64cbe70e1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200020000000000000000000000000000
test_single_encoding_strategy_curve:20144a070000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000055c08ca52497e2f1534b59e2917bf524d4765257000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000691d1499e622d69689cdf9004d05ec547d650ff21155c08ca52497e2f1534b59e2917bf524d4765257c02aaa39b223fe8d0a0e5c4f27ead9083c756cc277146b0a1d08b6844376df6d9da99ba7f1b19e710201000103cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000
test_single_swap_strategy_encoder_unwrap:30ace1b10000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be00000000000000000000000000000000000000000000000000000000000006838268900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09100000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000004148c4a9386769cda15972ca351ee585b996d184bcf3539441f103ca8477ddc35b4f34e91f490a181166440ceabe512ecc71bd00615ef5ba0ac5a7c58f2e0653511c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501020000000000000000000000000000
test_single_swap_strategy_encoder_wrap:30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000059fb7d3830e6fc064b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006838268900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09100000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041ca7017e254f4fc333dbe49435283ee7d7bbf0ab261f79a3606848abbc7387dc2563fcdb32e670f1f80e729326d8d9973aef060a7f7e28d9c8ec2420cad05aa6f1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000
test_split_output_cyclic_swap:7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005e703f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006838268900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09100000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004177fcc3f010414cde9910d59346b7cac5341063addb7f041740fd0851cff3cd914b8b63c61edabe968f912b2ed17f273221a995d049e5d198384b5dbd00edb2011c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d013950102006e01009999992e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc288e6a0c2ddd26feeb64f039a2c41296fcb3f56400000006e01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000000000000000000
test_split_input_cyclic_swap:7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ef619b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006838268900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09100000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004177fcc3f010414cde9910d59346b7cac5341063addb7f041740fd0851cff3cd914b8b63c61edabe968f912b2ed17f273221a995d049e5d198384b5dbd00edb2011c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139006e00019999992e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f56400102006e00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d80102005701000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b4e16d0168e52d35cacd2c6185b44281ec28c9dccd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000
test_split_swap_strategy_encoder:7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006838268900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a091000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041df0cad7d6d1228221ff55af2259877bbc38b6ef4af79f90757eab0576601a2f522e27881b929a5053eccb0d2fddc18db0bf3e905e407b5f17461e1bf64cbe70e1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164005700028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950002005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950002005702030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d5cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20100005701030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2010000000000000000000000000000000000000000000000000000000000
test_uniswap_v3_curve:e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb83ede3eca2a72b3aecc820e955b36f38437d01395cbcdf9626bc03e24f779434178a73a0b4bad62ed000100691d1499e622d69689cdf9004d05ec547d650ff2112260fac5e5542a773aa44fbcfedf7c193bc2c599dac17f958d2ee523a2206206994597c13d831ec7d51a44d3fae010294c616388b506acda1bfaae460301000105cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000
test_multi_protocol:51bcc7b600000000000000000000000000000000000000000000005150ae84a8cdf000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a2958f36da71a9200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000005150ae84a8cdf00000000000000000000000000000000000000000000000000000000000006838268a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006810a09200000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041cf9d1ecdf2b9df43bdac811677440db893f99f04f5addb24c82b15d766c87e5c0091919a2c43ed9dc6fbd1237ce2bed73a3ea27cd638af11c1bb5673468e1d051b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021400525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501020072c7183455a4c133ae270771860664b6b7ec320bb1c02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599a6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e3ede3eca2a72b3aecc820e955b36f38437d01395010500691d1499e622d69689cdf9004d05ec547d650ff2112260fac5e5542a773aa44fbcfedf7c193bc2c599dac17f958d2ee523a2206206994597c13d831ec7d51a44d3fae010294c616388b506acda1bfaae4603010001053ede3eca2a72b3aecc820e955b36f38437d013950071a0cb889707d426a7a386870a03bc70d1b0697598003ede3eca2a72b3aecc820e955b36f38437d01395dac17f958d2ee523a2206206994597c13d831ec7a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000001a36e2eb1c43200000032006cf62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c000000000000000000000000
test_encode_balancer_v2:c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0105
test_ekubo_encode_swap_multi:00ca4f73fe97d0b987a0d12b39bbd562c779bab6f60000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000001a36e2eb1c43200000032
test_encode_uniswap_v4_sequential_swap:4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990100cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec70000640000012260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c
test_encode_uniswap_v4_simple_swap:4c9edd5852cd905f086c759e8383e09bff1e68b3dac17f958d2ee523a2206206994597c13d831ec70100cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec7000064000001

View File

@@ -1,11 +1,13 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../TestUtils.sol";
import "@src/executors/BalancerV2Executor.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";
import {Constants} from "../Constants.sol";
contract BalancerV2ExecutorExposed is BalancerV2Executor {
constructor(address _permit2) BalancerV2Executor(_permit2) {}
function decodeParams(bytes calldata data)
external
pure
@@ -14,18 +16,15 @@ contract BalancerV2ExecutorExposed is BalancerV2Executor {
IERC20 tokenOut,
bytes32 poolId,
address receiver,
bool needsApproval
bool needsApproval,
TransferType transferType
)
{
return _decodeData(data);
}
}
contract BalancerV2ExecutorTest is
BalancerV2ExecutorExposed,
Test,
Constants
{
contract BalancerV2ExecutorTest is Constants, TestUtils {
using SafeERC20 for IERC20;
BalancerV2ExecutorExposed balancerV2Exposed;
@@ -37,12 +36,17 @@ contract BalancerV2ExecutorTest is
function setUp() public {
uint256 forkBlock = 17323404;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
balancerV2Exposed = new BalancerV2ExecutorExposed();
balancerV2Exposed = new BalancerV2ExecutorExposed(PERMIT2_ADDRESS);
}
function testDecodeParams() public view {
bytes memory params = abi.encodePacked(
WETH_ADDR, BAL_ADDR, WETH_BAL_POOL_ID, address(2), true
WETH_ADDR,
BAL_ADDR,
WETH_BAL_POOL_ID,
address(2),
true,
TokenTransfer.TransferType.NONE
);
(
@@ -50,7 +54,8 @@ contract BalancerV2ExecutorTest is
IERC20 tokenOut,
bytes32 poolId,
address receiver,
bool needsApproval
bool needsApproval,
TokenTransfer.TransferType transferType
) = balancerV2Exposed.decodeParams(params);
assertEq(address(tokenIn), WETH_ADDR);
@@ -58,6 +63,7 @@ contract BalancerV2ExecutorTest is
assertEq(poolId, WETH_BAL_POOL_ID);
assertEq(receiver, address(2));
assertEq(needsApproval, true);
assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE));
}
function testDecodeParamsInvalidDataLength() public {
@@ -70,8 +76,14 @@ contract BalancerV2ExecutorTest is
function testSwap() public {
uint256 amountIn = 10 ** 18;
bytes memory protocolData =
abi.encodePacked(WETH_ADDR, BAL_ADDR, WETH_BAL_POOL_ID, BOB, true);
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
BAL_ADDR,
WETH_BAL_POOL_ID,
BOB,
true,
TokenTransfer.TransferType.NONE
);
deal(WETH_ADDR, address(balancerV2Exposed), amountIn);
uint256 balanceBefore = BAL.balanceOf(BOB);
@@ -84,16 +96,15 @@ contract BalancerV2ExecutorTest is
}
function testDecodeIntegration() public view {
// Generated by the SwapEncoder - test_encode_balancer_v2
bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01";
loadCallDataFromFile("test_encode_balancer_v2");
(
IERC20 tokenIn,
IERC20 tokenOut,
bytes32 poolId,
address receiver,
bool needsApproval
bool needsApproval,
TokenTransfer.TransferType transferType
) = balancerV2Exposed.decodeParams(protocolData);
assertEq(address(tokenIn), WETH_ADDR);
@@ -101,12 +112,13 @@ contract BalancerV2ExecutorTest is
assertEq(poolId, WETH_BAL_POOL_ID);
assertEq(receiver, BOB);
assertEq(needsApproval, true);
assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE));
}
function testSwapIntegration() public {
// Generated by the SwapEncoder - test_encode_balancer_v2
bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01";
loadCallDataFromFile("test_encode_balancer_v2");
uint256 amountIn = 10 ** 18;
deal(WETH_ADDR, address(balancerV2Exposed), amountIn);

View File

@@ -22,7 +22,9 @@ interface MetaRegistry {
}
contract CurveExecutorExposed is CurveExecutor {
constructor(address _nativeToken) CurveExecutor(_nativeToken) {}
constructor(address _nativeToken, address _permit2)
CurveExecutor(_nativeToken, _permit2)
{}
function decodeData(bytes calldata data)
external
@@ -34,7 +36,9 @@ contract CurveExecutorExposed is CurveExecutor {
uint8 poolType,
int128 i,
int128 j,
bool tokenApprovalNeeded
bool tokenApprovalNeeded,
TokenTransfer.TransferType transferType,
address receiver
)
{
return _decodeData(data);
@@ -50,7 +54,8 @@ contract CurveExecutorTest is Test, Constants {
function setUp() public {
uint256 forkBlock = 22031795;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
curveExecutorExposed = new CurveExecutorExposed(ETH_ADDR_FOR_CURVE);
curveExecutorExposed =
new CurveExecutorExposed(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS);
metaRegistry = MetaRegistry(CURVE_META_REGISTRY);
}
@@ -62,7 +67,9 @@ contract CurveExecutorTest is Test, Constants {
uint8(3),
uint8(2),
uint8(0),
true
true,
TokenTransfer.TransferType.NONE,
ALICE
);
(
@@ -72,7 +79,9 @@ contract CurveExecutorTest is Test, Constants {
uint8 poolType,
int128 i,
int128 j,
bool tokenApprovalNeeded
bool tokenApprovalNeeded,
TokenTransfer.TransferType transferType,
address receiver
) = curveExecutorExposed.decodeData(data);
assertEq(tokenIn, WETH_ADDR);
@@ -82,6 +91,8 @@ contract CurveExecutorTest is Test, Constants {
assertEq(i, 2);
assertEq(j, 0);
assertEq(tokenApprovalNeeded, true);
assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE));
assertEq(receiver, ALICE);
}
function testTriPool() public {
@@ -89,15 +100,12 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 1 ether;
deal(DAI_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(DAI_ADDR, USDC_ADDR, TRIPOOL, 1);
bytes memory data = _getData(DAI_ADDR, USDC_ADDR, TRIPOOL, 1, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 999797);
assertEq(
IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), amountOut);
}
function testStEthPool() public {
@@ -106,14 +114,14 @@ contract CurveExecutorTest is Test, Constants {
deal(address(curveExecutorExposed), amountIn);
bytes memory data =
_getData(ETH_ADDR_FOR_CURVE, STETH_ADDR, STETH_POOL, 1);
_getData(ETH_ADDR_FOR_CURVE, STETH_ADDR, STETH_POOL, 1, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 1001072414418410897);
assertEq(
IERC20(STETH_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
IERC20(STETH_ADDR).balanceOf(ALICE),
amountOut - 1 // there is something weird in this pool, but won't investigate for now because we don't currently support it in the simulation
);
}
@@ -122,15 +130,13 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 1 ether;
deal(WETH_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(WETH_ADDR, WBTC_ADDR, TRICRYPTO2_POOL, 3);
bytes memory data =
_getData(WETH_ADDR, WBTC_ADDR, TRICRYPTO2_POOL, 3, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 2279618);
assertEq(
IERC20(WBTC_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
assertEq(IERC20(WBTC_ADDR).balanceOf(ALICE), amountOut);
}
function testSUSDPool() public {
@@ -138,15 +144,12 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 100 * 10 ** 6;
deal(USDC_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(USDC_ADDR, SUSD_ADDR, SUSD_POOL, 1);
bytes memory data = _getData(USDC_ADDR, SUSD_ADDR, SUSD_POOL, 1, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 100488101605550214590);
assertEq(
IERC20(SUSD_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
assertEq(IERC20(SUSD_ADDR).balanceOf(ALICE), amountOut);
}
function testFraxUsdcPool() public {
@@ -154,15 +157,13 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 1 ether;
deal(FRAX_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(FRAX_ADDR, USDC_ADDR, FRAX_USDC_POOL, 1);
bytes memory data =
_getData(FRAX_ADDR, USDC_ADDR, FRAX_USDC_POOL, 1, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 998097);
assertEq(
IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), amountOut);
}
function testUsdeUsdcPool() public {
@@ -170,15 +171,13 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 100 * 10 ** 6;
deal(USDC_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(USDC_ADDR, USDE_ADDR, USDE_USDC_POOL, 1);
bytes memory data =
_getData(USDC_ADDR, USDE_ADDR, USDE_USDC_POOL, 1, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 100064812138999986170);
assertEq(
IERC20(USDE_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
assertEq(IERC20(USDE_ADDR).balanceOf(ALICE), amountOut);
}
function testDolaFraxPyusdPool() public {
@@ -187,32 +186,27 @@ contract CurveExecutorTest is Test, Constants {
deal(DOLA_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
_getData(DOLA_ADDR, FRAXPYUSD_POOL, DOLA_FRAXPYUSD_POOL, 1);
_getData(DOLA_ADDR, FRAXPYUSD_POOL, DOLA_FRAXPYUSD_POOL, 1, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 99688992);
assertEq(
IERC20(FRAXPYUSD_POOL).balanceOf(address(curveExecutorExposed)),
amountOut
);
assertEq(IERC20(FRAXPYUSD_POOL).balanceOf(ALICE), amountOut);
}
function testCryptoPoolWithETH() public {
// Swapping XYO -> ETH on a CryptoPool, deployed by factory 0xF18056Bbd320E96A48e3Fbf8bC061322531aac99
uint256 amountIn = 1 ether;
uint256 initialBalance = address(curveExecutorExposed).balance; // this address already has some ETH assigned to it
uint256 initialBalance = address(ALICE).balance; // this address already has some ETH assigned to it
deal(XYO_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
_getData(XYO_ADDR, ETH_ADDR_FOR_CURVE, ETH_XYO_POOL, 2);
_getData(XYO_ADDR, ETH_ADDR_FOR_CURVE, ETH_XYO_POOL, 2, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 6081816039338);
assertEq(
address(curveExecutorExposed).balance, initialBalance + amountOut
);
assertEq(ALICE.balance, initialBalance + amountOut);
}
function testCryptoPool() public {
@@ -220,15 +214,13 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 1000 ether;
deal(BSGG_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(BSGG_ADDR, USDT_ADDR, BSGG_USDT_POOL, 2);
bytes memory data =
_getData(BSGG_ADDR, USDT_ADDR, BSGG_USDT_POOL, 2, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 23429);
assertEq(
IERC20(USDT_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
assertEq(IERC20(USDT_ADDR).balanceOf(ALICE), amountOut);
}
function testTricryptoPool() public {
@@ -236,15 +228,13 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 1 ether;
deal(WETH_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(WETH_ADDR, USDC_ADDR, TRICRYPTO_POOL, 2);
bytes memory data =
_getData(WETH_ADDR, USDC_ADDR, TRICRYPTO_POOL, 2, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 1861130974);
assertEq(
IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), amountOut);
}
function testTwoCryptoPool() public {
@@ -252,32 +242,27 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 1 ether;
deal(UWU_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(UWU_ADDR, WETH_ADDR, UWU_WETH_POOL, 2);
bytes memory data =
_getData(UWU_ADDR, WETH_ADDR, UWU_WETH_POOL, 2, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 2873786684675);
assertEq(
IERC20(WETH_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), amountOut);
}
function testStableSwapPool() public {
// Swapping CRVUSD -> USDT on a StableSwap pool, deployed by factory 0x4F8846Ae9380B90d2E71D5e3D042dff3E7ebb40d (plain pool)
uint256 amountIn = 1 ether;
deal(CRVUSD_ADDR, address(curveExecutorExposed), amountIn);
deal(USDT_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
_getData(CRVUSD_ADDR, USDT_ADDR, CRVUSD_USDT_POOL, 1);
_getData(USDT_ADDR, CRVUSD_ADDR, CRVUSD_USDT_POOL, 1, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 999910);
assertEq(
IERC20(USDT_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
assertEq(amountOut, 10436946786333182306400100);
assertEq(IERC20(CRVUSD_ADDR).balanceOf(ALICE), amountOut);
}
function testMetaPool() public {
@@ -286,22 +271,20 @@ contract CurveExecutorTest is Test, Constants {
deal(WTAO_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
_getData(WTAO_ADDR, WSTTAO_ADDR, WSTTAO_WTAO_POOL, 1);
_getData(WTAO_ADDR, WSTTAO_ADDR, WSTTAO_WTAO_POOL, 1, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 32797923610);
assertEq(
IERC20(WSTTAO_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
assertEq(IERC20(WSTTAO_ADDR).balanceOf(ALICE), amountOut);
}
function _getData(
address tokenIn,
address tokenOut,
address pool,
uint8 poolType
uint8 poolType,
address receiver
) internal view returns (bytes memory data) {
(int128 i, int128 j) = _getIndexes(tokenIn, tokenOut, pool);
data = abi.encodePacked(
@@ -311,7 +294,9 @@ contract CurveExecutorTest is Test, Constants {
poolType,
uint8(uint256(uint128(i))),
uint8(uint256(uint128(j))),
true
true,
TokenTransfer.TransferType.NONE,
receiver
);
}

View File

@@ -1,14 +1,15 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import {EkuboExecutor} from "@src/executors/EkuboExecutor.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../TestUtils.sol";
import {Constants} from "../Constants.sol";
import {Test, console} from "forge-std/Test.sol";
import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol";
import {EkuboExecutor, TokenTransfer} from "@src/executors/EkuboExecutor.sol";
import {ICore} from "@ekubo/interfaces/ICore.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol";
import {console} from "forge-std/Test.sol";
contract EkuboExecutorTest is Test, Constants {
contract EkuboExecutorTest is Constants, TestUtils {
address constant EXECUTOR_ADDRESS =
0xcA4F73Fe97D0B987a0D12B39BBD562c779BAb6f6; // Same address as in swap_encoder.rs tests
EkuboExecutor executor;
@@ -26,7 +27,7 @@ contract EkuboExecutorTest is Test, Constants {
deployCodeTo(
"executors/EkuboExecutor.sol",
abi.encode(CORE_ADDRESS),
abi.encode(CORE_ADDRESS, PERMIT2_ADDRESS),
EXECUTOR_ADDRESS
);
executor = EkuboExecutor(payable(EXECUTOR_ADDRESS));
@@ -44,6 +45,7 @@ contract EkuboExecutorTest is Test, Constants {
uint256 usdcBalanceBeforeExecutor = USDC.balanceOf(address(executor));
bytes memory data = abi.encodePacked(
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL), // transferType (transfer from executor to core)
address(executor), // receiver
NATIVE_TOKEN_ADDRESS, // tokenIn
USDC_ADDR, // tokenOut
@@ -80,6 +82,7 @@ contract EkuboExecutorTest is Test, Constants {
uint256 ethBalanceBeforeExecutor = address(executor).balance;
bytes memory data = abi.encodePacked(
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL), // transferType (transfer from executor to core)
address(executor), // receiver
USDC_ADDR, // tokenIn
NATIVE_TOKEN_ADDRESS, // tokenOut
@@ -137,6 +140,7 @@ contract EkuboExecutorTest is Test, Constants {
// Same test case as in swap_encoder::tests::ekubo::test_encode_swap_multi
function testMultiHopSwap() public {
bytes memory data = abi.encodePacked(
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL), // transferType
address(executor), // receiver
NATIVE_TOKEN_ADDRESS, // tokenIn
USDC_ADDR, // tokenOut of 1st swap
@@ -151,8 +155,6 @@ contract EkuboExecutorTest is Test, Constants {
// Data is generated by test case in swap_encoder::tests::ekubo::test_encode_swap_multi
function testMultiHopSwapIntegration() public {
multiHopSwap(
hex"ca4f73fe97d0b987a0d12b39bbd562c779bab6f60000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000001a36e2eb1c43200000032"
);
multiHopSwap(loadCallDataFromFile("test_ekubo_encode_swap_multi"));
}
}

View File

@@ -2,13 +2,18 @@
pragma solidity ^0.8.26;
import "@src/executors/UniswapV2Executor.sol";
import "@src/executors/TokenTransfer.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";
import {Constants} from "../Constants.sol";
import {Permit2TestHelper} from "../Permit2TestHelper.sol";
contract UniswapV2ExecutorExposed is UniswapV2Executor {
constructor(address _factory, bytes32 _initCode)
UniswapV2Executor(_factory, _initCode)
{}
constructor(
address _factory,
bytes32 _initCode,
address _permit2,
uint256 _feeBps
) UniswapV2Executor(_factory, _initCode, _permit2, _feeBps) {}
function decodeParams(bytes calldata data)
external
@@ -17,7 +22,8 @@ contract UniswapV2ExecutorExposed is UniswapV2Executor {
IERC20 inToken,
address target,
address receiver,
bool zeroForOne
bool zeroForOne,
TransferType transferType
)
{
return _decodeData(data);
@@ -46,7 +52,7 @@ contract FakeUniswapV2Pool {
}
}
contract UniswapV2ExecutorTest is Test, Constants {
contract UniswapV2ExecutorTest is Test, Constants, Permit2TestHelper {
using SafeERC20 for IERC20;
UniswapV2ExecutorExposed uniswapV2Exposed;
@@ -54,32 +60,54 @@ contract UniswapV2ExecutorTest is Test, Constants {
UniswapV2ExecutorExposed pancakeswapV2Exposed;
IERC20 WETH = IERC20(WETH_ADDR);
IERC20 DAI = IERC20(DAI_ADDR);
IAllowanceTransfer permit2;
function setUp() public {
uint256 forkBlock = 17323404;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
uniswapV2Exposed = new UniswapV2ExecutorExposed(
USV2_FACTORY_ETHEREUM, USV2_POOL_CODE_INIT_HASH
USV2_FACTORY_ETHEREUM, USV2_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS, 30
);
sushiswapV2Exposed = new UniswapV2ExecutorExposed(
SUSHISWAPV2_FACTORY_ETHEREUM, SUSHIV2_POOL_CODE_INIT_HASH
SUSHISWAPV2_FACTORY_ETHEREUM,
SUSHIV2_POOL_CODE_INIT_HASH,
PERMIT2_ADDRESS,
30
);
pancakeswapV2Exposed = new UniswapV2ExecutorExposed(
PANCAKESWAPV2_FACTORY_ETHEREUM, PANCAKEV2_POOL_CODE_INIT_HASH
PANCAKESWAPV2_FACTORY_ETHEREUM,
PANCAKEV2_POOL_CODE_INIT_HASH,
PERMIT2_ADDRESS,
25
);
permit2 = IAllowanceTransfer(PERMIT2_ADDRESS);
}
function testDecodeParams() public view {
bytes memory params =
abi.encodePacked(WETH_ADDR, address(2), address(3), false);
bytes memory params = abi.encodePacked(
WETH_ADDR,
address(2),
address(3),
false,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
(IERC20 tokenIn, address target, address receiver, bool zeroForOne) =
uniswapV2Exposed.decodeParams(params);
(
IERC20 tokenIn,
address target,
address receiver,
bool zeroForOne,
TokenTransfer.TransferType transferType
) = uniswapV2Exposed.decodeParams(params);
assertEq(address(tokenIn), WETH_ADDR);
assertEq(target, address(2));
assertEq(receiver, address(3));
assertEq(zeroForOne, false);
assertEq(
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL),
uint8(transferType)
);
}
function testDecodeParamsInvalidDataLength() public {
@@ -126,12 +154,17 @@ contract UniswapV2ExecutorTest is Test, Constants {
assertGe(amountOut, 0);
}
function testSwap() public {
function testSwapWithTransfer() public {
uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891;
bool zeroForOne = false;
bytes memory protocolData =
abi.encodePacked(WETH_ADDR, WETH_DAI_POOL, BOB, zeroForOne);
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
WETH_DAI_POOL,
BOB,
zeroForOne,
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
);
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
uniswapV2Exposed.swap(amountIn, protocolData);
@@ -140,24 +173,102 @@ contract UniswapV2ExecutorTest is Test, Constants {
assertGe(finalBalance, amountOut);
}
function testDecodeIntegration() public view {
// Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode
bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f5640000000000000000000000000000000000000000100";
function testSwapWithTransferFrom() public {
uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891;
bool zeroForOne = false;
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
WETH_DAI_POOL,
BOB,
zeroForOne,
uint8(TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL)
);
(IERC20 tokenIn, address target, address receiver, bool zeroForOne) =
uniswapV2Exposed.decodeParams(protocolData);
deal(WETH_ADDR, address(this), amountIn);
IERC20(WETH_ADDR).approve(address(uniswapV2Exposed), amountIn);
uniswapV2Exposed.swap(amountIn, protocolData);
uint256 finalBalance = DAI.balanceOf(BOB);
assertGe(finalBalance, amountOut);
}
function testSwapWithPermit2TransferFrom() public {
uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891;
bool zeroForOne = false;
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
WETH_DAI_POOL,
ALICE,
zeroForOne,
uint8(TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_PROTOCOL)
);
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(
WETH_ADDR, address(uniswapV2Exposed), amountIn
);
// Assume the permit2.approve method will be called from the TychoRouter
// Replicate this scenario in this test.
permit2.permit(ALICE, permitSingle, signature);
uniswapV2Exposed.swap(amountIn, protocolData);
vm.stopPrank();
uint256 finalBalance = DAI.balanceOf(ALICE);
assertGe(finalBalance, amountOut);
}
function testSwapNoTransfer() public {
uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891;
bool zeroForOne = false;
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
WETH_DAI_POOL,
BOB,
zeroForOne,
uint8(TokenTransfer.TransferType.NONE)
);
deal(WETH_ADDR, address(this), amountIn);
IERC20(WETH_ADDR).transfer(address(WETH_DAI_POOL), amountIn);
uniswapV2Exposed.swap(amountIn, protocolData);
uint256 finalBalance = DAI.balanceOf(BOB);
assertGe(finalBalance, amountOut);
}
function testDecodeIntegration() public view {
bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f564000000000000000000000000000000000000000010000";
(
IERC20 tokenIn,
address target,
address receiver,
bool zeroForOne,
TokenTransfer.TransferType transferType
) = uniswapV2Exposed.decodeParams(protocolData);
assertEq(address(tokenIn), WETH_ADDR);
assertEq(target, 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640);
assertEq(receiver, 0x0000000000000000000000000000000000000001);
assertEq(zeroForOne, false);
// TRANSFER = 0
assertEq(0, uint8(transferType));
}
function testSwapIntegration() public {
// Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode
bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb111d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e00";
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb111d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000";
uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891;
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
@@ -171,8 +282,13 @@ contract UniswapV2ExecutorTest is Test, Constants {
uint256 amountIn = 10 ** 18;
bool zeroForOne = false;
address fakePool = address(new FakeUniswapV2Pool(WETH_ADDR, DAI_ADDR));
bytes memory protocolData =
abi.encodePacked(WETH_ADDR, fakePool, BOB, zeroForOne);
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
fakePool,
BOB,
zeroForOne,
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
);
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
vm.expectRevert(UniswapV2Executor__InvalidTarget.selector);
@@ -186,8 +302,13 @@ contract UniswapV2ExecutorTest is Test, Constants {
vm.rollFork(26857267);
uint256 amountIn = 10 * 10 ** 6;
bool zeroForOne = true;
bytes memory protocolData =
abi.encodePacked(BASE_USDC, USDC_MAG7_POOL, BOB, zeroForOne);
bytes memory protocolData = abi.encodePacked(
BASE_USDC,
USDC_MAG7_POOL,
BOB,
zeroForOne,
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
);
deal(BASE_USDC, address(uniswapV2Exposed), amountIn);

View File

@@ -2,12 +2,14 @@
pragma solidity ^0.8.26;
import "@src/executors/UniswapV3Executor.sol";
import "@permit2/src/interfaces/IAllowanceTransfer.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";
import {Constants} from "../Constants.sol";
import {Permit2TestHelper} from "../Permit2TestHelper.sol";
contract UniswapV3ExecutorExposed is UniswapV3Executor {
constructor(address _factory, bytes32 _initCode)
UniswapV3Executor(_factory, _initCode)
constructor(address _factory, bytes32 _initCode, address _permit2)
UniswapV3Executor(_factory, _initCode, _permit2)
{}
function decodeData(bytes calldata data)
@@ -19,7 +21,8 @@ contract UniswapV3ExecutorExposed is UniswapV3Executor {
uint24 fee,
address receiver,
address target,
bool zeroForOne
bool zeroForOne,
TransferType transferType
)
{
return _decodeData(data);
@@ -35,30 +38,40 @@ contract UniswapV3ExecutorExposed is UniswapV3Executor {
}
}
contract UniswapV3ExecutorTest is Test, Constants {
contract UniswapV3ExecutorTest is Test, Constants, Permit2TestHelper {
using SafeERC20 for IERC20;
UniswapV3ExecutorExposed uniswapV3Exposed;
UniswapV3ExecutorExposed pancakeV3Exposed;
IERC20 WETH = IERC20(WETH_ADDR);
IERC20 DAI = IERC20(DAI_ADDR);
IAllowanceTransfer permit2;
function setUp() public {
uint256 forkBlock = 17323404;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
uniswapV3Exposed = new UniswapV3ExecutorExposed(
USV3_FACTORY_ETHEREUM, USV3_POOL_CODE_INIT_HASH
USV3_FACTORY_ETHEREUM, USV3_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS
);
pancakeV3Exposed = new UniswapV3ExecutorExposed(
PANCAKESWAPV3_DEPLOYER_ETHEREUM, PANCAKEV3_POOL_CODE_INIT_HASH
PANCAKESWAPV3_DEPLOYER_ETHEREUM,
PANCAKEV3_POOL_CODE_INIT_HASH,
PERMIT2_ADDRESS
);
permit2 = IAllowanceTransfer(PERMIT2_ADDRESS);
}
function testDecodeParams() public view {
uint24 expectedPoolFee = 500;
bytes memory data = abi.encodePacked(
WETH_ADDR, DAI_ADDR, expectedPoolFee, address(2), address(3), false
WETH_ADDR,
DAI_ADDR,
expectedPoolFee,
address(2),
address(3),
false,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
(
@@ -67,7 +80,8 @@ contract UniswapV3ExecutorTest is Test, Constants {
uint24 fee,
address receiver,
address target,
bool zeroForOne
bool zeroForOne,
TokenTransfer.TransferType transferType
) = uniswapV3Exposed.decodeData(data);
assertEq(tokenIn, WETH_ADDR);
@@ -76,6 +90,33 @@ contract UniswapV3ExecutorTest is Test, Constants {
assertEq(receiver, address(2));
assertEq(target, address(3));
assertEq(zeroForOne, false);
assertEq(
uint8(transferType),
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
);
}
function testSwapIntegration() public {
uint256 amountIn = 10 ** 18;
deal(WETH_ADDR, address(uniswapV3Exposed), amountIn);
uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI
bool zeroForOne = false;
bytes memory data = encodeUniswapV3Swap(
WETH_ADDR,
DAI_ADDR,
address(this),
DAI_WETH_USV3,
zeroForOne,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
uint256 amountOut = uniswapV3Exposed.swap(amountIn, data);
assertGe(amountOut, expAmountOut);
assertEq(IERC20(WETH_ADDR).balanceOf(address(uniswapV3Exposed)), 0);
assertGe(IERC20(DAI_ADDR).balanceOf(address(this)), expAmountOut);
}
function testDecodeParamsInvalidDataLength() public {
@@ -105,12 +146,18 @@ contract UniswapV3ExecutorTest is Test, Constants {
uint256 initialPoolReserve = IERC20(WETH_ADDR).balanceOf(DAI_WETH_USV3);
vm.startPrank(DAI_WETH_USV3);
bytes memory protocolData =
abi.encodePacked(WETH_ADDR, DAI_ADDR, poolFee);
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
DAI_ADDR,
poolFee,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL,
address(uniswapV3Exposed)
);
uint256 dataOffset = 3; // some offset
uint256 dataLength = protocolData.length;
bytes memory callbackData = abi.encodePacked(
bytes4(0xfa461e33),
int256(amountOwed), // amount0Delta
int256(0), // amount1Delta
dataOffset,
@@ -124,24 +171,6 @@ contract UniswapV3ExecutorTest is Test, Constants {
assertEq(finalPoolReserve - initialPoolReserve, amountOwed);
}
function testSwapIntegration() public {
uint256 amountIn = 10 ** 18;
deal(WETH_ADDR, address(uniswapV3Exposed), amountIn);
uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI
bool zeroForOne = false;
bytes memory data = encodeUniswapV3Swap(
WETH_ADDR, DAI_ADDR, address(this), DAI_WETH_USV3, zeroForOne
);
uint256 amountOut = uniswapV3Exposed.swap(amountIn, data);
assertGe(amountOut, expAmountOut);
assertEq(IERC20(WETH_ADDR).balanceOf(address(uniswapV3Exposed)), 0);
assertGe(IERC20(DAI_ADDR).balanceOf(address(this)), expAmountOut);
}
function testSwapFailureInvalidTarget() public {
uint256 amountIn = 10 ** 18;
deal(WETH_ADDR, address(uniswapV3Exposed), amountIn);
@@ -154,7 +183,8 @@ contract UniswapV3ExecutorTest is Test, Constants {
uint24(3000),
address(this),
fakePool,
zeroForOne
zeroForOne,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
vm.expectRevert(UniswapV3Executor__InvalidTarget.selector);
@@ -166,11 +196,18 @@ contract UniswapV3ExecutorTest is Test, Constants {
address tokenOut,
address receiver,
address target,
bool zero2one
bool zero2one,
TokenTransfer.TransferType transferType
) internal view returns (bytes memory) {
IUniswapV3Pool pool = IUniswapV3Pool(target);
return abi.encodePacked(
tokenIn, tokenOut, pool.fee(), receiver, target, zero2one
tokenIn,
tokenOut,
pool.fee(),
receiver,
target,
zero2one,
transferType
);
}
}

View File

@@ -2,14 +2,18 @@
pragma solidity ^0.8.26;
import "../../src/executors/UniswapV4Executor.sol";
import "../TestUtils.sol";
import "./UniswapV4Utils.sol";
import "@src/executors/TokenTransfer.sol";
import "@src/executors/UniswapV4Executor.sol";
import {Constants} from "../Constants.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";
import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";
contract UniswapV4ExecutorExposed is UniswapV4Executor {
constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {}
constructor(IPoolManager _poolManager, address _permit2)
UniswapV4Executor(_poolManager, _permit2)
{}
function decodeData(bytes calldata data)
external
@@ -18,7 +22,8 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor {
address tokenIn,
address tokenOut,
bool zeroForOne,
address callbackExecutor,
TokenTransfer.TransferType transferType,
address receiver,
UniswapV4Pool[] memory pools
)
{
@@ -26,7 +31,7 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor {
}
}
contract UniswapV4ExecutorTest is Test, Constants {
contract UniswapV4ExecutorTest is Constants, TestUtils {
using SafeERC20 for IERC20;
UniswapV4ExecutorExposed uniswapV4Exposed;
@@ -37,8 +42,9 @@ contract UniswapV4ExecutorTest is Test, Constants {
function setUp() public {
uint256 forkBlock = 21817316;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
uniswapV4Exposed =
new UniswapV4ExecutorExposed(IPoolManager(poolManager));
uniswapV4Exposed = new UniswapV4ExecutorExposed(
IPoolManager(poolManager), PERMIT2_ADDRESS
);
}
function testDecodeParams() public view {
@@ -47,6 +53,8 @@ contract UniswapV4ExecutorTest is Test, Constants {
int24 tickSpacing1 = 60;
uint24 pool2Fee = 1000;
int24 tickSpacing2 = -10;
TokenTransfer.TransferType transferType =
TokenTransfer.TransferType.TRANSFER_FROM_TO_PROTOCOL;
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](2);
@@ -62,21 +70,23 @@ contract UniswapV4ExecutorTest is Test, Constants {
});
bytes memory data = UniswapV4Utils.encodeExactInput(
USDE_ADDR, USDT_ADDR, zeroForOne, address(uniswapV4Exposed), pools
USDE_ADDR, USDT_ADDR, zeroForOne, transferType, ALICE, pools
);
(
address tokenIn,
address tokenOut,
bool zeroForOneDecoded,
address callbackExecutor,
TokenTransfer.TransferType transferTypeDecoded,
address receiver,
UniswapV4Executor.UniswapV4Pool[] memory decodedPools
) = uniswapV4Exposed.decodeData(data);
assertEq(tokenIn, USDE_ADDR);
assertEq(tokenOut, USDT_ADDR);
assertEq(zeroForOneDecoded, zeroForOne);
assertEq(callbackExecutor, address(uniswapV4Exposed));
assertEq(uint8(transferTypeDecoded), uint8(transferType));
assertEq(receiver, ALICE);
assertEq(decodedPools.length, 2);
assertEq(decodedPools[0].intermediaryToken, USDT_ADDR);
assertEq(decodedPools[0].fee, pool1Fee);
@@ -102,7 +112,12 @@ contract UniswapV4ExecutorTest is Test, Constants {
});
bytes memory data = UniswapV4Utils.encodeExactInput(
USDE_ADDR, USDT_ADDR, true, address(uniswapV4Exposed), pools
USDE_ADDR,
USDT_ADDR,
true,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL,
ALICE,
pools
);
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
@@ -111,14 +126,13 @@ contract UniswapV4ExecutorTest is Test, Constants {
USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn
);
assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut);
assertTrue(USDT.balanceOf(ALICE) == amountOut);
}
function testSingleSwapIntegration() public {
// USDE -> USDT
// Generated by the Tycho swap encoder - test_encode_uniswap_v4_simple_swap
bytes memory protocolData =
hex"4c9edd5852cd905f086c759e8383e09bff1e68b3dac17f958d2ee523a2206206994597c13d831ec701f62849f9a0b5bf2913b396098f7c7019b51a820adac17f958d2ee523a2206206994597c13d831ec7000064000001";
loadCallDataFromFile("test_encode_uniswap_v4_simple_swap");
uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager);
@@ -128,10 +142,9 @@ contract UniswapV4ExecutorTest is Test, Constants {
uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
assertEq(
USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn
USDE.balanceOf(ALICE), usdeBalanceBeforeSwapExecutor - amountIn
);
assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut);
assertTrue(USDT.balanceOf(ALICE) == amountOut);
}
function testMultipleSwap() public {
@@ -156,7 +169,12 @@ contract UniswapV4ExecutorTest is Test, Constants {
});
bytes memory data = UniswapV4Utils.encodeExactInput(
USDE_ADDR, WBTC_ADDR, true, address(uniswapV4Exposed), pools
USDE_ADDR,
WBTC_ADDR,
true,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL,
ALICE,
pools
);
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
@@ -165,17 +183,13 @@ contract UniswapV4ExecutorTest is Test, Constants {
USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn
);
assertTrue(
IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut
);
assertTrue(IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut);
}
function testMultipleSwapIntegration() public {
// USDE -> USDT -> WBTC
// Generated by the Tycho swap encoder - test_encode_uniswap_v4_sequential_swap
bytes memory protocolData =
hex"4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c59901f62849f9a0b5bf2913b396098f7c7019b51a820adac17f958d2ee523a2206206994597c13d831ec70000640000012260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c";
loadCallDataFromFile("test_encode_uniswap_v4_sequential_swap");
uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
@@ -189,8 +203,6 @@ contract UniswapV4ExecutorTest is Test, Constants {
USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn
);
assertTrue(
IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut
);
assertTrue(IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut);
}
}

View File

@@ -8,7 +8,8 @@ library UniswapV4Utils {
address tokenIn,
address tokenOut,
bool zeroForOne,
address callbackExecutor,
UniswapV4Executor.TransferType transferType,
address receiver,
UniswapV4Executor.UniswapV4Pool[] memory pools
) public pure returns (bytes memory) {
bytes memory encodedPools;
@@ -23,7 +24,7 @@ library UniswapV4Utils {
}
return abi.encodePacked(
tokenIn, tokenOut, zeroForOne, callbackExecutor, encodedPools
tokenIn, tokenOut, zeroForOne, transferType, receiver, encodedPools
);
}
}