Merge pull request #177 from propeller-heads/precious-backup-branch-2

feat: Gas optimizations feature
This commit is contained in:
dianacarvalho1
2025-04-23 15:16:48 +01:00
committed by GitHub
47 changed files with 8144 additions and 5064 deletions

View File

@@ -7,7 +7,7 @@
"pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9", "pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9",
"uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a", "uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a",
"vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1", "vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1",
"ekubo_v2": "0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7", "ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598",
"vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211" "vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211"
} }
} }

View File

@@ -6,9 +6,8 @@ use tycho_common::{
Bytes, Bytes,
}; };
use tycho_execution::encoding::{ use tycho_execution::encoding::{
evm::encoder_builder::EVMEncoderBuilder, evm::encoder_builders::TychoRouterEncoderBuilder,
models::{Solution, Swap}, models::{Solution, Swap},
tycho_encoder::TychoEncoder,
}; };
fn main() { fn main() {
@@ -19,10 +18,9 @@ fn main() {
.expect("Failed to create user address"); .expect("Failed to create user address");
// Initialize the encoder // Initialize the encoder
let encoder = EVMEncoderBuilder::new() let encoder = TychoRouterEncoderBuilder::new()
.chain(Chain::Ethereum) .chain(Chain::Ethereum)
.initialize_tycho_router_with_permit2(swapper_pk) .swapper_pk(swapper_pk)
.expect("Failed to create encoder builder")
.build() .build()
.expect("Failed to build encoder"); .expect("Failed to build encoder");
@@ -64,7 +62,7 @@ fn main() {
// Encode the solution // Encode the solution
let tx = encoder let tx = encoder
.encode_router_calldata(vec![solution.clone()]) .encode_calldata(vec![solution.clone()])
.expect("Failed to encode router calldata")[0] .expect("Failed to encode router calldata")[0]
.clone(); .clone();
println!(" ====== Simple swap WETH -> USDC ======"); println!(" ====== Simple swap WETH -> USDC ======");
@@ -137,7 +135,7 @@ fn main() {
// Encode the solution // Encode the solution
let complex_tx = encoder let complex_tx = encoder
.encode_router_calldata(vec![complex_solution]) .encode_calldata(vec![complex_solution])
.expect("Failed to encode router calldata")[0] .expect("Failed to encode router calldata")[0]
.clone(); .clone();

View File

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

View File

@@ -5,25 +5,28 @@ const hre = require("hardhat");
// Comment out the executors you don't want to deploy // Comment out the executors you don't want to deploy
const executors_to_deploy = { const executors_to_deploy = {
"ethereum": [ "ethereum": [
// USV2 - Args: Factory, Pool Init Code Hash // USV2 - Args: Factory, Pool Init Code Hash, Fee BPS
{ {
exchange: "UniswapV2Executor", args: [ exchange: "UniswapV2Executor", args: [
"0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
30
] ]
}, },
// SUSHISWAP - Args: Factory, Pool Init Code Hash // SUSHISWAP - Args: Factory, Pool Init Code Hash, Fee BPS, Fee BPS
{ {
exchange: "UniswapV2Executor", args: [ exchange: "UniswapV2Executor", args: [
"0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac", "0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac",
"0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303" "0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303",
30
] ]
}, },
// PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash // PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash, Fee BPS
{ {
exchange: "UniswapV2Executor", args: [ exchange: "UniswapV2Executor", args: [
"0x1097053Fd2ea711dad45caCcc45EfF7548fCB362", "0x1097053Fd2ea711dad45caCcc45EfF7548fCB362",
"0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d" "0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d",
25
] ]
}, },
// USV3 -Args: Factory, Pool Init Code Hash // USV3 -Args: Factory, Pool Init Code Hash
@@ -57,25 +60,28 @@ const executors_to_deploy = {
} }
], ],
"base": [ "base": [
// Args: Factory, Pool Init Code Hash // Args: Factory, Pool Init Code Hash, Fee BPS
{ {
exchange: "UniswapV2Executor", args: [ exchange: "UniswapV2Executor", args: [
"0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6", "0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6",
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
30
] ]
}, },
// SUSHISWAP V2 - Args: Factory, Pool Init Code Hash // SUSHISWAP V2 - Args: Factory, Pool Init Code Hash, Fee BPS
{ {
exchange: "UniswapV2Executor", args: [ exchange: "UniswapV2Executor", args: [
"0x71524B4f93c58fcbF659783284E38825f0622859", "0x71524B4f93c58fcbF659783284E38825f0622859",
"0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303" "0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303",
30
] ]
}, },
// PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash // PANCAKESWAP V2 - Args: Factory, Pool Init Code Hash, Fee BPS
{ {
exchange: "UniswapV2Executor", args: [ exchange: "UniswapV2Executor", args: [
"0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E", "0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E",
"0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d" "0x57224589c67f3f30a6b0d7a1b54cf3153ab84563bc609ef41dfb34f8b2974d2d",
25
] ]
}, },
// USV3 - Args: Factory, Pool Init Code Hash // USV3 - Args: Factory, Pool Init Code Hash
@@ -97,11 +103,12 @@ const executors_to_deploy = {
{exchange: "BalancerV2Executor", args: []}, {exchange: "BalancerV2Executor", args: []},
], ],
"unichain": [ "unichain": [
// Args: Factory, Pool Init Code Hash // Args: Factory, Pool Init Code Hash, Fee BPS
{ {
exchange: "UniswapV2Executor", args: [ exchange: "UniswapV2Executor", args: [
"0x1f98400000000000000000000000000000000002", "0x1f98400000000000000000000000000000000002",
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
30
] ]
}, },
// USV3 - Args: Factory, Pool Init Code Hash // USV3 - Args: Factory, Pool Init Code Hash

View File

@@ -4,7 +4,7 @@ pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol"; import "@interfaces/IExecutor.sol";
import "@interfaces/ICallback.sol"; import "@interfaces/ICallback.sol";
error Dispatcher__UnapprovedExecutor(); error Dispatcher__UnapprovedExecutor(address executor);
error Dispatcher__NonContractExecutor(); error Dispatcher__NonContractExecutor();
error Dispatcher__InvalidDataLength(); error Dispatcher__InvalidDataLength();
@@ -52,14 +52,18 @@ contract Dispatcher {
* @dev Calls an executor, assumes swap.protocolData contains * @dev Calls an executor, assumes swap.protocolData contains
* protocol-specific data required by the executor. * protocol-specific data required by the executor.
*/ */
// slither-disable-next-line delegatecall-loop // slither-disable-next-line delegatecall-loop,assembly
function _callExecutor( function _callExecutor(
address executor, address executor,
uint256 amount, uint256 amount,
bytes calldata data bytes calldata data
) internal returns (uint256 calculatedAmount) { ) internal returns (uint256 calculatedAmount) {
if (!executors[executor]) { if (!executors[executor]) {
revert Dispatcher__UnapprovedExecutor(); revert Dispatcher__UnapprovedExecutor(executor);
}
assembly {
tstore(0, executor)
} }
// slither-disable-next-line controlled-delegatecall,low-level-calls,calls-loop // slither-disable-next-line controlled-delegatecall,low-level-calls,calls-loop
@@ -80,11 +84,18 @@ contract Dispatcher {
calculatedAmount = abi.decode(result, (uint256)); calculatedAmount = abi.decode(result, (uint256));
} }
function _handleCallback(bytes calldata data) internal { // slither-disable-next-line assembly
address executor = address(uint160(bytes20(data[data.length - 20:]))); function _handleCallback(bytes calldata data)
internal
returns (bytes memory)
{
address executor;
assembly {
executor := tload(0)
}
if (!executors[executor]) { if (!executors[executor]) {
revert Dispatcher__UnapprovedExecutor(); revert Dispatcher__UnapprovedExecutor(executor);
} }
// slither-disable-next-line controlled-delegatecall,low-level-calls // slither-disable-next-line controlled-delegatecall,low-level-calls
@@ -101,5 +112,14 @@ contract Dispatcher {
) )
); );
} }
// to prevent multiple callbacks
assembly {
tstore(0, 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__AddressZero();
error TychoRouter__EmptySwaps(); error TychoRouter__EmptySwaps();
error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount); error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount);
error TychoRouter__AmountInDiffersFromConsumed( error TychoRouter__AmountOutNotFullyReceived(
uint256 amountIn, uint256 amountConsumed uint256 amountIn, uint256 amountConsumed
); );
error TychoRouter__MessageValueMismatch(uint256 value, uint256 amount); 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 //keccak256("NAME_OF_ROLE") : save gas on deployment
bytes32 public constant EXECUTOR_SETTER_ROLE = bytes32 public constant EXECUTOR_SETTER_ROLE =
0x6a1dd52dcad5bd732e45b6af4e7344fa284e2d7d4b23b5b09cb55d36b0685c87; 0x6a1dd52dcad5bd732e45b6af4e7344fa284e2d7d4b23b5b09cb55d36b0685c87;
bytes32 public constant FEE_SETTER_ROLE =
0xe6ad9a47fbda1dc18de1eb5eeb7d935e5e81b4748f3cfc61e233e64f88182060;
bytes32 public constant PAUSER_ROLE = bytes32 public constant PAUSER_ROLE =
0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a; 0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a;
bytes32 public constant UNPAUSER_ROLE = bytes32 public constant UNPAUSER_ROLE =
@@ -85,19 +83,9 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
bytes32 public constant FUND_RESCUER_ROLE = bytes32 public constant FUND_RESCUER_ROLE =
0x912e45d663a6f4cc1d0491d8f046e06c616f40352565ea1cdb86a0e1aaefa41b; 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( event Withdrawal(
address indexed token, uint256 amount, address indexed receiver 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) { constructor(address _permit2, address weth) {
if (_permit2 == address(0) || weth == address(0)) { 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. * @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 * 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 * @dev
* - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token. * - 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. * - 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. * - 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. * - 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 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 receiver The address to receive the output tokens.
* @param swaps Encoded swap graph data containing details of each swap. * @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, uint256 amountIn,
address tokenIn, address tokenIn,
address tokenOut, address tokenOut,
@@ -143,12 +130,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
address receiver, address receiver,
bytes calldata swaps bytes calldata swaps
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) { ) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
if (address(tokenIn) != address(0)) { return _splitSwapChecked(
IERC20(tokenIn).safeTransferFrom(
msg.sender, address(this), amountIn
);
}
return _swapChecked(
amountIn, amountIn,
tokenIn, tokenIn,
tokenOut, 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. * - 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. * - 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. * - 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. * - 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 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 signature A valid signature authorizing the Permit2 approval. Ignored if `wrapEth` is true.
* @param swaps Encoded swap graph data containing details of each swap. * @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, uint256 amountIn,
address tokenIn, address tokenIn,
address tokenOut, address tokenOut,
@@ -201,18 +182,12 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
bytes calldata signature, bytes calldata signature,
bytes calldata swaps bytes calldata swaps
) external payable whenNotPaused nonReentrant returns (uint256 amountOut) { ) 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)) { if (tokenIn != address(0)) {
permit2.permit(msg.sender, permitSingle, signature); permit2.permit(msg.sender, permitSingle, signature);
permit2.transferFrom(
msg.sender,
address(this),
uint160(amountIn),
permitSingle.details.token
);
} }
return _swapChecked( return _splitSwapChecked(
amountIn, amountIn,
tokenIn, tokenIn,
tokenOut, 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 This function centralizes the swap execution logic.
* @notice For detailed documentation on parameters and behavior, see the documentation for * @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, uint256 amountIn,
address tokenIn, address tokenIn,
address tokenOut, address tokenOut,
@@ -257,28 +424,8 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
tokenIn = address(_weth); tokenIn = address(_weth);
} }
uint256 initialBalance = tokenIn == address(0) uint256 initialBalanceTokenOut = _balanceOf(tokenOut, receiver);
? address(this).balance amountOut = _splitSwap(amountIn, nTokens, swaps);
: 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);
}
if (amountOut < minAmountOut) { if (amountOut < minAmountOut) {
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut); revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
@@ -286,11 +433,128 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
if (unwrapEth) { if (unwrapEth) {
_unwrapETH(amountOut); _unwrapETH(amountOut);
}
if (tokenOut == address(0)) {
Address.sendValue(payable(receiver), amountOut); 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 = _callExecutor(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. * @return The total amount of the buy token obtained after all swaps have been executed.
*/ */
function _swap(uint256 amountIn, uint256 nTokens, bytes calldata swaps_) function _splitSwap(
internal uint256 amountIn,
returns (uint256) uint256 nTokens,
{ bytes calldata swaps_
) internal returns (uint256) {
if (swaps_.length == 0) { if (swaps_.length == 0) {
revert TychoRouter__EmptySwaps(); revert TychoRouter__EmptySwaps();
} }
@@ -330,6 +595,8 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
uint8 tokenInIndex = 0; uint8 tokenInIndex = 0;
uint8 tokenOutIndex = 0; uint8 tokenOutIndex = 0;
uint24 split; uint24 split;
address executor;
bytes calldata protocolData;
bytes calldata swapData; bytes calldata swapData;
uint256[] memory remainingAmounts = new uint256[](nTokens); uint256[] memory remainingAmounts = new uint256[](nTokens);
@@ -340,17 +607,16 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
while (swaps_.length > 0) { while (swaps_.length > 0) {
(swapData, swaps_) = swaps_.next(); (swapData, swaps_) = swaps_.next();
tokenInIndex = swapData.tokenInIndex();
tokenOutIndex = swapData.tokenOutIndex(); (tokenInIndex, tokenOutIndex, split, executor, protocolData) =
split = swapData.splitPercentage(); swapData.decodeSplitSwap();
currentAmountIn = split > 0 currentAmountIn = split > 0
? (amounts[tokenInIndex] * split) / 0xffffff ? (amounts[tokenInIndex] * split) / 0xffffff
: remainingAmounts[tokenInIndex]; : remainingAmounts[tokenInIndex];
currentAmountOut = _callExecutor( currentAmountOut =
swapData.executor(), currentAmountIn, swapData.protocolData() _callExecutor(executor, currentAmountIn, protocolData);
);
// Checks if the output token is the same as the input token // Checks if the output token is the same as the input token
if (tokenOutIndex == 0) { if (tokenOutIndex == 0) {
cyclicSwapAmountOut += currentAmountOut; cyclicSwapAmountOut += currentAmountOut;
@@ -363,11 +629,41 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
return tokenOutIndex == 0 ? cyclicSwapAmountOut : amounts[tokenOutIndex]; 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 =
_callExecutor(executor, calculatedAmount, protocolData);
}
}
/** /**
* @dev We use the fallback function to allow flexibility on callback. * @dev We use the fallback function to allow flexibility on callback.
*/ */
fallback() external { fallback() external {
_handleCallback(msg.data); bytes memory result = _handleCallback(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); _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. * @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); 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. * @dev Called by UniswapV4 pool manager after achieving unlock state.
*/ */
@@ -549,47 +786,16 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
returns (bytes memory) returns (bytes memory)
{ {
if (data.length < 24) revert TychoRouter__InvalidDataLength(); if (data.length < 24) revert TychoRouter__InvalidDataLength();
_handleCallback(data); bytes memory result = _handleCallback(data);
return ""; return result;
} }
function locked(uint256) external { function _balanceOf(address token, address owner)
address executor = address(0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D); internal
view
// slither-disable-next-line controlled-delegatecall,low-level-calls returns (uint256)
(bool success, bytes memory result) = executor.delegatecall(msg.data); {
return
if (!success) { token == address(0) ? owner.balance : IERC20(token).balanceOf(owner);
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")
)
);
}
} }
} }

View File

@@ -10,14 +10,17 @@ import {
import {IAsset} from "@balancer-labs/v2-interfaces/contracts/vault/IAsset.sol"; import {IAsset} from "@balancer-labs/v2-interfaces/contracts/vault/IAsset.sol";
// slither-disable-next-line solc-version // slither-disable-next-line solc-version
import {IVault} from "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol"; import {IVault} from "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol";
import {TokenTransfer} from "./TokenTransfer.sol";
error BalancerV2Executor__InvalidDataLength(); error BalancerV2Executor__InvalidDataLength();
contract BalancerV2Executor is IExecutor { contract BalancerV2Executor is IExecutor, TokenTransfer {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
address private constant VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; address private constant VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
constructor(address _permit2) TokenTransfer(_permit2) {}
// slither-disable-next-line locked-ether // slither-disable-next-line locked-ether
function swap(uint256 givenAmount, bytes calldata data) function swap(uint256 givenAmount, bytes calldata data)
external external
@@ -29,9 +32,20 @@ contract BalancerV2Executor is IExecutor {
IERC20 tokenOut, IERC20 tokenOut,
bytes32 poolId, bytes32 poolId,
address receiver, address receiver,
bool needsApproval bool needsApproval,
TransferType transferType
) = _decodeData(data); ) = _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) { if (needsApproval) {
// slither-disable-next-line unused-return // slither-disable-next-line unused-return
tokenIn.forceApprove(VAULT, type(uint256).max); tokenIn.forceApprove(VAULT, type(uint256).max);
@@ -67,10 +81,11 @@ contract BalancerV2Executor is IExecutor {
IERC20 tokenOut, IERC20 tokenOut,
bytes32 poolId, bytes32 poolId,
address receiver, address receiver,
bool needsApproval bool needsApproval,
TransferType transferType
) )
{ {
if (data.length != 93) { if (data.length != 94) {
revert BalancerV2Executor__InvalidDataLength(); revert BalancerV2Executor__InvalidDataLength();
} }
@@ -79,5 +94,6 @@ contract BalancerV2Executor is IExecutor {
poolId = bytes32(data[40:72]); poolId = bytes32(data[40:72]);
receiver = address(bytes20(data[72:92])); receiver = address(bytes20(data[72:92]));
needsApproval = uint8(data[92]) > 0; 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 "@interfaces/IExecutor.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./TokenTransfer.sol";
import "@openzeppelin/contracts/utils/Address.sol";
error CurveExecutor__AddressZero(); error CurveExecutor__AddressZero();
error CurveExecutor__InvalidDataLength();
interface CryptoPool { interface CryptoPool {
// slither-disable-next-line naming-convention // slither-disable-next-line naming-convention
@@ -32,12 +35,14 @@ interface CryptoPoolETH {
// slither-disable-end naming-convention // slither-disable-end naming-convention
} }
contract CurveExecutor is IExecutor { contract CurveExecutor is IExecutor, TokenTransfer {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
address public immutable nativeToken; address public immutable nativeToken;
constructor(address _nativeToken) { constructor(address _nativeToken, address _permit2)
TokenTransfer(_permit2)
{
if (_nativeToken == address(0)) { if (_nativeToken == address(0)) {
revert CurveExecutor__AddressZero(); revert CurveExecutor__AddressZero();
} }
@@ -50,6 +55,8 @@ contract CurveExecutor is IExecutor {
payable payable
returns (uint256) returns (uint256)
{ {
if (data.length != 85) revert CurveExecutor__InvalidDataLength();
( (
address tokenIn, address tokenIn,
address tokenOut, address tokenOut,
@@ -57,9 +64,21 @@ contract CurveExecutor is IExecutor {
uint8 poolType, uint8 poolType,
int128 i, int128 i,
int128 j, int128 j,
bool tokenApprovalNeeded bool tokenApprovalNeeded,
TransferType transferType,
address receiver
) = _decodeData(data); ) = _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) { if (tokenApprovalNeeded && tokenIn != nativeToken) {
// slither-disable-next-line unused-return // slither-disable-next-line unused-return
IERC20(tokenIn).forceApprove(address(pool), type(uint256).max); IERC20(tokenIn).forceApprove(address(pool), type(uint256).max);
@@ -92,7 +111,16 @@ contract CurveExecutor is IExecutor {
} }
uint256 balanceAfter = _balanceOf(tokenOut); 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) function _decodeData(bytes calldata data)
@@ -105,7 +133,9 @@ contract CurveExecutor is IExecutor {
uint8 poolType, uint8 poolType,
int128 i, int128 i,
int128 j, int128 j,
bool tokenApprovalNeeded bool tokenApprovalNeeded,
TransferType transferType,
address receiver
) )
{ {
tokenIn = address(bytes20(data[0:20])); tokenIn = address(bytes20(data[0:20]));
@@ -115,6 +145,8 @@ contract CurveExecutor is IExecutor {
i = int128(uint128(uint8(data[61]))); i = int128(uint128(uint8(data[61])));
j = int128(uint128(uint8(data[62]))); j = int128(uint128(uint8(data[62])));
tokenApprovalNeeded = data[63] != 0; tokenApprovalNeeded = data[63] != 0;
transferType = TransferType(uint8(data[64]));
receiver = address(bytes20(data[65:85]));
} }
receive() external payable { receive() external payable {

View File

@@ -3,6 +3,7 @@ pragma solidity ^0.8.26;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IExecutor} from "@interfaces/IExecutor.sol"; import {IExecutor} from "@interfaces/IExecutor.sol";
import {ICallback} from "@interfaces/ICallback.sol";
import {ICore} from "@ekubo/interfaces/ICore.sol"; import {ICore} from "@ekubo/interfaces/ICore.sol";
import {ILocker, IPayer} from "@ekubo/interfaces/IFlashAccountant.sol"; import {ILocker, IPayer} from "@ekubo/interfaces/IFlashAccountant.sol";
import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.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 {LibBytes} from "@solady/utils/LibBytes.sol";
import {Config, EkuboPoolKey} from "@ekubo/types/poolKey.sol"; import {Config, EkuboPoolKey} from "@ekubo/types/poolKey.sol";
import {MAX_SQRT_RATIO, MIN_SQRT_RATIO} from "@ekubo/types/sqrtRatio.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__InvalidDataLength();
error EkuboExecutor__CoreOnly(); error EkuboExecutor__CoreOnly();
error EkuboExecutor__UnknownCallback(); error EkuboExecutor__UnknownCallback();
ICore immutable core; ICore immutable core;
uint256 constant POOL_DATA_OFFSET = 92; uint256 constant POOL_DATA_OFFSET = 77;
uint256 constant HOP_BYTE_LEN = 52; 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); core = ICore(_core);
} }
@@ -30,67 +41,57 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer {
payable payable
returns (uint256 calculatedAmount) 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 // amountIn must be at most type(int128).MAX
calculatedAmount = calculatedAmount = uint256(
uint256(_lock(bytes.concat(bytes16(uint128(amountIn)), data))); _lock(
bytes.concat(
bytes16(uint128(amountIn)), bytes20(msg.sender), data
)
)
);
} }
function locked(uint256) external coreOnly { function handleCallback(bytes calldata raw)
int128 nextAmountIn = int128(uint128(bytes16(msg.data[36:52]))); external
uint128 tokenInDebtAmount = uint128(nextAmountIn); returns (bytes memory)
{
verifyCallback(raw);
address receiver = address(bytes20(msg.data[52:72])); // Without selector and locker id
address tokenIn = address(bytes20(msg.data[72:POOL_DATA_OFFSET])); bytes calldata stripped = raw[36:];
address nextTokenIn = tokenIn; bytes4 selector = bytes4(raw[:4]);
uint256 hopsLength = (msg.data.length - POOL_DATA_OFFSET) / HOP_BYTE_LEN; bytes memory result = "";
if (selector == LOCKED_SELECTOR) {
uint256 offset = POOL_DATA_OFFSET; int128 calculatedAmount = _locked(stripped);
result = abi.encodePacked(calculatedAmount);
for (uint256 i = 0; i < hopsLength; i++) { } else if (selector == PAY_CALLBACK_SELECTOR) {
address nextTokenOut = _payCallback(stripped);
address(bytes20(LibBytes.loadCalldata(msg.data, offset))); } else {
Config poolConfig = revert EkuboExecutor__UnknownCallback();
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;
} }
_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 // slither-disable-next-line assembly
assembly ("memory-safe") { assembly ("memory-safe") {
mstore(0, nextAmountIn) mstore(0, calculatedAmount)
return(0x10, 16) return(0x10, 16)
} }
} }
function payCallback(uint256, address token) external coreOnly { function payCallback(uint256, address /*token*/ ) external coreOnly {
uint128 amount = uint128(bytes16(msg.data[68:84])); // Without selector and locker id
_payCallback(msg.data[36:]);
SafeTransferLib.safeTransfer(token, address(core), amount);
} }
function _lock(bytes memory data) 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); address target = address(core);
if (token == NATIVE_TOKEN_ADDRESS) { if (token == NATIVE_TOKEN_ADDRESS) {
@@ -134,9 +186,11 @@ contract EkuboExecutor is IExecutor, ILocker, IPayer {
mstore(free, shl(224, 0x0c11dedd)) mstore(free, shl(224, 0x0c11dedd))
mstore(add(free, 4), token) mstore(add(free, 4), token)
mstore(add(free, 36), shl(128, amount)) 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 // 4 (selector) + 32 (token) + 16 (amount) + 20 (sender) + 1 (transferType) = 73
if iszero(call(gas(), target, 0, free, 52, 0, 0)) { if iszero(call(gas(), target, 0, free, 73, 0, 0)) {
returndatacopy(0, 0, returndatasize()) returndatacopy(0, 0, returndatasize())
revert(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 // To receive withdrawals from Core
receive() external payable {} 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 "@interfaces/IExecutor.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@uniswap-v2/contracts/interfaces/IUniswapV2Pair.sol"; import "@uniswap-v2/contracts/interfaces/IUniswapV2Pair.sol";
import "./TokenTransfer.sol";
error UniswapV2Executor__InvalidDataLength(); error UniswapV2Executor__InvalidDataLength();
error UniswapV2Executor__InvalidTarget(); error UniswapV2Executor__InvalidTarget();
error UniswapV2Executor__InvalidFactory(); error UniswapV2Executor__InvalidFactory();
error UniswapV2Executor__InvalidInitCode(); error UniswapV2Executor__InvalidInitCode();
error UniswapV2Executor__InvalidFee();
contract UniswapV2Executor is IExecutor { contract UniswapV2Executor is IExecutor, TokenTransfer {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
address public immutable factory; address public immutable factory;
bytes32 public immutable initCode; bytes32 public immutable initCode;
address private immutable self; 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)) { if (_factory == address(0)) {
revert UniswapV2Executor__InvalidFactory(); revert UniswapV2Executor__InvalidFactory();
} }
@@ -26,6 +34,10 @@ contract UniswapV2Executor is IExecutor {
} }
factory = _factory; factory = _factory;
initCode = _initCode; initCode = _initCode;
if (_feeBps > 30) {
revert UniswapV2Executor__InvalidFee();
}
feeBps = _feeBps;
self = address(this); self = address(this);
} }
@@ -35,17 +47,21 @@ contract UniswapV2Executor is IExecutor {
payable payable
returns (uint256 calculatedAmount) returns (uint256 calculatedAmount)
{ {
IERC20 tokenIn;
address target; address target;
address receiver; address receiver;
bool zeroForOne; bool zeroForOne;
IERC20 tokenIn; TransferType transferType;
(tokenIn, target, receiver, zeroForOne) = _decodeData(data); (tokenIn, target, receiver, zeroForOne, transferType) =
_decodeData(data);
_verifyPairAddress(target); _verifyPairAddress(target);
calculatedAmount = _getAmountOut(target, givenAmount, zeroForOne); calculatedAmount = _getAmountOut(target, givenAmount, zeroForOne);
tokenIn.safeTransfer(target, givenAmount); _transfer(
address(tokenIn), msg.sender, target, givenAmount, transferType
);
IUniswapV2Pair pool = IUniswapV2Pair(target); IUniswapV2Pair pool = IUniswapV2Pair(target);
if (zeroForOne) { if (zeroForOne) {
@@ -62,16 +78,18 @@ contract UniswapV2Executor is IExecutor {
IERC20 inToken, IERC20 inToken,
address target, address target,
address receiver, address receiver,
bool zeroForOne bool zeroForOne,
TransferType transferType
) )
{ {
if (data.length != 61) { if (data.length != 62) {
revert UniswapV2Executor__InvalidDataLength(); revert UniswapV2Executor__InvalidDataLength();
} }
inToken = IERC20(address(bytes20(data[0:20]))); inToken = IERC20(address(bytes20(data[0:20])));
target = address(bytes20(data[20:40])); target = address(bytes20(data[20:40]));
receiver = address(bytes20(data[40:60])); receiver = address(bytes20(data[40:60]));
zeroForOne = uint8(data[60]) > 0; zeroForOne = uint8(data[60]) > 0;
transferType = TransferType(uint8(data[61]));
} }
function _getAmountOut(address target, uint256 amountIn, bool zeroForOne) function _getAmountOut(address target, uint256 amountIn, bool zeroForOne)
@@ -91,9 +109,9 @@ contract UniswapV2Executor is IExecutor {
} }
require(reserveIn > 0 && reserveOut > 0, "L"); require(reserveIn > 0 && reserveOut > 0, "L");
uint256 amountInWithFee = amountIn * 997; uint256 amountInWithFee = amountIn * (10000 - feeBps);
uint256 numerator = amountInWithFee * uint256(reserveOut); uint256 numerator = amountInWithFee * uint256(reserveOut);
uint256 denominator = (uint256(reserveIn) * 1000) + amountInWithFee; uint256 denominator = (uint256(reserveIn) * 10000) + amountInWithFee;
amount = numerator / denominator; amount = numerator / denominator;
} }

View File

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

View File

@@ -2,27 +2,48 @@
pragma solidity ^0.8.26; pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol"; import "@interfaces/IExecutor.sol";
import {ICallback} from "@interfaces/ICallback.sol";
import {TokenTransfer} from "./TokenTransfer.sol";
import { import {
IERC20, IERC20,
SafeERC20 SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import { import {
Currency, CurrencyLibrary Currency, CurrencyLibrary
} from "@uniswap/v4-core/src/types/Currency.sol"; } from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.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 {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 {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__InvalidDataLength();
error UniswapV4Executor__NotPoolManager();
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 SafeERC20 for IERC20;
using CurrencyLibrary for Currency; using CurrencyLibrary for Currency;
using SafeCast for *;
using TransientStateLibrary for IPoolManager;
IPoolManager public immutable poolManager;
struct UniswapV4Pool { struct UniswapV4Pool {
address intermediaryToken; address intermediaryToken;
@@ -30,7 +51,21 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
int24 tickSpacing; int24 tickSpacing;
} }
constructor(IPoolManager _poolManager) V4Router(_poolManager) {} constructor(IPoolManager _poolManager, address _permit2)
TokenTransfer(_permit2)
{
poolManager = _poolManager;
}
/**
* @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) function swap(uint256 amountIn, bytes calldata data)
external external
@@ -41,10 +76,10 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
address tokenIn, address tokenIn,
address tokenOut, address tokenOut,
bool zeroForOne, bool zeroForOne,
address callbackExecutor, TransferType transferType,
address receiver,
UniswapV4Executor.UniswapV4Pool[] memory pools UniswapV4Executor.UniswapV4Pool[] memory pools
) = _decodeData(data); ) = _decodeData(data);
bytes memory swapData; bytes memory swapData;
if (pools.length == 1) { if (pools.length == 1) {
PoolKey memory key = PoolKey({ PoolKey memory key = PoolKey({
@@ -54,26 +89,16 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
tickSpacing: pools[0].tickSpacing, tickSpacing: pools[0].tickSpacing,
hooks: IHooks(address(0)) hooks: IHooks(address(0))
}); });
bytes memory actions = abi.encodePacked( swapData = abi.encodeWithSelector(
uint8(Actions.SWAP_EXACT_IN_SINGLE), this.swapExactInputSingle.selector,
uint8(Actions.SETTLE_ALL), key,
uint8(Actions.TAKE_ALL) 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 { } else {
PathKey[] memory path = new PathKey[](pools.length); PathKey[] memory path = new PathKey[](pools.length);
for (uint256 i = 0; i < pools.length; i++) { for (uint256 i = 0; i < pools.length; i++) {
@@ -86,51 +111,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); Currency currencyIn = Currency.wrap(tokenIn);
params[0] = abi.encode( swapData = abi.encodeWithSelector(
IV4Router.ExactInputParams({ this.swapExactInput.selector,
currencyIn: currencyIn, currencyIn,
path: path, path,
amountIn: uint128(amountIn), amountIn,
amountOutMinimum: uint128(0) 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) bytes memory result = poolManager.unlock(swapData);
? address(this).balance uint128 amountOut = abi.decode(result, (uint128));
: IERC20(tokenOut).balanceOf(address(this));
executeActions(fullData); return amountOut;
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);
} }
function _decodeData(bytes calldata data) function _decodeData(bytes calldata data)
@@ -140,22 +136,24 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
address tokenIn, address tokenIn,
address tokenOut, address tokenOut,
bool zeroForOne, bool zeroForOne,
address callbackExecutor, TransferType transferType,
address receiver,
UniswapV4Pool[] memory pools UniswapV4Pool[] memory pools
) )
{ {
if (data.length < 87) { if (data.length < 88) {
revert UniswapV4Executor__InvalidDataLength(); revert UniswapV4Executor__InvalidDataLength();
} }
tokenIn = address(bytes20(data[0:20])); tokenIn = address(bytes20(data[0:20]));
tokenOut = address(bytes20(data[20:40])); tokenOut = address(bytes20(data[20:40]));
zeroForOne = (data[40] != 0); 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); pools = new UniswapV4Pool[](poolsLength);
bytes memory poolsData = data[61:]; bytes memory poolsData = data[62:];
uint256 offset = 0; uint256 offset = 0;
for (uint256 i = 0; i < poolsLength; i++) { for (uint256 i = 0; i < poolsLength; i++) {
address intermediaryToken; address intermediaryToken;
@@ -173,6 +171,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) function handleCallback(bytes calldata data)
external external
returns (bytes memory) returns (bytes memory)
@@ -181,15 +182,256 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback {
return _unlockCallback(data); 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( * @notice Handles the unlock callback from the pool manager. This is used for swaps against the executor directly (bypassing the router).
address(poolManager), amount */
); 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.
* The executor address is needed to perform the call. If the router is being used, the executor address is in
* transient storage. If it is not, then address(this) should be used.
*/
function _unlockCallback(bytes calldata data)
internal
returns (bytes memory)
{
address executor;
// slither-disable-next-line assembly
assembly {
executor := tload(0)
}
if (executor == address(0)) {
executor = address(this);
}
// 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) = executor.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 ADMIN = makeAddr("admin"); //admin=us
address BOB = makeAddr("bob"); //bob=someone!=us address BOB = makeAddr("bob"); //bob=someone!=us
address FUND_RESCUER = makeAddr("fundRescuer"); address FUND_RESCUER = makeAddr("fundRescuer");
address FEE_SETTER = makeAddr("feeSetter");
address FEE_RECEIVER = makeAddr("feeReceiver");
address EXECUTOR_SETTER = makeAddr("executorSetter"); address EXECUTOR_SETTER = makeAddr("executorSetter");
address ALICE = 0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2; address ALICE = 0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2;
uint256 ALICE_PK = uint256 ALICE_PK =

View File

@@ -7,7 +7,45 @@ import "../lib/LibSwap.sol";
contract LibSwapTest is Test { contract LibSwapTest is Test {
using LibSwap for bytes; 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 tokenInIndex = 1;
uint8 tokenOutIndex = 2; uint8 tokenOutIndex = 2;
uint24 split = 3; uint24 split = 3;
@@ -17,20 +55,32 @@ contract LibSwapTest is Test {
bytes memory swap = abi.encodePacked( bytes memory swap = abi.encodePacked(
tokenInIndex, tokenOutIndex, split, executor, protocolData 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 // This is necessary so that the compiler accepts bytes as a LibSwap.sol for testing
function assertSwap( // This is because this function takes calldata as input
function assertSplitSwap(
bytes calldata swap, bytes calldata swap,
uint8 tokenInIndex, uint8 tokenInIndex,
uint8 tokenOutIndex, uint8 tokenOutIndex,
uint24 split, uint24 split,
address executor address executor,
bytes calldata protocolData
) public pure { ) public pure {
assert(swap.tokenInIndex() == tokenInIndex); (
assert(swap.tokenOutIndex() == tokenOutIndex); uint8 decodedTokenInIndex,
assert(swap.splitPercentage() == split); uint8 decodedTokenOutIndex,
assert(swap.executor() == executor); 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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "./TychoRouterTestSetup.sol";
contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup {
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);
// Encoded solution generated using `test_sequential_encoding_strategy_usv4`
(bool success,) = tychoRouterAddr.call(
hex"7c553846000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000005064ff624d54346285543f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000006828a8d900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000680122e10000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000412fb2c4e85c1b2236aef343641c10f81e4abfd675f520d86778cb9db16c9f500d11fe28b99285dd1bef082b9ccde3360a8077c57ece0775677fddfd5ff11b6e081c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008d008b0001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d23119330002cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c6982508145454ce325ddbe47a25d4ec3d23119330061a80001f400000000000000000000000000000000000000"
);
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);
// Encoded solution generated using `test_multi_protocol`
(bool success,) = tychoRouterAddr.call(
hex"51bcc7b600000000000000000000000000000000000000000000005150ae84a8cdf000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a2958f36da71a9200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000005150ae84a8cdf0000000000000000000000000000000000000000000000000000000000000682f990000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006808130800000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000414e46e936cfd0f718a485f27c233cb85a64ab162edca753cbc7c9d1bc393a688275dc15bd930e210af2e5dd8e8d8f90ec8438b821297c469d80712aadcff73b071c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021400525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501020072c7183455a4c133ae270771860664b6b7ec320bb1c02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599a6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e3ede3eca2a72b3aecc820e955b36f38437d01395010500691d1499e622d69689cdf9004d05ec547d650ff2112260fac5e5542a773aa44fbcfedf7c193bc2c599dac17f958d2ee523a2206206994597c13d831ec7d51a44d3fae010294c616388b506acda1bfaae4603010001053ede3eca2a72b3aecc820e955b36f38437d013950071a0cb889707d426a7a386870a03bc70d1b0697598003ede3eca2a72b3aecc820e955b36f38437d01395dac17f958d2ee523a2206206994597c13d831ec7a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000001a36e2eb1c43200000032006cf62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c000000000000000000000000"
);
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);
// Encoded solution generated using `test_single_encoding_strategy_usv4_eth_in`
(bool success,) = tychoRouterAddr.call{value: 1 ether}(
hex"30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000007e0a55d4322a6e93c2379c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000682f92ce00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068080cd600000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041006930677d9715fb8c55f73546d3aaff4176ee1342b9b7ae34431a4356fc98a915f3103639d8e34cbaa591a3493e887dad6e816228200dee0a693408b4fa6fdc1b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006cf62849f9a0b5bf2913b396098f7c7019b51a820a00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330105cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc26982508145454ce325ddbe47a25d4ec3d23119330061a80001f40000000000000000000000000000000000000000"
);
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);
// Encoded solution generated using `test_single_encoding_strategy_usv4_eth_out`
(bool success,) = tychoRouterAddr.call(
hex"7c55384600000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f81490b4f29aade000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000000000000000000000000000000000006828a8d900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000680122e1000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041bf1373f3d3943e0865f8081b1569b4deb66b56b8690500c4c9f1c1f7e1299510720e3d4c92abf6ec75f0b14a87b92957fd43408562f26b8616857469f94012e21b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007300710001000000f62849f9a0b5bf2913b396098f7c7019b51a820aa0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000002cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000bb800003c00000000000000000000000000"
);
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);
// Encoded solution generated using `test_single_encoding_strategy_ekubo`
(bool success,) = address(tychoRouter).call{value: 1 ether}(
hex"20144a070000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000071a0cb889707d426a7a386870a03bc70d1b069759805cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000000000000000000000000000000000"
);
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);
// Encoded solution generated using `test_single_encoding_strategy_curve`
(bool success,) = tychoRouterAddr.call(
hex"20144a070000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000055c08ca52497e2f1534b59e2917bf524d4765257000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000691d1499e622d69689cdf9004d05ec547d650ff21155c08ca52497e2f1534b59e2917bf524d4765257c02aaa39b223fe8d0a0e5c4f27ead9083c756cc277146b0a1d08b6844376df6d9da99ba7f1b19e710201000103cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000"
);
assertTrue(success, "Call Failed");
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 2877855391767);
vm.stopPrank();
}
}

View File

@@ -0,0 +1,505 @@
// 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);
// Encoded solution generated using `test_sequential_swap_strategy_encoder`
(bool success,) = tychoRouterAddr.call(
hex"51bcc7b60000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000682714ab00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067ff8eb300000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000412fe66c22814eb271e37bb03303bae445eb96aa50fae9680a0ae685ee5795aebf1f5bb7718154c69680bcfc00cc9be525b2b021f57a1bddb4db622139acd425d41b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d940004375dff511095cc5a197a54140a24efef3a416000200525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20105000000000000000000000000000000000000000000000000"
);
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);
// Encoded solution generated using `test_sequential_swap_strategy_encoder_no_permit2`
(bool success,) = tychoRouterAddr.call(
hex"e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000a800525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d940004375dff511095cc5a197a54140a24efef3a416000100525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20105000000000000000000000000000000000000000000000000"
);
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);
// Encoded solution generated using `test_sequential_strategy_cyclic_swap`
(bool success,) = tychoRouterAddr.call(
hex"51bcc7b60000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ec8f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000682f96a300000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000680810ab00000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000415de1a1f5644d780aa3e22af583e87639ff7d519518576da5b10c15748d75d7f64b9d4fc2439869fc226ca4a8b69c6cc4b284427b0d5d73c72e54f115cdf2bbca1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f5640010200692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000000000000000000000000"
);
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);
// Encoded solution generated using `test_uniswap_v3_uniswap_v2`
(bool success,) = tychoRouterAddr.call(
hex"e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000bf00692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb8004375dff511095cc5a197a54140a24efef3a416cbcdf9626bc03e24f779434178a73a0b4bad62ed000100525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2010500"
);
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);
// Encoded solution generated using `test_uniswap_v3_uniswap_v3`
(bool success,) = tychoRouterAddr.call(
hex"e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb83ede3eca2a72b3aecc820e955b36f38437d01395cbcdf9626bc03e24f779434178a73a0b4bad62ed000100692e234dae75c793f67a35089c9d99245e1c58470b2260fac5e5542a773aa44fbcfedf7c193bc2c599a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc299ac8ca7087fa4a2a1fb6357269965a2014abc35010000000000000000000000"
);
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);
// Encoded solution generated using `test_uniswap_v3_curve`
(bool success,) = tychoRouterAddr.call(
hex"e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000d600692e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599000bb83ede3eca2a72b3aecc820e955b36f38437d01395cbcdf9626bc03e24f779434178a73a0b4bad62ed000100691d1499e622d69689cdf9004d05ec547d650ff2112260fac5e5542a773aa44fbcfedf7c193bc2c599dac17f958d2ee523a2206206994597c13d831ec7d51a44d3fae010294c616388b506acda1bfaae460301000105cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000"
);
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);
// Encoded solution generated using `test_uniswap_v3_curve`
(bool success,) = tychoRouterAddr.call(
hex"e8a980d70000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c80072c7183455a4c133ae270771860664b6b7ec320bb1c02aaa39b223fe8d0a0e5c4f27ead9083c756cc22260fac5e5542a773aa44fbcfedf7c193bc2c599a6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e004375dff511095cc5a197a54140a24efef3a416010300525615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20105000000000000000000000000000000000000000000000000"
);
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,388 @@
// 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);
// Encoded solution generated using `test_single_swap_strategy_encoder_no_permit2`
(bool success,) = tychoRouterAddr.call(
hex"20144a070000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000058e7926ee858a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200010000000000000000000000000000"
);
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);
// Encoded solution generated using `test_single_swap_strategy_encoder`
(bool success,) = tychoRouterAddr.call(
hex"30ace1b10000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000006b56051582a970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000682f946a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068080e7200000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041c0ace69971589bd5136c309c83a06d60a7a54a49a2f1cecdf51cc5aecd4f7cce07a1b4a152d758fb6c3e4a73f8cf96ca3b3e8ab82b402733b7979a67021e99a51c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200020000000000000000000000000000"
);
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);
// Encoded solution generated using `test_single_swap_strategy_encoder_wrap`
(bool success,) = tychoRouterAddr.call{value: 1 ether}(
hex"30ace1b10000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000059fb7d3830e6fc064b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000682f965f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006808106700000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041cdef1c27d45a13adde80b5d93c8786741b69ddbe7684c4356f3dc7d4aa8029cb3cba0aac801787a7993ba0be72a6b459fa2a5c18a8e4938a0ccd3503d1be81841c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000"
);
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);
// Encoded solution generated using `test_single_swap_strategy_encoder_unwrap`
(bool success,) = tychoRouterAddr.call(
hex"30ace1b10000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be0000000000000000000000000000000000000000000000000000000000000682db45d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000068062e6500000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000041de45f1a73e8a22fc958af300f93cff06b49e74667bb29b810aed4254fef0dae6340ceb95265d81f5b158bcade2b5a2e3efa8bfa521a6466c0b1ce0bcfddc19d21c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139501020000000000000000000000000000"
);
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);
// Encoded solution generated using `test_single_swap_strategy_encoder_no_transfer_in`
(bool success,) = tychoRouterAddr.call(
hex"20144a070000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000058e7926ee858a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000"
);
vm.stopPrank();
uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 2018817438608734439722);
}
}

View File

@@ -0,0 +1,723 @@
// 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 testSplitSwapSingleUSV3Permit2() 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 = encodeSplitSwap(
uint8(0), uint8(1), uint24(0), address(usv3Executor), protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
tychoRouter.splitSwapPermit2(
amountIn,
WETH_ADDR,
DAI_ADDR,
expAmountOut - 1,
false,
false,
2,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
assertGe(finalBalance, expAmountOut);
vm.stopPrank();
}
function testEmptySwapsRevert() public {
uint256 amountIn = 10 ** 18;
bytes memory swaps = "";
vm.expectRevert(TychoRouter__EmptySwaps.selector);
tychoRouter.exposedSplitSwap(amountIn, 2, swaps);
}
function testSplitSwapSingleUSV4CallbackPermit2() 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 = encodeSplitSwap(
uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
tychoRouter.splitSwapPermit2(
amountIn,
USDE_ADDR,
USDT_ADDR,
99943850,
false,
false,
2,
ALICE,
permitSingle,
signature,
pleEncode(swaps)
);
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 = encodeSplitSwap(
uint8(0), uint8(1), uint24(0), address(usv4Executor), protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps));
assertEq(IERC20(WBTC_ADDR).balanceOf(ALICE), 118281);
}
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);
// Encoded solution generated using `test_split_swap_strategy_encoder`
(bool success,) = tychoRouterAddr.call(
hex"7c5538460000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000018f61ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006826193a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067fe9342000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041d137d0776bc16ff9c49bfd3e96103ceb6926654f314489cafcf5a64ab7a9c4f2061ed5ffdef67c33c3c5b78036d28d9eb73da156a0e68d8740235be50e88a3481b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164005700028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950002005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d013950002005702030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d5cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20100005701030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a416cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2010000000000000000000000000000000000000000000000000000000000"
);
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);
// Encoded solution generated using `test_split_input_cyclic_swap`
(bool success,) = tychoRouterAddr.call(
hex"7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005ef619b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000006826193a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067fe934200000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004194fc497ac440b520981d23b4713425da21dc1c801e657d218a917b5c51339a660b9a5fe0a346cb0aacc0d67ebf03f8fa3ec9fade437ef1b08ea837b2442931b61b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139006e00019999992e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f56400102006e00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d80102005701000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b4e16d0168e52d35cacd2c6185b44281ec28c9dccd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000"
);
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);
// Encoded solution generated using `test_split_output_cyclic_swap`
(bool success,) = tychoRouterAddr.call(
hex"7c5538460000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005e703f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000682f963200000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000006808103a0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000413c46b497d6f01110f05257114b978d2fd2d493ac8dae2c7892bbfa593fc5d062384590828248348fe87b234c3417e463f12d4732e287a56882841a92bc41e9121b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000139005700010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48b4e16d0168e52d35cacd2c6185b44281ec28c9dc3ede3eca2a72b3aecc820e955b36f38437d013950102006e01009999992e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc288e6a0c2ddd26feeb64f039a2c41296fcb3f56400000006e01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb8cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc28ad599c3a0ff1de082011efddc58f1908eb6e6d8000000000000000000"
);
assertTrue(success, "Call Failed");
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), 99444510);
vm.stopPrank();
}
}

View File

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

View File

@@ -6,6 +6,8 @@ import {Test} from "../../lib/forge-std/src/Test.sol";
import {Constants} from "../Constants.sol"; import {Constants} from "../Constants.sol";
contract BalancerV2ExecutorExposed is BalancerV2Executor { contract BalancerV2ExecutorExposed is BalancerV2Executor {
constructor(address _permit2) BalancerV2Executor(_permit2) {}
function decodeParams(bytes calldata data) function decodeParams(bytes calldata data)
external external
pure pure
@@ -14,18 +16,15 @@ contract BalancerV2ExecutorExposed is BalancerV2Executor {
IERC20 tokenOut, IERC20 tokenOut,
bytes32 poolId, bytes32 poolId,
address receiver, address receiver,
bool needsApproval bool needsApproval,
TransferType transferType
) )
{ {
return _decodeData(data); return _decodeData(data);
} }
} }
contract BalancerV2ExecutorTest is contract BalancerV2ExecutorTest is Test, Constants {
BalancerV2ExecutorExposed,
Test,
Constants
{
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
BalancerV2ExecutorExposed balancerV2Exposed; BalancerV2ExecutorExposed balancerV2Exposed;
@@ -37,12 +36,17 @@ contract BalancerV2ExecutorTest is
function setUp() public { function setUp() public {
uint256 forkBlock = 17323404; uint256 forkBlock = 17323404;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
balancerV2Exposed = new BalancerV2ExecutorExposed(); balancerV2Exposed = new BalancerV2ExecutorExposed(PERMIT2_ADDRESS);
} }
function testDecodeParams() public view { function testDecodeParams() public view {
bytes memory params = abi.encodePacked( 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, IERC20 tokenOut,
bytes32 poolId, bytes32 poolId,
address receiver, address receiver,
bool needsApproval bool needsApproval,
TokenTransfer.TransferType transferType
) = balancerV2Exposed.decodeParams(params); ) = balancerV2Exposed.decodeParams(params);
assertEq(address(tokenIn), WETH_ADDR); assertEq(address(tokenIn), WETH_ADDR);
@@ -58,6 +63,7 @@ contract BalancerV2ExecutorTest is
assertEq(poolId, WETH_BAL_POOL_ID); assertEq(poolId, WETH_BAL_POOL_ID);
assertEq(receiver, address(2)); assertEq(receiver, address(2));
assertEq(needsApproval, true); assertEq(needsApproval, true);
assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE));
} }
function testDecodeParamsInvalidDataLength() public { function testDecodeParamsInvalidDataLength() public {
@@ -70,8 +76,14 @@ contract BalancerV2ExecutorTest is
function testSwap() public { function testSwap() public {
uint256 amountIn = 10 ** 18; uint256 amountIn = 10 ** 18;
bytes memory protocolData = bytes memory protocolData = abi.encodePacked(
abi.encodePacked(WETH_ADDR, BAL_ADDR, WETH_BAL_POOL_ID, BOB, true); WETH_ADDR,
BAL_ADDR,
WETH_BAL_POOL_ID,
BOB,
true,
TokenTransfer.TransferType.NONE
);
deal(WETH_ADDR, address(balancerV2Exposed), amountIn); deal(WETH_ADDR, address(balancerV2Exposed), amountIn);
uint256 balanceBefore = BAL.balanceOf(BOB); uint256 balanceBefore = BAL.balanceOf(BOB);
@@ -86,14 +98,15 @@ contract BalancerV2ExecutorTest is
function testDecodeIntegration() public view { function testDecodeIntegration() public view {
// Generated by the SwapEncoder - test_encode_balancer_v2 // Generated by the SwapEncoder - test_encode_balancer_v2
bytes memory protocolData = bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01"; hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0105";
( (
IERC20 tokenIn, IERC20 tokenIn,
IERC20 tokenOut, IERC20 tokenOut,
bytes32 poolId, bytes32 poolId,
address receiver, address receiver,
bool needsApproval bool needsApproval,
TokenTransfer.TransferType transferType
) = balancerV2Exposed.decodeParams(protocolData); ) = balancerV2Exposed.decodeParams(protocolData);
assertEq(address(tokenIn), WETH_ADDR); assertEq(address(tokenIn), WETH_ADDR);
@@ -101,12 +114,13 @@ contract BalancerV2ExecutorTest is
assertEq(poolId, WETH_BAL_POOL_ID); assertEq(poolId, WETH_BAL_POOL_ID);
assertEq(receiver, BOB); assertEq(receiver, BOB);
assertEq(needsApproval, true); assertEq(needsApproval, true);
assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE));
} }
function testSwapIntegration() public { function testSwapIntegration() public {
// Generated by the SwapEncoder - test_encode_balancer_v2 // Generated by the SwapEncoder - test_encode_balancer_v2
bytes memory protocolData = bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01"; hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0105";
uint256 amountIn = 10 ** 18; uint256 amountIn = 10 ** 18;
deal(WETH_ADDR, address(balancerV2Exposed), amountIn); deal(WETH_ADDR, address(balancerV2Exposed), amountIn);

View File

@@ -22,7 +22,9 @@ interface MetaRegistry {
} }
contract CurveExecutorExposed is CurveExecutor { contract CurveExecutorExposed is CurveExecutor {
constructor(address _nativeToken) CurveExecutor(_nativeToken) {} constructor(address _nativeToken, address _permit2)
CurveExecutor(_nativeToken, _permit2)
{}
function decodeData(bytes calldata data) function decodeData(bytes calldata data)
external external
@@ -34,7 +36,9 @@ contract CurveExecutorExposed is CurveExecutor {
uint8 poolType, uint8 poolType,
int128 i, int128 i,
int128 j, int128 j,
bool tokenApprovalNeeded bool tokenApprovalNeeded,
TokenTransfer.TransferType transferType,
address receiver
) )
{ {
return _decodeData(data); return _decodeData(data);
@@ -50,7 +54,8 @@ contract CurveExecutorTest is Test, Constants {
function setUp() public { function setUp() public {
uint256 forkBlock = 22031795; uint256 forkBlock = 22031795;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); 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); metaRegistry = MetaRegistry(CURVE_META_REGISTRY);
} }
@@ -62,7 +67,9 @@ contract CurveExecutorTest is Test, Constants {
uint8(3), uint8(3),
uint8(2), uint8(2),
uint8(0), uint8(0),
true true,
TokenTransfer.TransferType.NONE,
ALICE
); );
( (
@@ -72,7 +79,9 @@ contract CurveExecutorTest is Test, Constants {
uint8 poolType, uint8 poolType,
int128 i, int128 i,
int128 j, int128 j,
bool tokenApprovalNeeded bool tokenApprovalNeeded,
TokenTransfer.TransferType transferType,
address receiver
) = curveExecutorExposed.decodeData(data); ) = curveExecutorExposed.decodeData(data);
assertEq(tokenIn, WETH_ADDR); assertEq(tokenIn, WETH_ADDR);
@@ -82,6 +91,8 @@ contract CurveExecutorTest is Test, Constants {
assertEq(i, 2); assertEq(i, 2);
assertEq(j, 0); assertEq(j, 0);
assertEq(tokenApprovalNeeded, true); assertEq(tokenApprovalNeeded, true);
assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE));
assertEq(receiver, ALICE);
} }
function testTriPool() public { function testTriPool() public {
@@ -89,15 +100,12 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 1 ether; uint256 amountIn = 1 ether;
deal(DAI_ADDR, address(curveExecutorExposed), amountIn); 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); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 999797); assertEq(amountOut, 999797);
assertEq( assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), amountOut);
IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
} }
function testStEthPool() public { function testStEthPool() public {
@@ -106,14 +114,14 @@ contract CurveExecutorTest is Test, Constants {
deal(address(curveExecutorExposed), amountIn); deal(address(curveExecutorExposed), amountIn);
bytes memory data = 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); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 1001072414418410897); assertEq(amountOut, 1001072414418410897);
assertEq( assertEq(
IERC20(STETH_ADDR).balanceOf(address(curveExecutorExposed)), IERC20(STETH_ADDR).balanceOf(ALICE),
amountOut 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; uint256 amountIn = 1 ether;
deal(WETH_ADDR, address(curveExecutorExposed), amountIn); 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); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 2279618); assertEq(amountOut, 2279618);
assertEq( assertEq(IERC20(WBTC_ADDR).balanceOf(ALICE), amountOut);
IERC20(WBTC_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
} }
function testSUSDPool() public { function testSUSDPool() public {
@@ -138,15 +144,12 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 100 * 10 ** 6; uint256 amountIn = 100 * 10 ** 6;
deal(USDC_ADDR, address(curveExecutorExposed), amountIn); 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); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 100488101605550214590); assertEq(amountOut, 100488101605550214590);
assertEq( assertEq(IERC20(SUSD_ADDR).balanceOf(ALICE), amountOut);
IERC20(SUSD_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
} }
function testFraxUsdcPool() public { function testFraxUsdcPool() public {
@@ -154,15 +157,13 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 1 ether; uint256 amountIn = 1 ether;
deal(FRAX_ADDR, address(curveExecutorExposed), amountIn); 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); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 998097); assertEq(amountOut, 998097);
assertEq( assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), amountOut);
IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
} }
function testUsdeUsdcPool() public { function testUsdeUsdcPool() public {
@@ -170,15 +171,13 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 100 * 10 ** 6; uint256 amountIn = 100 * 10 ** 6;
deal(USDC_ADDR, address(curveExecutorExposed), amountIn); 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); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 100064812138999986170); assertEq(amountOut, 100064812138999986170);
assertEq( assertEq(IERC20(USDE_ADDR).balanceOf(ALICE), amountOut);
IERC20(USDE_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
} }
function testDolaFraxPyusdPool() public { function testDolaFraxPyusdPool() public {
@@ -187,32 +186,27 @@ contract CurveExecutorTest is Test, Constants {
deal(DOLA_ADDR, address(curveExecutorExposed), amountIn); deal(DOLA_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = 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); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 99688992); assertEq(amountOut, 99688992);
assertEq( assertEq(IERC20(FRAXPYUSD_POOL).balanceOf(ALICE), amountOut);
IERC20(FRAXPYUSD_POOL).balanceOf(address(curveExecutorExposed)),
amountOut
);
} }
function testCryptoPoolWithETH() public { function testCryptoPoolWithETH() public {
// Swapping XYO -> ETH on a CryptoPool, deployed by factory 0xF18056Bbd320E96A48e3Fbf8bC061322531aac99 // Swapping XYO -> ETH on a CryptoPool, deployed by factory 0xF18056Bbd320E96A48e3Fbf8bC061322531aac99
uint256 amountIn = 1 ether; 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); deal(XYO_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = 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); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 6081816039338); assertEq(amountOut, 6081816039338);
assertEq( assertEq(ALICE.balance, initialBalance + amountOut);
address(curveExecutorExposed).balance, initialBalance + amountOut
);
} }
function testCryptoPool() public { function testCryptoPool() public {
@@ -220,15 +214,13 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 1000 ether; uint256 amountIn = 1000 ether;
deal(BSGG_ADDR, address(curveExecutorExposed), amountIn); 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); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 23429); assertEq(amountOut, 23429);
assertEq( assertEq(IERC20(USDT_ADDR).balanceOf(ALICE), amountOut);
IERC20(USDT_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
} }
function testTricryptoPool() public { function testTricryptoPool() public {
@@ -236,15 +228,13 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 1 ether; uint256 amountIn = 1 ether;
deal(WETH_ADDR, address(curveExecutorExposed), amountIn); 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); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 1861130974); assertEq(amountOut, 1861130974);
assertEq( assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), amountOut);
IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
} }
function testTwoCryptoPool() public { function testTwoCryptoPool() public {
@@ -252,15 +242,13 @@ contract CurveExecutorTest is Test, Constants {
uint256 amountIn = 1 ether; uint256 amountIn = 1 ether;
deal(UWU_ADDR, address(curveExecutorExposed), amountIn); 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); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 2873786684675); assertEq(amountOut, 2873786684675);
assertEq( assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), amountOut);
IERC20(WETH_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
} }
function testStableSwapPool() public { function testStableSwapPool() public {
@@ -269,15 +257,12 @@ contract CurveExecutorTest is Test, Constants {
deal(USDT_ADDR, address(curveExecutorExposed), amountIn); deal(USDT_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = bytes memory data =
_getData(USDT_ADDR, CRVUSD_ADDR, CRVUSD_USDT_POOL, 1); _getData(USDT_ADDR, CRVUSD_ADDR, CRVUSD_USDT_POOL, 1, ALICE);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 10436946786333182306400100); assertEq(amountOut, 10436946786333182306400100);
assertEq( assertEq(IERC20(CRVUSD_ADDR).balanceOf(ALICE), amountOut);
IERC20(CRVUSD_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
} }
function testMetaPool() public { function testMetaPool() public {
@@ -286,22 +271,20 @@ contract CurveExecutorTest is Test, Constants {
deal(WTAO_ADDR, address(curveExecutorExposed), amountIn); deal(WTAO_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = 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); uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 32797923610); assertEq(amountOut, 32797923610);
assertEq( assertEq(IERC20(WSTTAO_ADDR).balanceOf(ALICE), amountOut);
IERC20(WSTTAO_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
} }
function _getData( function _getData(
address tokenIn, address tokenIn,
address tokenOut, address tokenOut,
address pool, address pool,
uint8 poolType uint8 poolType,
address receiver
) internal view returns (bytes memory data) { ) internal view returns (bytes memory data) {
(int128 i, int128 j) = _getIndexes(tokenIn, tokenOut, pool); (int128 i, int128 j) = _getIndexes(tokenIn, tokenOut, pool);
data = abi.encodePacked( data = abi.encodePacked(
@@ -311,7 +294,9 @@ contract CurveExecutorTest is Test, Constants {
poolType, poolType,
uint8(uint256(uint128(i))), uint8(uint256(uint128(i))),
uint8(uint256(uint128(j))), uint8(uint256(uint128(j))),
true true,
TokenTransfer.TransferType.NONE,
receiver
); );
} }

View File

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

View File

@@ -2,13 +2,18 @@
pragma solidity ^0.8.26; pragma solidity ^0.8.26;
import "@src/executors/UniswapV2Executor.sol"; import "@src/executors/UniswapV2Executor.sol";
import "@src/executors/TokenTransfer.sol";
import {Test} from "../../lib/forge-std/src/Test.sol"; import {Test} from "../../lib/forge-std/src/Test.sol";
import {Constants} from "../Constants.sol"; import {Constants} from "../Constants.sol";
import {Permit2TestHelper} from "../Permit2TestHelper.sol";
contract UniswapV2ExecutorExposed is UniswapV2Executor { contract UniswapV2ExecutorExposed is UniswapV2Executor {
constructor(address _factory, bytes32 _initCode) constructor(
UniswapV2Executor(_factory, _initCode) address _factory,
{} bytes32 _initCode,
address _permit2,
uint256 _feeBps
) UniswapV2Executor(_factory, _initCode, _permit2, _feeBps) {}
function decodeParams(bytes calldata data) function decodeParams(bytes calldata data)
external external
@@ -17,7 +22,8 @@ contract UniswapV2ExecutorExposed is UniswapV2Executor {
IERC20 inToken, IERC20 inToken,
address target, address target,
address receiver, address receiver,
bool zeroForOne bool zeroForOne,
TransferType transferType
) )
{ {
return _decodeData(data); return _decodeData(data);
@@ -46,7 +52,7 @@ contract FakeUniswapV2Pool {
} }
} }
contract UniswapV2ExecutorTest is Test, Constants { contract UniswapV2ExecutorTest is Test, Constants, Permit2TestHelper {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
UniswapV2ExecutorExposed uniswapV2Exposed; UniswapV2ExecutorExposed uniswapV2Exposed;
@@ -54,32 +60,54 @@ contract UniswapV2ExecutorTest is Test, Constants {
UniswapV2ExecutorExposed pancakeswapV2Exposed; UniswapV2ExecutorExposed pancakeswapV2Exposed;
IERC20 WETH = IERC20(WETH_ADDR); IERC20 WETH = IERC20(WETH_ADDR);
IERC20 DAI = IERC20(DAI_ADDR); IERC20 DAI = IERC20(DAI_ADDR);
IAllowanceTransfer permit2;
function setUp() public { function setUp() public {
uint256 forkBlock = 17323404; uint256 forkBlock = 17323404;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
uniswapV2Exposed = new UniswapV2ExecutorExposed( 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( 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( 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 { function testDecodeParams() public view {
bytes memory params = bytes memory params = abi.encodePacked(
abi.encodePacked(WETH_ADDR, address(2), address(3), false); 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(address(tokenIn), WETH_ADDR);
assertEq(target, address(2)); assertEq(target, address(2));
assertEq(receiver, address(3)); assertEq(receiver, address(3));
assertEq(zeroForOne, false); assertEq(zeroForOne, false);
assertEq(
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL),
uint8(transferType)
);
} }
function testDecodeParamsInvalidDataLength() public { function testDecodeParamsInvalidDataLength() public {
@@ -126,12 +154,17 @@ contract UniswapV2ExecutorTest is Test, Constants {
assertGe(amountOut, 0); assertGe(amountOut, 0);
} }
function testSwap() public { function testSwapWithTransfer() public {
uint256 amountIn = 10 ** 18; uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891; uint256 amountOut = 1847751195973566072891;
bool zeroForOne = false; bool zeroForOne = false;
bytes memory protocolData = bytes memory protocolData = abi.encodePacked(
abi.encodePacked(WETH_ADDR, WETH_DAI_POOL, BOB, zeroForOne); WETH_ADDR,
WETH_DAI_POOL,
BOB,
zeroForOne,
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
);
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn); deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
uniswapV2Exposed.swap(amountIn, protocolData); uniswapV2Exposed.swap(amountIn, protocolData);
@@ -140,24 +173,104 @@ contract UniswapV2ExecutorTest is Test, Constants {
assertGe(finalBalance, amountOut); assertGe(finalBalance, amountOut);
} }
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)
);
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 { function testDecodeIntegration() public view {
// Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode // Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode
bytes memory protocolData = bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f5640000000000000000000000000000000000000000100"; hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f564000000000000000000000000000000000000000010000";
(IERC20 tokenIn, address target, address receiver, bool zeroForOne) = (
uniswapV2Exposed.decodeParams(protocolData); IERC20 tokenIn,
address target,
address receiver,
bool zeroForOne,
TokenTransfer.TransferType transferType
) = uniswapV2Exposed.decodeParams(protocolData);
assertEq(address(tokenIn), WETH_ADDR); assertEq(address(tokenIn), WETH_ADDR);
assertEq(target, 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640); assertEq(target, 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640);
assertEq(receiver, 0x0000000000000000000000000000000000000001); assertEq(receiver, 0x0000000000000000000000000000000000000001);
assertEq(zeroForOne, false); assertEq(zeroForOne, false);
// TRANSFER = 0
assertEq(0, uint8(transferType));
} }
function testSwapIntegration() public { function testSwapIntegration() public {
// Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode // Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode
bytes memory protocolData = bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb111d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e00"; hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb111d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000";
uint256 amountIn = 10 ** 18; uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891; uint256 amountOut = 1847751195973566072891;
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn); deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
@@ -171,8 +284,13 @@ contract UniswapV2ExecutorTest is Test, Constants {
uint256 amountIn = 10 ** 18; uint256 amountIn = 10 ** 18;
bool zeroForOne = false; bool zeroForOne = false;
address fakePool = address(new FakeUniswapV2Pool(WETH_ADDR, DAI_ADDR)); address fakePool = address(new FakeUniswapV2Pool(WETH_ADDR, DAI_ADDR));
bytes memory protocolData = bytes memory protocolData = abi.encodePacked(
abi.encodePacked(WETH_ADDR, fakePool, BOB, zeroForOne); WETH_ADDR,
fakePool,
BOB,
zeroForOne,
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
);
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn); deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
vm.expectRevert(UniswapV2Executor__InvalidTarget.selector); vm.expectRevert(UniswapV2Executor__InvalidTarget.selector);
@@ -186,8 +304,13 @@ contract UniswapV2ExecutorTest is Test, Constants {
vm.rollFork(26857267); vm.rollFork(26857267);
uint256 amountIn = 10 * 10 ** 6; uint256 amountIn = 10 * 10 ** 6;
bool zeroForOne = true; bool zeroForOne = true;
bytes memory protocolData = bytes memory protocolData = abi.encodePacked(
abi.encodePacked(BASE_USDC, USDC_MAG7_POOL, BOB, zeroForOne); BASE_USDC,
USDC_MAG7_POOL,
BOB,
zeroForOne,
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
);
deal(BASE_USDC, address(uniswapV2Exposed), amountIn); deal(BASE_USDC, address(uniswapV2Exposed), amountIn);

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,9 @@ use std::io::{self, Read};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use tycho_common::{hex_bytes::Bytes, models::Chain}; use tycho_common::{hex_bytes::Bytes, models::Chain};
use tycho_execution::encoding::{ use tycho_execution::encoding::{
evm::encoder_builder::EVMEncoderBuilder, models::Solution, tycho_encoder::TychoEncoder, evm::encoder_builders::{TychoExecutorEncoderBuilder, TychoRouterEncoderBuilder},
models::Solution,
tycho_encoder::TychoEncoder,
}; };
#[derive(Parser)] #[derive(Parser)]
@@ -42,28 +44,27 @@ pub struct Cli {
#[command(subcommand)] #[command(subcommand)]
pub command: Commands, pub command: Commands,
#[arg(short, long)] #[arg(short, long)]
chain: Chain,
#[arg(short, long)]
executors_file_path: Option<String>, executors_file_path: Option<String>,
#[arg(short, long)] #[arg(short, long)]
router_address: Option<Bytes>, router_address: Option<Bytes>,
#[arg(short, long)]
swapper_pk: Option<String>,
#[arg(short, long)]
token_in_already_in_router: Option<bool>,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum Commands { pub enum Commands {
/// Use the Tycho router encoding strategy /// Use Tycho router encoding
TychoRouter, TychoRouter,
/// Use the Tycho router encoding strategy with Permit2 approval and token in transfer /// Use direct execution encoding
TychoRouterPermit2 { TychoExecutor,
#[arg(short, long)]
swapper_pk: String,
},
/// Use the direct execution encoding strategy
DirectExecution,
} }
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse(); let cli = Cli::parse();
let chain = Chain::Ethereum;
// Read from stdin until EOF // Read from stdin until EOF
let mut buffer = String::new(); let mut buffer = String::new();
io::stdin() io::stdin()
@@ -75,24 +76,30 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
let solution: Solution = serde_json::from_str(&buffer)?; let solution: Solution = serde_json::from_str(&buffer)?;
let mut builder = EVMEncoderBuilder::new().chain(chain); let chain = cli.chain;
let encoder: Box<dyn TychoEncoder> = match cli.command {
if let Some(config_path) = cli.executors_file_path { Commands::TychoRouter => {
builder = builder.executors_file_path(config_path); let mut builder = TychoRouterEncoderBuilder::new().chain(chain);
} if let Some(config_path) = cli.executors_file_path {
if let Some(router_address) = cli.router_address { builder = builder.executors_file_path(config_path);
builder = builder.router_address(router_address); }
} if let Some(router_address) = cli.router_address {
builder = builder.router_address(router_address);
builder = match cli.command { }
Commands::TychoRouter => builder.initialize_tycho_router()?, if let Some(swapper_pk) = cli.swapper_pk {
Commands::TychoRouterPermit2 { swapper_pk } => { builder = builder.swapper_pk(swapper_pk);
builder.initialize_tycho_router_with_permit2(swapper_pk)? }
if let Some(token_in_already_in_router) = cli.token_in_already_in_router {
builder = builder.token_in_already_in_router(token_in_already_in_router);
}
builder.build()?
} }
Commands::DirectExecution => builder.initialize_direct_execution()?, Commands::TychoExecutor => TychoExecutorEncoderBuilder::new()
.chain(chain)
.build()?,
}; };
let encoder = builder.build()?;
let transactions = encoder.encode_router_calldata(vec![solution])?; let transactions = encoder.encode_calldata(vec![solution])?;
let encoded = serde_json::json!({ let encoded = serde_json::json!({
"to": format!("0x{}", hex::encode(&transactions[0].to)), "to": format!("0x{}", hex::encode(&transactions[0].to)),
"value": format!("0x{}", hex::encode(transactions[0].value.to_bytes_be())), "value": format!("0x{}", hex::encode(transactions[0].value.to_bytes_be())),

View File

@@ -172,7 +172,7 @@ mod tests {
use alloy_primitives::Uint; use alloy_primitives::Uint;
use num_bigint::BigUint; use num_bigint::BigUint;
use tycho_common::models::Chain as TychoCoreChain; use tycho_common::models::Chain as TychoCommonChain;
use super::*; use super::*;
@@ -208,7 +208,7 @@ mod tests {
} }
fn eth_chain() -> Chain { fn eth_chain() -> Chain {
TychoCoreChain::Ethereum.into() TychoCommonChain::Ethereum.into()
} }
#[test] #[test]

View File

@@ -13,7 +13,36 @@ pub const PROTOCOL_SPECIFIC_CONFIG: &str =
pub static GROUPABLE_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| { pub static GROUPABLE_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
let mut set = HashSet::new(); let mut set = HashSet::new();
set.insert("uniswap_v4"); set.insert("uniswap_v4");
set.insert("balancer_v3"); set.insert("vm:balancer_v3");
set.insert("ekubo_v2");
set
});
/// These protocols need an external in transfer to the pool. This transfer can be from the router,
/// from the user or from the previous pool. Any protocols that are not defined here expect funds to
/// be in the router at the time of swap and do the transfer themselves from msg.sender
pub static IN_TRANSFER_REQUIRED_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
let mut set = HashSet::new();
set.insert("uniswap_v2");
set.insert("sushiswap_v2");
set.insert("pancakeswap_v2");
set.insert("uniswap_v3");
set.insert("pancakeswap_v3");
set.insert("uniswap_v4");
set.insert("ekubo_v2");
set
});
// The protocols here are a subset of the ones defined in IN_TRANSFER_REQUIRED_PROTOCOLS. The tokens
// can not be sent directly from the previous pool into a pool of this protocol. The tokens need to
// be sent to the router and only then transferred into the pool. This is the case for uniswap v3
// because of the callback logic. The only way for this to work it would be to call the second swap
// during the callback of the first swap. This is currently not supported.
pub static CALLBACK_CONSTRAINED_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
let mut set = HashSet::new();
set.insert("uniswap_v3");
set.insert("pancakeswap_v3");
set.insert("uniswap_v4");
set.insert("ekubo_v2"); set.insert("ekubo_v2");
set set
}); });

View File

@@ -1,150 +0,0 @@
use tycho_common::{models::Chain, Bytes};
use crate::encoding::{
errors::EncodingError,
evm::{
strategy_encoder::strategy_encoders::{ExecutorStrategyEncoder, SplitSwapStrategyEncoder},
swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
tycho_encoder::EVMTychoEncoder,
},
strategy_encoder::StrategyEncoder,
};
/// Builder pattern for constructing an `EVMTychoEncoder` with customizable options.
///
/// This struct allows setting a chain and strategy encoder before building the final encoder.
pub struct EVMEncoderBuilder {
strategy: Option<Box<dyn StrategyEncoder>>,
chain: Option<Chain>,
executors_file_path: Option<String>,
router_address: Option<Bytes>,
}
impl Default for EVMEncoderBuilder {
fn default() -> Self {
Self::new()
}
}
impl EVMEncoderBuilder {
pub fn new() -> Self {
EVMEncoderBuilder {
chain: None,
strategy: None,
executors_file_path: None,
router_address: None,
}
}
pub fn chain(mut self, chain: Chain) -> Self {
self.chain = Some(chain);
self
}
/// Sets the `executors_file_path` manually.
/// If it's not set, the default path will be used (config/executor_addresses.json)
pub fn executors_file_path(mut self, executors_file_path: String) -> Self {
self.executors_file_path = Some(executors_file_path);
self
}
/// Sets the `router_address` manually.
/// If it's not set, the default router address will be used (config/router_addresses.json)
pub fn router_address(mut self, router_address: Bytes) -> Self {
self.router_address = Some(router_address);
self
}
/// Sets the `strategy_encoder` manually.
///
/// **Note**: This method should not be used in combination with `tycho_router` or
/// `direct_execution`.
pub fn strategy_encoder(mut self, strategy: Box<dyn StrategyEncoder>) -> Self {
self.strategy = Some(strategy);
self
}
/// Shortcut method to initialize a `SplitSwapStrategyEncoder` without any approval nor token in
/// transfer. **Note**: Should not be used at the same time as `strategy_encoder`.
pub fn initialize_tycho_router(self) -> Result<Self, EncodingError> {
if let Some(chain) = self.chain {
let swap_encoder_registry =
SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?;
let strategy = Box::new(SplitSwapStrategyEncoder::new(
chain,
swap_encoder_registry,
None,
self.router_address.clone(),
)?);
Ok(EVMEncoderBuilder {
chain: Some(chain),
strategy: Some(strategy),
executors_file_path: self.executors_file_path,
router_address: self.router_address,
})
} else {
Err(EncodingError::FatalError(
"Please set the chain before setting the tycho router".to_string(),
))
}
}
/// Shortcut method to initialize a `SplitSwapStrategyEncoder` with Permit2 approval and token
/// in transfer. **Note**: Should not be used at the same time as `strategy_encoder`.
pub fn initialize_tycho_router_with_permit2(
self,
swapper_pk: String,
) -> Result<Self, EncodingError> {
if let Some(chain) = self.chain {
let swap_encoder_registry =
SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?;
let strategy = Box::new(SplitSwapStrategyEncoder::new(
chain,
swap_encoder_registry,
Some(swapper_pk),
self.router_address.clone(),
)?);
Ok(EVMEncoderBuilder {
chain: Some(chain),
strategy: Some(strategy),
executors_file_path: self.executors_file_path,
router_address: self.router_address,
})
} else {
Err(EncodingError::FatalError(
"Please set the chain before setting the tycho router".to_string(),
))
}
}
/// Shortcut method to initialize an `ExecutorStrategyEncoder`.
/// **Note**: Should not be used at the same time as `strategy_encoder`.
pub fn initialize_direct_execution(self) -> Result<Self, EncodingError> {
if let Some(chain) = self.chain {
let swap_encoder_registry =
SwapEncoderRegistry::new(self.executors_file_path.clone(), chain)?;
let strategy = Box::new(ExecutorStrategyEncoder::new(swap_encoder_registry));
Ok(EVMEncoderBuilder {
chain: Some(chain),
strategy: Some(strategy),
executors_file_path: self.executors_file_path,
router_address: self.router_address,
})
} else {
Err(EncodingError::FatalError(
"Please set the chain before setting the strategy".to_string(),
))
}
}
/// Builds the `EVMTychoEncoder` instance using the configured chain and strategy.
/// Returns an error if either the chain or strategy has not been set.
pub fn build(self) -> Result<EVMTychoEncoder, EncodingError> {
if let (Some(chain), Some(strategy)) = (self.chain, self.strategy) {
EVMTychoEncoder::new(chain, strategy)
} else {
Err(EncodingError::FatalError(
"Please set the chain and strategy before building the encoder".to_string(),
))
}
}
}

View File

@@ -0,0 +1,155 @@
use std::collections::HashMap;
use tycho_common::{models::Chain as TychoCommonChain, Bytes};
use crate::encoding::{
errors::EncodingError,
evm::{
constants::DEFAULT_ROUTERS_JSON,
swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
tycho_encoders::{TychoExecutorEncoder, TychoRouterEncoder},
},
models::Chain,
tycho_encoder::TychoEncoder,
};
/// Builder pattern for constructing a `TychoRouterEncoder` with customizable options.
///
/// This struct allows setting a chain and strategy encoder before building the final encoder.
pub struct TychoRouterEncoderBuilder {
swapper_pk: Option<String>,
chain: Option<Chain>,
executors_file_path: Option<String>,
router_address: Option<Bytes>,
token_in_already_in_router: Option<bool>,
}
impl Default for TychoRouterEncoderBuilder {
fn default() -> Self {
Self::new()
}
}
impl TychoRouterEncoderBuilder {
pub fn new() -> Self {
TychoRouterEncoderBuilder {
swapper_pk: None,
chain: None,
executors_file_path: None,
router_address: None,
token_in_already_in_router: None,
}
}
pub fn chain(mut self, chain: TychoCommonChain) -> Self {
self.chain = Some(chain.into());
self
}
/// Sets the `executors_file_path` manually.
/// If it's not set, the default path will be used (config/executor_addresses.json)
pub fn executors_file_path(mut self, executors_file_path: String) -> Self {
self.executors_file_path = Some(executors_file_path);
self
}
/// Sets the `router_address` manually.
/// If it's not set, the default router address will be used (config/router_addresses.json)
pub fn router_address(mut self, router_address: Bytes) -> Self {
self.router_address = Some(router_address);
self
}
pub fn swapper_pk(mut self, swapper_pk: String) -> Self {
self.swapper_pk = Some(swapper_pk);
self
}
// Sets the `token_in_already_in_router` flag.
// If set to true, the encoder will assume that the token in is already in the router.
// WARNING: this is an advanced feature and should be used with caution. Make sure you have
// checks to make sure that your tokens won't be lost. The Router is not considered safe to hold
// tokens, so if this is not done within the same transaction you will lose your tokens.
pub fn token_in_already_in_router(mut self, token_in_already_in_router: bool) -> Self {
self.token_in_already_in_router = Some(token_in_already_in_router);
self
}
/// Builds the `TychoRouterEncoder` instance using the configured chain.
/// Returns an error if either the chain has not been set.
pub fn build(self) -> Result<Box<dyn TychoEncoder>, EncodingError> {
if let Some(chain) = self.chain {
let tycho_router_address;
if let Some(address) = self.router_address {
tycho_router_address = address;
} else {
let default_routers: HashMap<String, Bytes> =
serde_json::from_str(DEFAULT_ROUTERS_JSON)?;
tycho_router_address = default_routers
.get(&chain.name)
.ok_or(EncodingError::FatalError(
"No default router address found for chain".to_string(),
))?
.to_owned();
}
let swap_encoder_registry =
SwapEncoderRegistry::new(self.executors_file_path.clone(), chain.clone())?;
Ok(Box::new(TychoRouterEncoder::new(
chain,
swap_encoder_registry,
self.swapper_pk,
tycho_router_address,
self.token_in_already_in_router
.unwrap_or(false),
)?))
} else {
Err(EncodingError::FatalError(
"Please set the chain and router address before building the encoder".to_string(),
))
}
}
}
/// Builder pattern for constructing a `TychoExecutorEncoder` with customizable options.
pub struct TychoExecutorEncoderBuilder {
chain: Option<Chain>,
executors_file_path: Option<String>,
}
impl Default for TychoExecutorEncoderBuilder {
fn default() -> Self {
Self::new()
}
}
impl TychoExecutorEncoderBuilder {
pub fn new() -> Self {
TychoExecutorEncoderBuilder { chain: None, executors_file_path: None }
}
pub fn chain(mut self, chain: TychoCommonChain) -> Self {
self.chain = Some(chain.into());
self
}
/// Sets the `executors_file_path` manually.
/// If it's not set, the default path will be used (config/executor_addresses.json)
pub fn executors_file_path(mut self, executors_file_path: String) -> Self {
self.executors_file_path = Some(executors_file_path);
self
}
/// Builds the `TychoExecutorEncoder` instance using the configured chain and strategy.
/// Returns an error if either the chain or strategy has not been set.
pub fn build(self) -> Result<Box<dyn TychoEncoder>, EncodingError> {
if let Some(chain) = self.chain {
let swap_encoder_registry =
SwapEncoderRegistry::new(self.executors_file_path.clone(), chain.clone())?;
Ok(Box::new(TychoExecutorEncoder::new(chain, swap_encoder_registry)?))
} else {
Err(EncodingError::FatalError(
"Please set the chain and strategy before building the encoder".to_string(),
))
}
}
}

View File

@@ -6,15 +6,15 @@ use crate::encoding::{evm::constants::GROUPABLE_PROTOCOLS, models::Swap};
/// optimization. /// optimization.
/// ///
/// # Fields /// # Fields
/// * `input_token`: Bytes, the input token of the first swap /// * `token_in`: Bytes, the input token of the first swap
/// * `output_token`: Bytes, the output token of the final swap /// * `token_out`: Bytes, the output token of the final swap
/// * `protocol_system`: String, the protocol system of the swaps /// * `protocol_system`: String, the protocol system of the swaps
/// * `swaps`: Vec<Swap>, the sequence of swaps to be executed as a group /// * `swaps`: Vec<Swap>, the sequence of swaps to be executed as a group
/// * `split`: f64, the split percentage of the first swap in the group /// * `split`: f64, the split percentage of the first swap in the group
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub struct SwapGroup { pub struct SwapGroup {
pub input_token: Bytes, pub token_in: Bytes,
pub output_token: Bytes, pub token_out: Bytes,
pub protocol_system: String, pub protocol_system: String,
pub swaps: Vec<Swap>, pub swaps: Vec<Swap>,
pub split: f64, pub split: f64,
@@ -44,7 +44,7 @@ pub fn group_swaps(swaps: Vec<Swap>) -> Vec<SwapGroup> {
if let Some(group) = current_group.as_mut() { if let Some(group) = current_group.as_mut() {
group.swaps.push(swap.clone()); group.swaps.push(swap.clone());
// Update the output token of the current group. // Update the output token of the current group.
group.output_token = swap.token_out.clone(); group.token_out = swap.token_out.clone();
} }
} else { } else {
// Not second or later USV4 pool. Push the current group (if it exists) and then // Not second or later USV4 pool. Push the current group (if it exists) and then
@@ -53,8 +53,8 @@ pub fn group_swaps(swaps: Vec<Swap>) -> Vec<SwapGroup> {
grouped_swaps.push(group.clone()); grouped_swaps.push(group.clone());
} }
current_group = Some(SwapGroup { current_group = Some(SwapGroup {
input_token: swap.token_in.clone(), token_in: swap.token_in.clone(),
output_token: swap.token_out.clone(), token_out: swap.token_out.clone(),
protocol_system: current_swap_protocol.clone(), protocol_system: current_swap_protocol.clone(),
swaps: vec![swap.clone()], swaps: vec![swap.clone()],
split: swap.split, split: swap.split,
@@ -135,15 +135,15 @@ mod tests {
vec![ vec![
SwapGroup { SwapGroup {
swaps: vec![swap_weth_wbtc, swap_wbtc_usdc], swaps: vec![swap_weth_wbtc, swap_wbtc_usdc],
input_token: weth, token_in: weth,
output_token: usdc.clone(), token_out: usdc.clone(),
protocol_system: "uniswap_v4".to_string(), protocol_system: "uniswap_v4".to_string(),
split: 0f64, split: 0f64,
}, },
SwapGroup { SwapGroup {
swaps: vec![swap_usdc_dai], swaps: vec![swap_usdc_dai],
input_token: usdc, token_in: usdc,
output_token: dai, token_out: dai,
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
split: 0f64, split: 0f64,
} }
@@ -216,22 +216,22 @@ mod tests {
vec![ vec![
SwapGroup { SwapGroup {
swaps: vec![swap_wbtc_weth], swaps: vec![swap_wbtc_weth],
input_token: wbtc.clone(), token_in: wbtc.clone(),
output_token: weth.clone(), token_out: weth.clone(),
protocol_system: "uniswap_v4".to_string(), protocol_system: "uniswap_v4".to_string(),
split: 0f64, split: 0f64,
}, },
SwapGroup { SwapGroup {
swaps: vec![swap_weth_usdc], swaps: vec![swap_weth_usdc],
input_token: weth.clone(), token_in: weth.clone(),
output_token: usdc.clone(), token_out: usdc.clone(),
protocol_system: "uniswap_v4".to_string(), protocol_system: "uniswap_v4".to_string(),
split: 0.5f64, split: 0.5f64,
}, },
SwapGroup { SwapGroup {
swaps: vec![swap_weth_dai, swap_dai_usdc], swaps: vec![swap_weth_dai, swap_dai_usdc],
input_token: weth, token_in: weth,
output_token: usdc, token_out: usdc,
protocol_system: "uniswap_v4".to_string(), protocol_system: "uniswap_v4".to_string(),
split: 0f64, split: 0f64,
} }
@@ -255,7 +255,7 @@ mod tests {
let swap_weth_wbtc = Swap { let swap_weth_wbtc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
protocol_system: "balancer_v3".to_string(), protocol_system: "vm:balancer_v3".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), token_in: weth.clone(),
@@ -264,7 +264,7 @@ mod tests {
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
protocol_system: "balancer_v3".to_string(), protocol_system: "vm:balancer_v3".to_string(),
..Default::default() ..Default::default()
}, },
token_in: wbtc.clone(), token_in: wbtc.clone(),
@@ -304,15 +304,15 @@ mod tests {
vec![ vec![
SwapGroup { SwapGroup {
swaps: vec![swap_weth_wbtc, swap_wbtc_usdc], swaps: vec![swap_weth_wbtc, swap_wbtc_usdc],
input_token: weth.clone(), token_in: weth.clone(),
output_token: usdc.clone(), token_out: usdc.clone(),
protocol_system: "balancer_v3".to_string(), protocol_system: "vm:balancer_v3".to_string(),
split: 0.5f64, split: 0.5f64,
}, },
SwapGroup { SwapGroup {
swaps: vec![swap_weth_dai, swap_dai_usdc], swaps: vec![swap_weth_dai, swap_dai_usdc],
input_token: weth, token_in: weth,
output_token: usdc, token_out: usdc,
protocol_system: "uniswap_v4".to_string(), protocol_system: "uniswap_v4".to_string(),
split: 0f64, split: 0f64,
} }

View File

@@ -1,7 +1,8 @@
pub mod approvals; pub mod approvals;
mod constants; mod constants;
pub mod encoder_builder; pub mod encoder_builders;
mod group_swaps;
pub mod strategy_encoder; pub mod strategy_encoder;
mod swap_encoder; mod swap_encoder;
pub mod tycho_encoder; pub mod tycho_encoders;
pub mod utils; pub mod utils;

View File

@@ -1,3 +1,4 @@
mod group_swaps;
pub mod strategy_encoders; pub mod strategy_encoders;
mod strategy_validators; mod strategy_validators;
mod transfer_optimizations;

File diff suppressed because it is too large Load Diff

View File

@@ -7,92 +7,10 @@ use crate::encoding::{
models::{NativeAction, Solution, Swap}, models::{NativeAction, Solution, Swap},
}; };
/// Validates whether a sequence of split swaps represents a valid solution. pub trait SwapValidator {
#[derive(Clone)]
pub struct SplitSwapValidator;
impl SplitSwapValidator {
/// Raises an error if the split percentages are invalid.
///
/// Split percentages are considered valid if all the following conditions are met:
/// * Each split amount is < 1 (100%)
/// * There is exactly one 0% split for each token, and it's the last swap specified, signifying
/// to the router to send the remainder of the token to the designated protocol
/// * The sum of all non-remainder splits for each token is < 1 (100%)
/// * There are no negative split amounts
pub fn validate_split_percentages(&self, swaps: &[Swap]) -> Result<(), EncodingError> {
let mut swaps_by_token: HashMap<Bytes, Vec<&Swap>> = HashMap::new();
for swap in swaps {
if swap.split >= 1.0 {
return Err(EncodingError::InvalidInput(format!(
"Split percentage must be less than 1 (100%), got {}",
swap.split
)));
}
swaps_by_token
.entry(swap.token_in.clone())
.or_default()
.push(swap);
}
for (token, token_swaps) in swaps_by_token {
// Single swaps don't need remainder handling
if token_swaps.len() == 1 {
if token_swaps[0].split != 0.0 {
return Err(EncodingError::InvalidInput(format!(
"Single swap must have 0% split for token {:?}",
token
)));
}
continue;
}
let mut found_zero_split = false;
let mut total_percentage = 0.0;
for (i, swap) in token_swaps.iter().enumerate() {
match (swap.split == 0.0, i == token_swaps.len() - 1) {
(true, false) => {
return Err(EncodingError::InvalidInput(format!(
"The 0% split for token {:?} must be the last swap",
token
)))
}
(true, true) => found_zero_split = true,
(false, _) => {
if swap.split < 0.0 {
return Err(EncodingError::InvalidInput(format!(
"All splits must be >= 0% for token {:?}",
token
)));
}
total_percentage += swap.split;
}
}
}
if !found_zero_split {
return Err(EncodingError::InvalidInput(format!(
"Token {:?} must have exactly one 0% split for remainder handling",
token
)));
}
// Total must be <100% to leave room for remainder
if total_percentage >= 1.0 {
return Err(EncodingError::InvalidInput(format!(
"Total of non-remainder splits for token {:?} must be <100%, got {}%",
token,
total_percentage * 100.0
)));
}
}
Ok(())
}
/// Raises an error if the solution does not have checked amount set or slippage with checked /// Raises an error if the solution does not have checked amount set or slippage with checked
/// amount set. /// amount set.
pub fn validate_solution_min_amounts(&self, solution: &Solution) -> Result<(), EncodingError> { fn validate_solution_min_amounts(&self, solution: &Solution) -> Result<(), EncodingError> {
if solution.checked_amount.is_none() && if solution.checked_amount.is_none() &&
(solution.slippage.is_none() || solution.expected_amount.is_none()) (solution.slippage.is_none() || solution.expected_amount.is_none())
{ {
@@ -113,7 +31,7 @@ impl SplitSwapValidator {
/// If the given token is the native token and the native action is WRAP, it will be converted /// If the given token is the native token and the native action is WRAP, it will be converted
/// to the wrapped token before validating the swap path. The same principle applies for the /// to the wrapped token before validating the swap path. The same principle applies for the
/// checked token and the UNWRAP action. /// checked token and the UNWRAP action.
pub fn validate_swap_path( fn validate_swap_path(
&self, &self,
swaps: &[Swap], swaps: &[Swap],
given_token: &Bytes, given_token: &Bytes,
@@ -197,6 +115,98 @@ impl SplitSwapValidator {
} }
} }
/// Validates whether a sequence of split swaps represents a valid solution.
#[derive(Clone)]
pub struct SplitSwapValidator;
impl SwapValidator for SplitSwapValidator {}
impl SplitSwapValidator {
/// Raises an error if the split percentages are invalid.
///
/// Split percentages are considered valid if all the following conditions are met:
/// * Each split amount is < 1 (100%)
/// * There is exactly one 0% split for each token, and it's the last swap specified, signifying
/// to the router to send the remainder of the token to the designated protocol
/// * The sum of all non-remainder splits for each token is < 1 (100%)
/// * There are no negative split amounts
pub fn validate_split_percentages(&self, swaps: &[Swap]) -> Result<(), EncodingError> {
let mut swaps_by_token: HashMap<Bytes, Vec<&Swap>> = HashMap::new();
for swap in swaps {
if swap.split >= 1.0 {
return Err(EncodingError::InvalidInput(format!(
"Split percentage must be less than 1 (100%), got {}",
swap.split
)));
}
swaps_by_token
.entry(swap.token_in.clone())
.or_default()
.push(swap);
}
for (token, token_swaps) in swaps_by_token {
// Single swaps don't need remainder handling
if token_swaps.len() == 1 {
if token_swaps[0].split != 0.0 {
return Err(EncodingError::InvalidInput(format!(
"Single swap must have 0% split for token {:?}",
token
)));
}
continue;
}
let mut found_zero_split = false;
let mut total_percentage = 0.0;
for (i, swap) in token_swaps.iter().enumerate() {
match (swap.split == 0.0, i == token_swaps.len() - 1) {
(true, false) => {
return Err(EncodingError::InvalidInput(format!(
"The 0% split for token {:?} must be the last swap",
token
)))
}
(true, true) => found_zero_split = true,
(false, _) => {
if swap.split < 0.0 {
return Err(EncodingError::InvalidInput(format!(
"All splits must be >= 0% for token {:?}",
token
)));
}
total_percentage += swap.split;
}
}
}
if !found_zero_split {
return Err(EncodingError::InvalidInput(format!(
"Token {:?} must have exactly one 0% split for remainder handling",
token
)));
}
// Total must be <100% to leave room for remainder
if total_percentage >= 1.0 {
return Err(EncodingError::InvalidInput(format!(
"Total of non-remainder splits for token {:?} must be <100%, got {}%",
token,
total_percentage * 100.0
)));
}
}
Ok(())
}
}
/// Validates whether a sequence of sequential swaps represents a valid solution.
#[derive(Clone)]
pub struct SequentialSwapValidator;
impl SwapValidator for SequentialSwapValidator {}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::str::FromStr; use std::str::FromStr;

View File

@@ -0,0 +1,352 @@
use std::str::FromStr;
use tycho_common::Bytes;
use crate::encoding::{
errors::EncodingError,
evm::{
constants::{CALLBACK_CONSTRAINED_PROTOCOLS, IN_TRANSFER_REQUIRED_PROTOCOLS},
group_swaps::SwapGroup,
},
models::TransferType,
};
/// A struct that defines how the tokens will be transferred into the given pool given the solution.
#[derive(Clone)]
pub struct TransferOptimization {
native_token: Bytes,
wrapped_token: Bytes,
permit2: bool,
token_in_already_in_router: bool,
router_address: Bytes,
}
impl TransferOptimization {
pub fn new(
native_token: Bytes,
wrapped_token: Bytes,
permit2: bool,
token_in_already_in_router: bool,
router_address: Bytes,
) -> Self {
TransferOptimization {
native_token,
wrapped_token,
permit2,
token_in_already_in_router,
router_address,
}
}
/// Returns the transfer method that should be used for the given swap and solution.
pub fn get_transfer_type(
&self,
swap: SwapGroup,
given_token: Bytes,
wrap: bool,
in_between_swap_optimization: bool,
) -> TransferType {
let in_transfer_required: bool =
IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&swap.protocol_system.as_str());
let is_first_swap = swap.token_in == given_token;
if swap.token_in == self.native_token {
// Funds are already in router. All protocols currently take care of native transfers.
TransferType::None
} else if (swap.token_in == self.wrapped_token) && wrap {
// Wrapping already happened in the router so we can just use a normal transfer.
TransferType::TransferToProtocol
} else if is_first_swap {
if in_transfer_required {
if self.token_in_already_in_router {
// Transfer from router to pool.
TransferType::TransferToProtocol
} else if self.permit2 {
// Transfer from swapper to pool using permit2.
TransferType::TransferPermit2ToProtocol
} else {
// Transfer from swapper to pool.
TransferType::TransferFromToProtocol
}
// in transfer is not necessary for these protocols. Only make a transfer if the
// tokens are not already in the router
} else if !self.token_in_already_in_router {
if self.permit2 {
// Transfer from swapper to router using permit2.
TransferType::TransferPermit2ToRouter
} else {
// Transfer from swapper to router.
TransferType::TransferFromToRouter
}
} else {
TransferType::None
}
// all other swaps
} else if !in_transfer_required || in_between_swap_optimization {
// funds should already be in the router or in the next pool
TransferType::None
} else {
TransferType::TransferToProtocol
}
}
// Returns the optimized receiver of the swap. This is used to chain swaps together and avoid
// unnecessary token transfers.
// Returns the receiver address and a boolean indicating whether the receiver is optimized (this
// is necessary for the next swap transfer type decision).
pub fn get_receiver(
&self,
solution_receiver: Bytes,
next_swap: Option<&SwapGroup>,
) -> Result<(Bytes, bool), EncodingError> {
if let Some(next) = next_swap {
// if the protocol of the next swap supports transfer in optimization
if IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&next.protocol_system.as_str()) {
// if the protocol does not allow for chained swaps, we can't optimize the
// receiver of this swap nor the transfer in of the next swap
if CALLBACK_CONSTRAINED_PROTOCOLS.contains(&next.protocol_system.as_str()) {
Ok((self.router_address.clone(), false))
} else {
Ok((
Bytes::from_str(&next.swaps[0].component.id.clone()).map_err(|_| {
EncodingError::FatalError("Invalid component id".to_string())
})?,
true,
))
}
} else {
// the protocol of the next swap does not support transfer in optimization
Ok((self.router_address.clone(), false))
}
} else {
// last swap - there is no next swap
Ok((solution_receiver, false))
}
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::hex;
use rstest::rstest;
use tycho_common::models::protocol::ProtocolComponent;
use super::*;
use crate::encoding::models::Swap;
fn weth() -> Bytes {
Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec())
}
fn eth() -> Bytes {
Bytes::from(hex!("0000000000000000000000000000000000000000").to_vec())
}
fn dai() -> Bytes {
Bytes::from(hex!("6b175474e89094c44da98b954eedeac495271d0f").to_vec())
}
fn usdc() -> Bytes {
Bytes::from(hex!("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").to_vec())
}
fn router_address() -> Bytes {
Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f")
}
#[test]
fn test_first_swap_transfer_from_permit2() {
// The swap token is the same as the given token, which is not the native token
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: weth(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let optimization = TransferOptimization::new(eth(), weth(), true, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false);
assert_eq!(transfer_method, TransferType::TransferPermit2ToProtocol);
}
#[test]
fn test_first_swap_transfer_from() {
// The swap token is the same as the given token, which is not the native token
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: weth(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false);
assert_eq!(transfer_method, TransferType::TransferFromToProtocol);
}
#[test]
fn test_first_swap_native() {
// The swap token is the same as the given token, and it's the native token.
// No transfer action is needed.
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: eth(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), false, false);
assert_eq!(transfer_method, TransferType::None);
}
#[test]
fn test_first_swap_wrapped() {
// The swap token is NOT the same as the given token, but we are wrapping.
// Since the swap's token in is the wrapped token - this is the first swap.
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: weth(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), true, false);
assert_eq!(transfer_method, TransferType::TransferToProtocol);
}
#[test]
fn test_not_first_swap() {
// The swap token is NOT the same as the given token, and we are NOT wrapping.
// Thus, this is not the first swap.
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: usdc(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false);
assert_eq!(transfer_method, TransferType::TransferToProtocol);
}
#[test]
fn test_not_first_swap_funds_in_router() {
// Not the first swap and the protocol requires the funds to be in the router (which they
// already are, so the transfer type is None)
let swap = SwapGroup {
protocol_system: "vm:curve".to_string(),
token_in: usdc(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false);
assert_eq!(transfer_method, TransferType::None);
}
#[test]
fn test_not_first_swap_in_between_swap_optimization() {
// Not the first swap and the in between swaps are optimized. The funds should already be in
// the next pool or in the router
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: usdc(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, true);
assert_eq!(transfer_method, TransferType::None);
}
#[test]
fn test_first_swap_tokens_already_in_router_optimization() {
// It is the first swap, tokens are already in the router and the protocol requires the
// transfer in
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: usdc(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let optimization = TransferOptimization::new(eth(), weth(), false, true, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), usdc(), false, false);
assert_eq!(transfer_method, TransferType::TransferToProtocol);
}
#[test]
fn test_first_swap_tokens_already_in_router_no_transfer_needed_optimization() {
// It is the first swap, tokens are already in the router and the protocol does not require
// the transfer in
let swap = SwapGroup {
protocol_system: "vm:curve".to_string(),
token_in: usdc(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let optimization = TransferOptimization::new(eth(), weth(), false, true, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), usdc(), false, false);
assert_eq!(transfer_method, TransferType::None);
}
fn receiver() -> Bytes {
Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2")
}
fn component_id() -> Bytes {
Bytes::from("0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11")
}
#[rstest]
// there is no next swap -> receiver is the solution receiver
#[case(None, receiver(), false)]
// protocol of next swap supports transfer in optimization
#[case(Some("uniswap_v2"), component_id(), true)]
// protocol of next swap supports transfer in optimization but is callback constrained
#[case(Some("uniswap_v3"), router_address(), false)]
// protocol of next swap does not support transfer in optimization
#[case(Some("vm:curve"), router_address(), false)]
fn test_get_receiver(
#[case] protocol: Option<&str>,
#[case] expected_receiver: Bytes,
#[case] expected_optimization: bool,
) {
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let next_swap = if protocol.is_none() {
None
} else {
Some(SwapGroup {
protocol_system: protocol.unwrap().to_string(),
token_in: usdc(),
token_out: dai(),
split: 0f64,
swaps: vec![Swap {
component: ProtocolComponent {
protocol_system: protocol.unwrap().to_string(),
id: component_id().to_string(),
..Default::default()
},
token_in: usdc(),
token_out: dai(),
split: 0f64,
}],
})
};
let result = optimization.get_receiver(receiver(), next_swap.as_ref());
assert!(result.is_ok());
let (actual_receiver, optimization_flag) = result.unwrap();
assert_eq!(actual_receiver, expected_receiver);
assert_eq!(optimization_flag, expected_optimization);
}
}

View File

@@ -20,11 +20,7 @@ pub struct SwapEncoderRegistry {
impl SwapEncoderRegistry { impl SwapEncoderRegistry {
/// Populates the registry with the `SwapEncoders` for the given blockchain by parsing the /// Populates the registry with the `SwapEncoders` for the given blockchain by parsing the
/// executors' addresses in the file at the given path. /// executors' addresses in the file at the given path.
pub fn new( pub fn new(executors_file_path: Option<String>, chain: Chain) -> Result<Self, EncodingError> {
executors_file_path: Option<String>,
blockchain: tycho_common::models::Chain,
) -> Result<Self, EncodingError> {
let chain = Chain::from(blockchain);
let config_str = if let Some(ref path) = executors_file_path { let config_str = if let Some(ref path) = executors_file_path {
fs::read_to_string(path).map_err(|e| { fs::read_to_string(path).map_err(|e| {
EncodingError::FatalError(format!( EncodingError::FatalError(format!(

File diff suppressed because it is too large Load Diff

View File

@@ -1,720 +0,0 @@
use std::collections::HashSet;
use num_bigint::BigUint;
use tycho_common::Bytes;
use crate::encoding::{
errors::EncodingError,
models::{Chain, NativeAction, Solution, Transaction},
strategy_encoder::StrategyEncoder,
tycho_encoder::TychoEncoder,
};
/// Represents an encoder for a swap using any strategy supported by the strategy registry.
///
/// # Fields
/// * `strategy_encoder`: Strategy encoder to follow for encoding the solution
/// * `native_address`: Address of the chain's native token
/// * `wrapped_address`: Address of the chain's wrapped native token
pub struct EVMTychoEncoder {
strategy_encoder: Box<dyn StrategyEncoder>,
native_address: Bytes,
wrapped_address: Bytes,
}
impl Clone for EVMTychoEncoder {
fn clone(&self) -> Self {
Self {
strategy_encoder: self.strategy_encoder.clone_box(),
native_address: self.native_address.clone(),
wrapped_address: self.wrapped_address.clone(),
}
}
}
impl EVMTychoEncoder {
pub fn new(
chain: tycho_common::models::Chain,
strategy_encoder: Box<dyn StrategyEncoder>,
) -> Result<Self, EncodingError> {
let chain: Chain = Chain::from(chain);
let native_address = chain.native_token()?;
let wrapped_address = chain.wrapped_token()?;
Ok(EVMTychoEncoder { strategy_encoder, native_address, wrapped_address })
}
}
impl EVMTychoEncoder {
/// Raises an `EncodingError` if the solution is not considered valid.
///
/// A solution is considered valid if all the following conditions are met:
/// * The solution is not exact out.
/// * The solution has at least one swap.
/// * If the solution is wrapping, the given token is the chain's native token and the first
/// swap's input is the chain's wrapped token.
/// * If the solution is unwrapping, the checked token is the chain's native token and the last
/// swap's output is the chain's wrapped token.
/// * The token cannot appear more than once in the solution unless it is the first and last
/// token (i.e. a true cyclical swap).
fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> {
if solution.exact_out {
return Err(EncodingError::FatalError(
"Currently only exact input solutions are supported".to_string(),
));
}
if solution.swaps.is_empty() {
return Err(EncodingError::FatalError("No swaps found in solution".to_string()));
}
if let Some(native_action) = solution.clone().native_action {
if native_action == NativeAction::Wrap {
if solution.given_token != self.native_address {
return Err(EncodingError::FatalError(
"Native token must be the input token in order to wrap".to_string(),
));
}
if let Some(first_swap) = solution.swaps.first() {
if first_swap.token_in != self.wrapped_address {
return Err(EncodingError::FatalError(
"Wrapped token must be the first swap's input in order to wrap"
.to_string(),
));
}
}
} else if native_action == NativeAction::Unwrap {
if solution.checked_token != self.native_address {
return Err(EncodingError::FatalError(
"Native token must be the output token in order to unwrap".to_string(),
));
}
if let Some(last_swap) = solution.swaps.last() {
if last_swap.token_out != self.wrapped_address {
return Err(EncodingError::FatalError(
"Wrapped token must be the last swap's output in order to unwrap"
.to_string(),
));
}
}
}
}
let mut solution_tokens = vec![];
let mut split_tokens_already_considered = HashSet::new();
for (i, swap) in solution.swaps.iter().enumerate() {
// so we don't count the split tokens more than once
if swap.split != 0.0 {
if !split_tokens_already_considered.contains(&swap.token_in) {
solution_tokens.push(swap.token_in.clone());
split_tokens_already_considered.insert(swap.token_in.clone());
}
} else {
// it might be the last swap of the split or a regular swap
if !split_tokens_already_considered.contains(&swap.token_in) {
solution_tokens.push(swap.token_in.clone());
}
}
if i == solution.swaps.len() - 1 {
solution_tokens.push(swap.token_out.clone());
}
}
if solution_tokens.len() !=
solution_tokens
.iter()
.cloned()
.collect::<HashSet<Bytes>>()
.len()
{
if let Some(last_swap) = solution.swaps.last() {
if solution.swaps[0].token_in != last_swap.token_out {
return Err(EncodingError::FatalError(
"Cyclical swaps are only allowed if they are the first and last token of a solution".to_string(),
));
} else {
// it is a valid cyclical swap
// we don't support any wrapping or unwrapping in this case
if let Some(_native_action) = solution.clone().native_action {
return Err(EncodingError::FatalError(
"Wrapping/Unwrapping is not available in cyclical swaps".to_string(),
));
}
}
}
}
Ok(())
}
}
impl TychoEncoder for EVMTychoEncoder {
fn encode_router_calldata(
&self,
solutions: Vec<Solution>,
) -> Result<Vec<Transaction>, EncodingError> {
let mut transactions: Vec<Transaction> = Vec::new();
for solution in solutions.iter() {
self.validate_solution(solution)?;
let (contract_interaction, target_address) = self
.strategy_encoder
.encode_strategy(solution.clone())?;
let value = if solution.given_token == self.native_address {
solution.given_amount.clone()
} else {
BigUint::ZERO
};
transactions.push(Transaction {
value,
data: contract_interaction,
to: target_address,
});
}
Ok(transactions)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use tycho_common::models::{protocol::ProtocolComponent, Chain as TychoCoreChain};
use super::*;
use crate::encoding::{
models::Swap, strategy_encoder::StrategyEncoder, swap_encoder::SwapEncoder,
};
fn dai() -> Bytes {
Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap()
}
fn eth() -> Bytes {
Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap()
}
fn weth() -> Bytes {
Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap()
}
fn usdc() -> Bytes {
Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap()
}
fn wbtc() -> Bytes {
Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap()
}
#[derive(Clone)]
struct MockStrategy;
impl StrategyEncoder for MockStrategy {
fn encode_strategy(&self, _solution: Solution) -> Result<(Vec<u8>, Bytes), EncodingError> {
Ok((
Bytes::from_str("0x1234")
.unwrap()
.to_vec(),
Bytes::from_str("0xabcd").unwrap(),
))
}
fn get_swap_encoder(&self, _protocol_system: &str) -> Option<&Box<dyn SwapEncoder>> {
None
}
fn clone_box(&self) -> Box<dyn StrategyEncoder> {
Box::new(self.clone())
}
}
fn get_mocked_tycho_encoder() -> EVMTychoEncoder {
let strategy_encoder = Box::new(MockStrategy {});
EVMTychoEncoder::new(TychoCoreChain::Ethereum, strategy_encoder).unwrap()
}
#[test]
fn test_encode_router_calldata() {
let encoder = get_mocked_tycho_encoder();
let eth_amount_in = BigUint::from(1000u32);
let swap = Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth(),
token_out: dai(),
split: 0f64,
};
let solution = Solution {
exact_out: false,
given_amount: eth_amount_in.clone(),
given_token: eth(),
swaps: vec![swap],
native_action: Some(NativeAction::Wrap),
..Default::default()
};
let transactions = encoder.encode_router_calldata(vec![solution]);
assert!(transactions.is_ok());
let transactions = transactions.unwrap();
assert_eq!(transactions.len(), 1);
assert_eq!(transactions[0].value, eth_amount_in);
assert_eq!(transactions[0].data, Bytes::from_str("0x1234").unwrap());
assert_eq!(transactions[0].to, Bytes::from_str("0xabcd").unwrap());
}
#[test]
fn test_validate_fails_for_exact_out() {
let encoder = get_mocked_tycho_encoder();
let solution = Solution {
exact_out: true, // This should cause an error
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_err());
assert_eq!(
result.err().unwrap(),
EncodingError::FatalError(
"Currently only exact input solutions are supported".to_string()
)
);
}
#[test]
fn test_validate_passes_for_wrap() {
let encoder = get_mocked_tycho_encoder();
let swap = Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth(),
token_out: dai(),
split: 0f64,
};
let solution = Solution {
exact_out: false,
given_token: eth(),
checked_token: dai(),
checked_amount: None,
swaps: vec![swap],
native_action: Some(NativeAction::Wrap),
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_ok());
}
#[test]
fn test_validate_fails_for_wrap_wrong_input() {
let encoder = get_mocked_tycho_encoder();
let swap = Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth(),
token_out: dai(),
split: 0f64,
};
let solution = Solution {
exact_out: false,
given_token: weth(),
swaps: vec![swap],
native_action: Some(NativeAction::Wrap),
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_err());
assert_eq!(
result.err().unwrap(),
EncodingError::FatalError(
"Native token must be the input token in order to wrap".to_string()
)
);
}
#[test]
fn test_validate_fails_for_wrap_wrong_first_swap() {
let encoder = get_mocked_tycho_encoder();
let swap = Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: eth(),
token_out: dai(),
split: 0f64,
};
let solution = Solution {
exact_out: false,
given_token: eth(),
swaps: vec![swap],
native_action: Some(NativeAction::Wrap),
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_err());
assert_eq!(
result.err().unwrap(),
EncodingError::FatalError(
"Wrapped token must be the first swap's input in order to wrap".to_string()
)
);
}
#[test]
fn test_validate_fails_no_swaps() {
let encoder = get_mocked_tycho_encoder();
let solution = Solution {
exact_out: false,
given_token: eth(),
swaps: vec![],
native_action: Some(NativeAction::Wrap),
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_err());
assert_eq!(
result.err().unwrap(),
EncodingError::FatalError("No swaps found in solution".to_string())
);
}
#[test]
fn test_validate_passes_for_unwrap() {
let encoder = get_mocked_tycho_encoder();
let swap = Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: weth(),
split: 0f64,
};
let solution = Solution {
exact_out: false,
checked_token: eth(),
checked_amount: None,
swaps: vec![swap],
native_action: Some(NativeAction::Unwrap),
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_ok());
}
#[test]
fn test_validate_fails_for_unwrap_wrong_output() {
let encoder = get_mocked_tycho_encoder();
let swap = Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: weth(),
split: 0f64,
};
let solution = Solution {
exact_out: false,
given_token: dai(),
checked_token: weth(),
swaps: vec![swap],
native_action: Some(NativeAction::Unwrap),
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_err());
assert_eq!(
result.err().unwrap(),
EncodingError::FatalError(
"Native token must be the output token in order to unwrap".to_string()
)
);
}
#[test]
fn test_validate_fails_for_unwrap_wrong_last_swap() {
let encoder = get_mocked_tycho_encoder();
let swap = Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: eth(),
split: 0f64,
};
let solution = Solution {
exact_out: false,
checked_token: eth(),
swaps: vec![swap],
native_action: Some(NativeAction::Unwrap),
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_err());
assert_eq!(
result.err().unwrap(),
EncodingError::FatalError(
"Wrapped token must be the last swap's output in order to unwrap".to_string()
)
);
}
#[test]
fn test_validate_cyclical_swap() {
// This validation passes because the cyclical swap is the first and last token
// 50% -> WETH
// DAI - -> DAI
// 50% -> WETH
// (some of the pool addresses in this test are fake)
let encoder = get_mocked_tycho_encoder();
let swaps = vec![
Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: weth(),
split: 0.5f64,
},
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: weth(),
split: 0f64,
},
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth(),
token_out: dai(),
split: 0f64,
},
];
let solution = Solution {
exact_out: false,
given_token: dai(),
checked_token: dai(),
swaps,
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_ok());
}
#[test]
fn test_validate_cyclical_swap_fail() {
// This test should fail because the cyclical swap is not the first and last token
// DAI -> WETH -> USDC -> DAI -> WBTC
// (some of the pool addresses in this test are fake)
let encoder = get_mocked_tycho_encoder();
let swaps = vec![
Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: weth(),
split: 0f64,
},
Swap {
component: ProtocolComponent {
id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth(),
token_out: usdc(),
split: 0f64,
},
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: usdc(),
token_out: dai(),
split: 0f64,
},
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: wbtc(),
split: 0f64,
},
];
let solution = Solution {
exact_out: false,
given_token: dai(),
checked_token: wbtc(),
swaps,
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_err());
assert_eq!(
result.err().unwrap(),
EncodingError::FatalError(
"Cyclical swaps are only allowed if they are the first and last token of a solution".to_string()
)
);
}
#[test]
fn test_validate_cyclical_swap_split_output() {
// This validation passes because it is a valid cyclical swap
// -> WETH
// WETH -> DAI
// -> WETH
// (some of the pool addresses in this test are fake)
let encoder = get_mocked_tycho_encoder();
let swaps = vec![
Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth(),
token_out: dai(),
split: 0f64,
},
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: weth(),
split: 0.5f64,
},
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: weth(),
split: 0f64,
},
];
let solution = Solution {
exact_out: false,
given_token: weth(),
checked_token: weth(),
swaps,
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_ok());
}
#[test]
fn test_validate_cyclical_swap_native_action_fail() {
// This validation fails because there is a native action with a valid cyclical swap
// ETH -> WETH -> DAI -> WETH
// (some of the pool addresses in this test are fake)
let encoder = get_mocked_tycho_encoder();
let swaps = vec![
Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth(),
token_out: dai(),
split: 0f64,
},
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: weth(),
split: 0f64,
},
];
let solution = Solution {
exact_out: false,
given_token: eth(),
checked_token: weth(),
swaps,
native_action: Some(NativeAction::Wrap),
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_err());
assert_eq!(
result.err().unwrap(),
EncodingError::FatalError(
"Wrapping/Unwrapping is not available in cyclical swaps"
.to_string()
.to_string()
)
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ use alloy::{
transports::BoxTransport, transports::BoxTransport,
}; };
use alloy_primitives::{aliases::U24, keccak256, Address, FixedBytes, Keccak256, U256, U8}; use alloy_primitives::{aliases::U24, keccak256, Address, FixedBytes, Keccak256, U256, U8};
use alloy_sol_types::SolValue;
use num_bigint::BigUint; use num_bigint::BigUint;
use tokio::runtime::{Handle, Runtime}; use tokio::runtime::{Handle, Runtime};
use tycho_common::Bytes; use tycho_common::Bytes;
@@ -150,6 +151,21 @@ pub async fn get_client() -> Result<Arc<RootProvider<BoxTransport>>, EncodingErr
Ok(Arc::new(client)) Ok(Arc::new(client))
} }
/// Uses prefix-length encoding to efficient encode action data.
///
/// Prefix-length encoding is a data encoding method where the beginning of a data segment
/// (the "prefix") contains information about the length of the following data.
pub fn ple_encode(action_data_array: Vec<Vec<u8>>) -> Vec<u8> {
let mut encoded_action_data: Vec<u8> = Vec::new();
for action_data in action_data_array {
let args = (encoded_action_data, action_data.len() as u16, action_data);
encoded_action_data = args.abi_encode_packed();
}
encoded_action_data
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use num_bigint::BigUint; use num_bigint::BigUint;

View File

@@ -2,7 +2,7 @@ use hex;
use num_bigint::BigUint; use num_bigint::BigUint;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tycho_common::{ use tycho_common::{
models::{protocol::ProtocolComponent, Chain as TychoCoreChain}, models::{protocol::ProtocolComponent, Chain as TychoCommonChain},
Bytes, Bytes,
}; };
@@ -96,6 +96,27 @@ pub struct Transaction {
pub data: Vec<u8>, pub data: Vec<u8>,
} }
/// Represents the type of transfer to be performed into the pool.
///
/// # Fields
///
/// * `TransferToProtocol`: Transfer the token from the router into the protocol.
/// * `TransferFromToProtocol`: Transfer the token from the sender to the protocol.
/// * `TransferPermit2ToProtocol`: Transfer the token from the sender to the protocol using Permit2.
/// * `TransferFromToRouter`: Transfer the token from the sender to the router.
/// * `TransferPermit2ToRouter`: Transfer the token from the sender to the router using Permit2.
/// * `None`: No transfer is needed. Tokens are already in the pool.
#[repr(u8)]
#[derive(Clone, Debug, PartialEq)]
pub enum TransferType {
TransferToProtocol = 0,
TransferFromToProtocol = 1,
TransferPermit2ToProtocol = 2,
TransferFromToRouter = 3,
TransferPermit2ToRouter = 4,
None = 5,
}
/// Represents necessary attributes for encoding an order. /// Represents necessary attributes for encoding an order.
/// ///
/// # Fields /// # Fields
@@ -113,6 +134,7 @@ pub struct EncodingContext {
pub router_address: Option<Bytes>, pub router_address: Option<Bytes>,
pub group_token_in: Bytes, pub group_token_in: Bytes,
pub group_token_out: Bytes, pub group_token_out: Bytes,
pub transfer_type: TransferType,
} }
#[derive(Clone, PartialEq, Eq, Hash)] #[derive(Clone, PartialEq, Eq, Hash)]
@@ -121,15 +143,15 @@ pub struct Chain {
pub name: String, pub name: String,
} }
impl From<TychoCoreChain> for Chain { impl From<TychoCommonChain> for Chain {
fn from(chain: TychoCoreChain) -> Self { fn from(chain: TychoCommonChain) -> Self {
match chain { match chain {
TychoCoreChain::Ethereum => Chain { id: 1, name: chain.to_string() }, TychoCommonChain::Ethereum => Chain { id: 1, name: chain.to_string() },
TychoCoreChain::ZkSync => Chain { id: 324, name: chain.to_string() }, TychoCommonChain::ZkSync => Chain { id: 324, name: chain.to_string() },
TychoCoreChain::Arbitrum => Chain { id: 42161, name: chain.to_string() }, TychoCommonChain::Arbitrum => Chain { id: 42161, name: chain.to_string() },
TychoCoreChain::Starknet => Chain { id: 0, name: chain.to_string() }, TychoCommonChain::Starknet => Chain { id: 0, name: chain.to_string() },
TychoCoreChain::Base => Chain { id: 8453, name: chain.to_string() }, TychoCommonChain::Base => Chain { id: 8453, name: chain.to_string() },
TychoCoreChain::Unichain => Chain { id: 130, name: chain.to_string() }, TychoCommonChain::Unichain => Chain { id: 130, name: chain.to_string() },
} }
} }
} }

View File

@@ -14,8 +14,7 @@ pub trait TychoEncoder {
/// ///
/// # Returns /// # Returns
/// * `Result<Vec<Transaction>, EncodingError>` - Vector of executable transactions /// * `Result<Vec<Transaction>, EncodingError>` - Vector of executable transactions
fn encode_router_calldata( fn encode_calldata(&self, solutions: Vec<Solution>) -> Result<Vec<Transaction>, EncodingError>;
&self,
solutions: Vec<Solution>, fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError>;
) -> Result<Vec<Transaction>, EncodingError>;
} }