Merge pull request #120 from propeller-heads/router/tnl/ENG-4317-sequential-in
feat: Add sequential swap methods
This commit is contained in:
@@ -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:];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,6 +222,112 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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. This function performs a transferFrom to retrieve tokens from the caller.
|
||||||
|
*
|
||||||
|
* @dev
|
||||||
|
* - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token.
|
||||||
|
* - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver.
|
||||||
|
* - Swaps are executed sequentially using the `_swap` function.
|
||||||
|
* - A fee is deducted from the output token if `fee > 0`, and the remaining amount is sent to the receiver.
|
||||||
|
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater than 0.
|
||||||
|
*
|
||||||
|
* @param amountIn The input token amount to be swapped.
|
||||||
|
* @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, after deducting fees if applicable.
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||||
|
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.
|
||||||
|
* - A fee is deducted from the output token if `fee > 0`, and the remaining amount is sent to the receiver.
|
||||||
|
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is greater than 0.
|
||||||
|
*
|
||||||
|
* @param amountIn The input token amount to be swapped.
|
||||||
|
* @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, after deducting fees if applicable.
|
||||||
|
*/
|
||||||
|
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, transfer and handle approval.
|
||||||
|
if (tokenIn != address(0)) {
|
||||||
|
permit2.permit(msg.sender, permitSingle, signature);
|
||||||
|
permit2.transferFrom(
|
||||||
|
msg.sender,
|
||||||
|
address(this),
|
||||||
|
uint160(amountIn),
|
||||||
|
permitSingle.details.token
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _sequentialSwapChecked(
|
||||||
|
amountIn,
|
||||||
|
tokenIn,
|
||||||
|
tokenOut,
|
||||||
|
minAmountOut,
|
||||||
|
wrapEth,
|
||||||
|
unwrapEth,
|
||||||
|
receiver,
|
||||||
|
swaps
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @notice Executes a single swap operation.
|
* @notice Executes a single swap operation.
|
||||||
* This function enables optional ETH wrapping/unwrapping, and validates the output amount against a user-specified minimum.
|
* This function enables optional ETH wrapping/unwrapping, and validates the output amount against a user-specified minimum.
|
||||||
@@ -239,7 +345,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
* @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 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 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 unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver.
|
||||||
* @param nTokens The total number of tokens involved in the swap graph (used to initialize arrays for internal calculations).
|
|
||||||
* @param receiver The address to receive the output tokens.
|
* @param receiver The address to receive the output tokens.
|
||||||
* @param swapData Encoded swap details.
|
* @param swapData Encoded swap details.
|
||||||
*
|
*
|
||||||
@@ -252,7 +357,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
uint256 minAmountOut,
|
uint256 minAmountOut,
|
||||||
bool wrapEth,
|
bool wrapEth,
|
||||||
bool unwrapEth,
|
bool unwrapEth,
|
||||||
uint256 nTokens,
|
|
||||||
address receiver,
|
address receiver,
|
||||||
bytes calldata swapData
|
bytes calldata swapData
|
||||||
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
|
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
|
||||||
@@ -264,7 +368,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
minAmountOut,
|
minAmountOut,
|
||||||
wrapEth,
|
wrapEth,
|
||||||
unwrapEth,
|
unwrapEth,
|
||||||
nTokens,
|
|
||||||
receiver,
|
receiver,
|
||||||
swapData
|
swapData
|
||||||
);
|
);
|
||||||
@@ -288,7 +391,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
* @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 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 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 unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver.
|
||||||
* @param nTokens The total number of tokens involved in the swap graph (used to initialize arrays for internal calculations).
|
|
||||||
* @param receiver The address to receive the output tokens.
|
* @param 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 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 signature A valid signature authorizing the Permit2 approval. Ignored if `wrapEth` is true.
|
||||||
@@ -303,7 +405,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
uint256 minAmountOut,
|
uint256 minAmountOut,
|
||||||
bool wrapEth,
|
bool wrapEth,
|
||||||
bool unwrapEth,
|
bool unwrapEth,
|
||||||
uint256 nTokens,
|
|
||||||
address receiver,
|
address receiver,
|
||||||
IAllowanceTransfer.PermitSingle calldata permitSingle,
|
IAllowanceTransfer.PermitSingle calldata permitSingle,
|
||||||
bytes calldata signature,
|
bytes calldata signature,
|
||||||
@@ -327,7 +428,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
minAmountOut,
|
minAmountOut,
|
||||||
wrapEth,
|
wrapEth,
|
||||||
unwrapEth,
|
unwrapEth,
|
||||||
nTokens,
|
|
||||||
receiver,
|
receiver,
|
||||||
swapData
|
swapData
|
||||||
);
|
);
|
||||||
@@ -417,7 +517,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
uint256 minAmountOut,
|
uint256 minAmountOut,
|
||||||
bool wrapEth,
|
bool wrapEth,
|
||||||
bool unwrapEth,
|
bool unwrapEth,
|
||||||
uint256 nTokens,
|
|
||||||
address receiver,
|
address receiver,
|
||||||
bytes calldata swap_
|
bytes calldata swap_
|
||||||
) internal returns (uint256 amountOut) {
|
) internal returns (uint256 amountOut) {
|
||||||
@@ -438,8 +537,10 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
? address(this).balance
|
? address(this).balance
|
||||||
: IERC20(tokenIn).balanceOf(address(this));
|
: IERC20(tokenIn).balanceOf(address(this));
|
||||||
|
|
||||||
amountOut =
|
(address executor, bytes calldata protocolData) =
|
||||||
_callExecutor(swap_.executor(), amountIn, swap_.protocolData());
|
swap_.decodeSingleSwap();
|
||||||
|
|
||||||
|
amountOut = _callExecutor(executor, amountIn, protocolData);
|
||||||
uint256 currentBalance = tokenIn == address(0)
|
uint256 currentBalance = tokenIn == address(0)
|
||||||
? address(this).balance
|
? address(this).balance
|
||||||
: IERC20(tokenIn).balanceOf(address(this));
|
: IERC20(tokenIn).balanceOf(address(this));
|
||||||
@@ -472,6 +573,75 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 initialBalance = tokenIn == address(0)
|
||||||
|
? address(this).balance
|
||||||
|
: IERC20(tokenIn).balanceOf(address(this));
|
||||||
|
|
||||||
|
amountOut = _sequentialSwap(amountIn, 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) {
|
||||||
|
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unwrapEth) {
|
||||||
|
_unwrapETH(amountOut);
|
||||||
|
}
|
||||||
|
if (tokenOut == address(0)) {
|
||||||
|
Address.sendValue(payable(receiver), amountOut);
|
||||||
|
} else {
|
||||||
|
IERC20(tokenOut).safeTransfer(receiver, amountOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Executes sequential swaps as defined by the provided swap graph.
|
* @dev Executes sequential swaps as defined by the provided swap graph.
|
||||||
*
|
*
|
||||||
@@ -509,6 +679,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);
|
||||||
@@ -519,17 +691,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;
|
||||||
@@ -542,6 +713,31 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,921 +206,10 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
|||||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testSplitSwapSimple() public {
|
|
||||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
|
||||||
// 1 WETH -> DAI
|
|
||||||
// (USV2)
|
|
||||||
uint256 amountIn = 1 ether;
|
|
||||||
deal(WETH_ADDR, tychoRouterAddr, amountIn);
|
|
||||||
|
|
||||||
bytes memory protocolData = encodeUniswapV2Swap(
|
|
||||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
|
||||||
);
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr);
|
|
||||||
assertEq(daiBalance, 2659881924818443699787);
|
|
||||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSplitSwapSimplePermit2() 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, amountIn);
|
|
||||||
|
|
||||||
bytes memory protocolData = encodeUniswapV2Swap(
|
|
||||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory swap = encodeSplitSwap(
|
|
||||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
|
||||||
);
|
|
||||||
bytes[] memory swaps = new bytes[](1);
|
|
||||||
swaps[0] = swap;
|
|
||||||
|
|
||||||
tychoRouter.splitSwapPermit2(
|
|
||||||
amountIn,
|
|
||||||
WETH_ADDR,
|
|
||||||
DAI_ADDR,
|
|
||||||
2659881924818443699786,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
2,
|
|
||||||
ALICE,
|
|
||||||
permitSingle,
|
|
||||||
signature,
|
|
||||||
pleEncode(swaps)
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
|
||||||
assertEq(daiBalance, 2659881924818443699787);
|
|
||||||
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0);
|
|
||||||
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSplitSwapMultipleHops() public {
|
|
||||||
// Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2
|
|
||||||
// 1 WETH -> DAI -> USDC
|
|
||||||
// (univ2) (univ2)
|
|
||||||
uint256 amountIn = 1 ether;
|
|
||||||
deal(WETH_ADDR, tychoRouterAddr, amountIn);
|
|
||||||
|
|
||||||
bytes[] memory swaps = new bytes[](2);
|
|
||||||
// WETH -> DAI
|
|
||||||
swaps[0] = encodeSplitSwap(
|
|
||||||
uint8(0),
|
|
||||||
uint8(1),
|
|
||||||
uint24(0),
|
|
||||||
address(usv2Executor),
|
|
||||||
encodeUniswapV2Swap(
|
|
||||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// DAI -> USDC
|
|
||||||
swaps[1] = encodeSplitSwap(
|
|
||||||
uint8(1),
|
|
||||||
uint8(2),
|
|
||||||
uint24(0),
|
|
||||||
address(usv2Executor),
|
|
||||||
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true)
|
|
||||||
);
|
|
||||||
|
|
||||||
tychoRouter.exposedSplitSwap(amountIn, 3, pleEncode(swaps));
|
|
||||||
|
|
||||||
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr);
|
|
||||||
assertEq(usdcBalance, 2644659787);
|
|
||||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSplitSwapSplitHops() public {
|
|
||||||
// Trade 1 WETH for USDC through DAI and WBTC with 4 swaps on Uniswap V2
|
|
||||||
// -> DAI ->
|
|
||||||
// 1 WETH USDC
|
|
||||||
// -> WBTC ->
|
|
||||||
// (univ2) (univ2)
|
|
||||||
uint256 amountIn = 1 ether;
|
|
||||||
deal(WETH_ADDR, tychoRouterAddr, amountIn);
|
|
||||||
|
|
||||||
bytes[] memory swaps = new bytes[](4);
|
|
||||||
// WETH -> WBTC (60%)
|
|
||||||
swaps[0] = encodeSplitSwap(
|
|
||||||
uint8(0),
|
|
||||||
uint8(1),
|
|
||||||
(0xffffff * 60) / 100, // 60%
|
|
||||||
address(usv2Executor),
|
|
||||||
encodeUniswapV2Swap(
|
|
||||||
WETH_ADDR, WETH_WBTC_POOL, tychoRouterAddr, false
|
|
||||||
)
|
|
||||||
);
|
|
||||||
// WBTC -> USDC
|
|
||||||
swaps[1] = encodeSplitSwap(
|
|
||||||
uint8(1),
|
|
||||||
uint8(2),
|
|
||||||
uint24(0),
|
|
||||||
address(usv2Executor),
|
|
||||||
encodeUniswapV2Swap(
|
|
||||||
WBTC_ADDR, USDC_WBTC_POOL, tychoRouterAddr, true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
// WETH -> DAI
|
|
||||||
swaps[2] = encodeSplitSwap(
|
|
||||||
uint8(0),
|
|
||||||
uint8(3),
|
|
||||||
uint24(0),
|
|
||||||
address(usv2Executor),
|
|
||||||
encodeUniswapV2Swap(
|
|
||||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// DAI -> USDC
|
|
||||||
swaps[3] = encodeSplitSwap(
|
|
||||||
uint8(3),
|
|
||||||
uint8(2),
|
|
||||||
uint24(0),
|
|
||||||
address(usv2Executor),
|
|
||||||
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true)
|
|
||||||
);
|
|
||||||
|
|
||||||
tychoRouter.exposedSplitSwap(amountIn, 4, pleEncode(swaps));
|
|
||||||
|
|
||||||
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr);
|
|
||||||
assertEq(usdcBalance, 2615491639);
|
|
||||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSingleSwapChecked() public {
|
|
||||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
|
||||||
// Does permit2 token approval and transfer
|
|
||||||
// Checks amount out at the end
|
|
||||||
uint256 amountIn = 1 ether;
|
|
||||||
deal(WETH_ADDR, ALICE, amountIn);
|
|
||||||
|
|
||||||
vm.startPrank(ALICE);
|
|
||||||
|
|
||||||
(
|
|
||||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
|
||||||
bytes memory signature
|
|
||||||
) = handlePermit2Approval(WETH_ADDR, amountIn);
|
|
||||||
|
|
||||||
bytes memory protocolData = encodeUniswapV2Swap(
|
|
||||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory swap = encodeSplitSwap(
|
|
||||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 minAmountOut = 2600 * 1e18;
|
|
||||||
uint256 amountOut = tychoRouter.singleSwapPermit2(
|
|
||||||
amountIn,
|
|
||||||
WETH_ADDR,
|
|
||||||
DAI_ADDR,
|
|
||||||
minAmountOut,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
2,
|
|
||||||
ALICE,
|
|
||||||
permitSingle,
|
|
||||||
signature,
|
|
||||||
swap
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 expectedAmount = 2659881924818443699787;
|
|
||||||
assertEq(amountOut, expectedAmount);
|
|
||||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
|
||||||
assertEq(daiBalance, expectedAmount);
|
|
||||||
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0);
|
|
||||||
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSplitSwapCheckedUndefinedMinAmount() 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);
|
|
||||||
(
|
|
||||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
|
||||||
bytes memory signature
|
|
||||||
) = handlePermit2Approval(WETH_ADDR, amountIn);
|
|
||||||
|
|
||||||
bytes memory protocolData = encodeUniswapV2Swap(
|
|
||||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory swap = encodeSplitSwap(
|
|
||||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
|
||||||
);
|
|
||||||
bytes[] memory swaps = new bytes[](1);
|
|
||||||
swaps[0] = swap;
|
|
||||||
uint256 minAmountOut = 0;
|
|
||||||
|
|
||||||
vm.expectRevert(TychoRouter__UndefinedMinAmountOut.selector);
|
|
||||||
tychoRouter.splitSwapPermit2(
|
|
||||||
amountIn,
|
|
||||||
WETH_ADDR,
|
|
||||||
DAI_ADDR,
|
|
||||||
minAmountOut,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
2,
|
|
||||||
ALICE,
|
|
||||||
permitSingle,
|
|
||||||
signature,
|
|
||||||
pleEncode(swaps)
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSingleSwapCheckedNoPermit2() 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, tychoRouterAddr, false
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory swap = encodeSplitSwap(
|
|
||||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 minAmountOut = 2600 * 1e18;
|
|
||||||
uint256 amountOut = tychoRouter.singleSwap(
|
|
||||||
amountIn,
|
|
||||||
WETH_ADDR,
|
|
||||||
DAI_ADDR,
|
|
||||||
minAmountOut,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
2,
|
|
||||||
ALICE,
|
|
||||||
swap
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 expectedAmount = 2659881924818443699787;
|
|
||||||
assertEq(amountOut, expectedAmount);
|
|
||||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
|
||||||
assertEq(daiBalance, expectedAmount);
|
|
||||||
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 0);
|
|
||||||
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSplitSwapCheckedLessApprovalFailure() public {
|
|
||||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
|
||||||
// Fails while transferring the tokenIn to the router due to insufficient approval
|
|
||||||
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 protocolData = encodeUniswapV2Swap(
|
|
||||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory swap = encodeSplitSwap(
|
|
||||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
|
||||||
);
|
|
||||||
bytes[] memory swaps = new bytes[](1);
|
|
||||||
swaps[0] = swap;
|
|
||||||
|
|
||||||
uint256 minAmountOut = 2600 * 1e18;
|
|
||||||
vm.expectRevert();
|
|
||||||
tychoRouter.splitSwap(
|
|
||||||
amountIn,
|
|
||||||
WETH_ADDR,
|
|
||||||
DAI_ADDR,
|
|
||||||
minAmountOut,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
2,
|
|
||||||
ALICE,
|
|
||||||
pleEncode(swaps)
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSplitSwapCheckedNegativeSlippageFailure() public {
|
|
||||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
|
||||||
// Does permit2 token approval and transfer
|
|
||||||
// Checks amount out at the end and fails
|
|
||||||
uint256 amountIn = 1 ether;
|
|
||||||
deal(WETH_ADDR, ALICE, amountIn);
|
|
||||||
|
|
||||||
vm.startPrank(ALICE);
|
|
||||||
|
|
||||||
(
|
|
||||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
|
||||||
bytes memory signature
|
|
||||||
) = handlePermit2Approval(WETH_ADDR, amountIn);
|
|
||||||
|
|
||||||
bytes memory protocolData = encodeUniswapV2Swap(
|
|
||||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory swap = encodeSplitSwap(
|
|
||||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
|
||||||
);
|
|
||||||
bytes[] memory swaps = new bytes[](1);
|
|
||||||
swaps[0] = swap;
|
|
||||||
|
|
||||||
uint256 minAmountOut = 3000 * 1e18;
|
|
||||||
vm.expectRevert(
|
|
||||||
abi.encodeWithSelector(
|
|
||||||
TychoRouter__NegativeSlippage.selector,
|
|
||||||
2659881924818443699787, // actual amountOut
|
|
||||||
minAmountOut
|
|
||||||
)
|
|
||||||
);
|
|
||||||
tychoRouter.splitSwapPermit2(
|
|
||||||
amountIn,
|
|
||||||
WETH_ADDR,
|
|
||||||
DAI_ADDR,
|
|
||||||
minAmountOut,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
2,
|
|
||||||
ALICE,
|
|
||||||
permitSingle,
|
|
||||||
signature,
|
|
||||||
pleEncode(swaps)
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSplitSwapFee() public {
|
|
||||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
|
||||||
// Does permit2 token approval and transfer
|
|
||||||
// Takes fee at the end
|
|
||||||
|
|
||||||
vm.startPrank(FEE_SETTER);
|
|
||||||
tychoRouter.setFee(100);
|
|
||||||
tychoRouter.setFeeReceiver(FEE_RECEIVER);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
uint256 amountIn = 1 ether;
|
|
||||||
deal(WETH_ADDR, ALICE, amountIn);
|
|
||||||
|
|
||||||
vm.startPrank(ALICE);
|
|
||||||
|
|
||||||
(
|
|
||||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
|
||||||
bytes memory signature
|
|
||||||
) = handlePermit2Approval(WETH_ADDR, amountIn);
|
|
||||||
|
|
||||||
bytes memory protocolData = encodeUniswapV2Swap(
|
|
||||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory swap = encodeSplitSwap(
|
|
||||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
|
||||||
);
|
|
||||||
bytes[] memory swaps = new bytes[](1);
|
|
||||||
swaps[0] = swap;
|
|
||||||
|
|
||||||
uint256 amountOut = tychoRouter.splitSwapPermit2(
|
|
||||||
amountIn,
|
|
||||||
WETH_ADDR,
|
|
||||||
DAI_ADDR,
|
|
||||||
2633283105570259262780,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
2,
|
|
||||||
ALICE,
|
|
||||||
permitSingle,
|
|
||||||
signature,
|
|
||||||
pleEncode(swaps)
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 expectedAmount = 2633283105570259262790;
|
|
||||||
assertEq(amountOut, expectedAmount);
|
|
||||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
|
||||||
assertEq(daiBalance, expectedAmount);
|
|
||||||
assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26598819248184436997);
|
|
||||||
|
|
||||||
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, tychoRouterAddr, false
|
|
||||||
);
|
|
||||||
|
|
||||||
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,
|
|
||||||
2659881924818443699780,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
2,
|
|
||||||
ALICE,
|
|
||||||
emptyPermitSingle,
|
|
||||||
"",
|
|
||||||
pleEncode(swaps)
|
|
||||||
);
|
|
||||||
uint256 expectedAmount = 2659881924818443699787;
|
|
||||||
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, amountIn);
|
|
||||||
|
|
||||||
bytes memory protocolData =
|
|
||||||
encodeUniswapV2Swap(DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true);
|
|
||||||
|
|
||||||
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),
|
|
||||||
1120007305574805920,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
2,
|
|
||||||
ALICE,
|
|
||||||
permitSingle,
|
|
||||||
signature,
|
|
||||||
pleEncode(swaps)
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 expectedAmount = 1120007305574805922; // 1.12 ETH
|
|
||||||
assertEq(amountOut, expectedAmount);
|
|
||||||
assertEq(ALICE.balance, expectedAmount);
|
|
||||||
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSplitSwapSingleUSV3() public {
|
|
||||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V3
|
|
||||||
// 1 WETH -> DAI
|
|
||||||
// (USV3)
|
|
||||||
uint256 amountIn = 10 ** 18;
|
|
||||||
deal(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, tychoRouterAddr, DAI_WETH_USV3, zeroForOne
|
|
||||||
);
|
|
||||||
bytes memory swap = encodeSplitSwap(
|
|
||||||
uint8(0), uint8(1), uint24(0), address(usv3Executor), protocolData
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes[] memory swaps = new bytes[](1);
|
|
||||||
swaps[0] = swap;
|
|
||||||
|
|
||||||
tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps));
|
|
||||||
|
|
||||||
uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr);
|
|
||||||
assertGe(finalBalance, expAmountOut);
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSplitSwapSingleUSV3Permit2() public {
|
|
||||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V3 using Permit2
|
|
||||||
// 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, amountIn);
|
|
||||||
|
|
||||||
uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI
|
|
||||||
bool zeroForOne = false;
|
|
||||||
bytes memory protocolData = encodeUniswapV3Swap(
|
|
||||||
WETH_ADDR, DAI_ADDR, tychoRouterAddr, DAI_WETH_USV3, zeroForOne
|
|
||||||
);
|
|
||||||
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 {
|
function testEmptySwapsRevert() public {
|
||||||
uint256 amountIn = 10 ** 18;
|
uint256 amountIn = 10 ** 18;
|
||||||
bytes memory swaps = "";
|
bytes memory swaps = "";
|
||||||
vm.expectRevert(TychoRouter__EmptySwaps.selector);
|
vm.expectRevert(TychoRouter__EmptySwaps.selector);
|
||||||
tychoRouter.exposedSplitSwap(amountIn, 2, swaps);
|
tychoRouter.exposedSplitSwap(amountIn, 2, swaps);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testSplitSwapAmountInNotFullySpent() public {
|
|
||||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
|
||||||
// Has invalid data as input! There is only one swap with 60% of the input amount
|
|
||||||
uint256 amountIn = 1 ether;
|
|
||||||
deal(WETH_ADDR, ALICE, amountIn);
|
|
||||||
|
|
||||||
vm.startPrank(ALICE);
|
|
||||||
|
|
||||||
(
|
|
||||||
IAllowanceTransfer.PermitSingle memory permitSingle,
|
|
||||||
bytes memory signature
|
|
||||||
) = handlePermit2Approval(WETH_ADDR, amountIn);
|
|
||||||
|
|
||||||
bytes memory protocolData = encodeUniswapV2Swap(
|
|
||||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory swap = encodeSplitSwap(
|
|
||||||
uint8(0),
|
|
||||||
uint8(1),
|
|
||||||
(0xffffff * 60) / 100, // 60%
|
|
||||||
address(usv2Executor),
|
|
||||||
protocolData
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes[] memory swaps = new bytes[](1);
|
|
||||||
swaps[0] = swap;
|
|
||||||
|
|
||||||
vm.expectRevert(
|
|
||||||
abi.encodeWithSelector(
|
|
||||||
TychoRouter__AmountInDiffersFromConsumed.selector,
|
|
||||||
1000000000000000000,
|
|
||||||
600000000000000000
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
tychoRouter.splitSwapPermit2(
|
|
||||||
amountIn,
|
|
||||||
WETH_ADDR,
|
|
||||||
DAI_ADDR,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
2,
|
|
||||||
ALICE,
|
|
||||||
permitSingle,
|
|
||||||
signature,
|
|
||||||
pleEncode(swaps)
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSplitSwapSingleUSV4Callback() public {
|
|
||||||
uint256 amountIn = 100 ether;
|
|
||||||
deal(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, address(usv4Executor), 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(USDT_ADDR).balanceOf(tychoRouterAddr), 99943852);
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 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, address(usv4Executor), 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), 99943852);
|
|
||||||
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, address(usv4Executor), 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(tychoRouterAddr), 102718);
|
|
||||||
}
|
|
||||||
|
|
||||||
function testCyclicSequentialSwap() public {
|
|
||||||
// This test has start and end tokens that are the same
|
|
||||||
// The flow is:
|
|
||||||
// USDC -> WETH -> USDC using two pools
|
|
||||||
uint256 amountIn = 100 * 10 ** 6;
|
|
||||||
deal(USDC_ADDR, tychoRouterAddr, amountIn);
|
|
||||||
|
|
||||||
bytes memory usdcWethV3Pool1ZeroOneData = encodeUniswapV3Swap(
|
|
||||||
USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3, true
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap(
|
|
||||||
WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes[] memory swaps = new bytes[](2);
|
|
||||||
// USDC -> WETH
|
|
||||||
swaps[0] = encodeSplitSwap(
|
|
||||||
uint8(0),
|
|
||||||
uint8(1),
|
|
||||||
uint24(0),
|
|
||||||
address(usv3Executor),
|
|
||||||
usdcWethV3Pool1ZeroOneData
|
|
||||||
);
|
|
||||||
// WETH -> USDC
|
|
||||||
swaps[1] = encodeSplitSwap(
|
|
||||||
uint8(1),
|
|
||||||
uint8(0),
|
|
||||||
uint24(0),
|
|
||||||
address(usv3Executor),
|
|
||||||
usdcWethV3Pool2OneZeroData
|
|
||||||
);
|
|
||||||
|
|
||||||
tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps));
|
|
||||||
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99889294);
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSplitInputCyclicSwap() 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, tychoRouterAddr, amountIn);
|
|
||||||
|
|
||||||
bytes memory usdcWethV3Pool1ZeroOneData = encodeUniswapV3Swap(
|
|
||||||
USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3, true
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory usdcWethV3Pool2ZeroOneData = encodeUniswapV3Swap(
|
|
||||||
USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, true
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory wethUsdcV2OneZeroData = encodeUniswapV2Swap(
|
|
||||||
WETH_ADDR, USDC_WETH_USV2, tychoRouterAddr, false
|
|
||||||
);
|
|
||||||
|
|
||||||
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));
|
|
||||||
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99574171);
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSplitOutputCyclicSwap() 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
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory usdcWethV3Pool1OneZeroData = encodeUniswapV3Swap(
|
|
||||||
WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3, false
|
|
||||||
);
|
|
||||||
|
|
||||||
bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap(
|
|
||||||
WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false
|
|
||||||
);
|
|
||||||
|
|
||||||
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), 99525908);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base Network Tests
|
|
||||||
// Make sure to set the RPC_URL to base network
|
|
||||||
function testSplitSwapSingleBase() 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
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
344
foundry/test/TychoRouterSequentialSwap.t.sol
Normal file
344
foundry/test/TychoRouterSequentialSwap.t.sol
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
// 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 {
|
||||||
|
bytes32 public constant FEE_SETTER_ROLE =
|
||||||
|
0xe6ad9a47fbda1dc18de1eb5eeb7d935e5e81b4748f3cfc61e233e64f88182060;
|
||||||
|
|
||||||
|
function _getSequentialSwaps() internal view returns (bytes[] memory) {
|
||||||
|
// Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2
|
||||||
|
// 1 WETH -> DAI -> USDC
|
||||||
|
// (univ2) (univ2)
|
||||||
|
bytes[] memory swaps = new bytes[](2);
|
||||||
|
// WETH -> DAI
|
||||||
|
swaps[0] = encodeSequentialSwap(
|
||||||
|
address(usv2Executor),
|
||||||
|
encodeUniswapV2Swap(
|
||||||
|
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// DAI -> USDC
|
||||||
|
swaps[1] = encodeSequentialSwap(
|
||||||
|
address(usv2Executor),
|
||||||
|
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true)
|
||||||
|
);
|
||||||
|
return swaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSequentialSwapInternalMethod() public {
|
||||||
|
// Trade 1 WETH for USDC through DAI - see _getSequentialSwaps for more info
|
||||||
|
uint256 amountIn = 1 ether;
|
||||||
|
deal(WETH_ADDR, tychoRouterAddr, amountIn);
|
||||||
|
|
||||||
|
bytes[] memory swaps = _getSequentialSwaps();
|
||||||
|
tychoRouter.exposedSequentialSwap(amountIn, pleEncode(swaps));
|
||||||
|
|
||||||
|
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr);
|
||||||
|
assertEq(usdcBalance, 2644659787);
|
||||||
|
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, amountIn);
|
||||||
|
|
||||||
|
bytes[] memory swaps = _getSequentialSwaps();
|
||||||
|
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, 2644659787);
|
||||||
|
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();
|
||||||
|
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, 2644659787);
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
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, amountIn);
|
||||||
|
|
||||||
|
bytes[] memory swaps = _getSequentialSwaps();
|
||||||
|
|
||||||
|
uint256 minAmountOut = 3000 * 1e18;
|
||||||
|
|
||||||
|
vm.expectRevert(
|
||||||
|
abi.encodeWithSelector(
|
||||||
|
TychoRouter__NegativeSlippage.selector,
|
||||||
|
2644659787, // actual amountOut
|
||||||
|
minAmountOut
|
||||||
|
)
|
||||||
|
);
|
||||||
|
tychoRouter.sequentialSwapPermit2(
|
||||||
|
amountIn,
|
||||||
|
WETH_ADDR,
|
||||||
|
DAI_ADDR,
|
||||||
|
minAmountOut,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
ALICE,
|
||||||
|
permitSingle,
|
||||||
|
signature,
|
||||||
|
pleEncode(swaps)
|
||||||
|
);
|
||||||
|
vm.stopPrank();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSequentialSwapFee() public {
|
||||||
|
// Trade 1 WETH for USDC
|
||||||
|
// Takes 1% fee at the end
|
||||||
|
|
||||||
|
vm.startPrank(FEE_SETTER);
|
||||||
|
tychoRouter.setFee(100);
|
||||||
|
tychoRouter.setFeeReceiver(FEE_RECEIVER);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
uint256 amountIn = 1 ether;
|
||||||
|
deal(WETH_ADDR, ALICE, amountIn);
|
||||||
|
|
||||||
|
vm.startPrank(ALICE);
|
||||||
|
|
||||||
|
(
|
||||||
|
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||||
|
bytes memory signature
|
||||||
|
) = handlePermit2Approval(WETH_ADDR, amountIn);
|
||||||
|
|
||||||
|
bytes[] memory swaps = _getSequentialSwaps();
|
||||||
|
|
||||||
|
uint256 amountOut = tychoRouter.sequentialSwapPermit2(
|
||||||
|
amountIn,
|
||||||
|
WETH_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
1000_000000,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
ALICE,
|
||||||
|
permitSingle,
|
||||||
|
signature,
|
||||||
|
pleEncode(swaps)
|
||||||
|
);
|
||||||
|
|
||||||
|
uint256 expectedAmount = 2618213190;
|
||||||
|
assertEq(amountOut, expectedAmount);
|
||||||
|
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(ALICE);
|
||||||
|
assertEq(usdcBalance, expectedAmount);
|
||||||
|
assertEq(IERC20(USDC_ADDR).balanceOf(FEE_RECEIVER), 26446597);
|
||||||
|
|
||||||
|
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 = _getSequentialSwaps();
|
||||||
|
|
||||||
|
uint256 amountOut = tychoRouter.sequentialSwapPermit2{value: amountIn}(
|
||||||
|
amountIn,
|
||||||
|
address(0),
|
||||||
|
USDC_ADDR,
|
||||||
|
1000_000000,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
ALICE,
|
||||||
|
emptyPermitSingle,
|
||||||
|
"",
|
||||||
|
pleEncode(swaps)
|
||||||
|
);
|
||||||
|
uint256 expectedAmount = 2644659787;
|
||||||
|
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, amountIn);
|
||||||
|
|
||||||
|
bytes[] memory swaps = new bytes[](2);
|
||||||
|
|
||||||
|
// USDC -> DAI
|
||||||
|
swaps[0] = encodeSequentialSwap(
|
||||||
|
address(usv2Executor),
|
||||||
|
encodeUniswapV2Swap(
|
||||||
|
USDC_ADDR, DAI_USDC_POOL, tychoRouterAddr, false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// DAI -> WETH
|
||||||
|
swaps[1] = encodeSequentialSwap(
|
||||||
|
address(usv2Executor),
|
||||||
|
encodeUniswapV2Swap(DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
uint256 amountOut = tychoRouter.sequentialSwapPermit2(
|
||||||
|
amountIn,
|
||||||
|
USDC_ADDR,
|
||||||
|
address(0),
|
||||||
|
1 * 10 ** 18, // min amount
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
ALICE,
|
||||||
|
permitSingle,
|
||||||
|
signature,
|
||||||
|
pleEncode(swaps)
|
||||||
|
);
|
||||||
|
|
||||||
|
uint256 expectedAmount = 1111174255471849849; // 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
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap(
|
||||||
|
WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false
|
||||||
|
);
|
||||||
|
|
||||||
|
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), 99889294);
|
||||||
|
}
|
||||||
|
}
|
||||||
307
foundry/test/TychoRouterSingleSwap.t.sol
Normal file
307
foundry/test/TychoRouterSingleSwap.t.sol
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
// 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 {
|
||||||
|
bytes32 public constant FEE_SETTER_ROLE =
|
||||||
|
0xe6ad9a47fbda1dc18de1eb5eeb7d935e5e81b4748f3cfc61e233e64f88182060;
|
||||||
|
|
||||||
|
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, amountIn);
|
||||||
|
|
||||||
|
bytes memory protocolData = encodeUniswapV2Swap(
|
||||||
|
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes memory swap =
|
||||||
|
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||||
|
|
||||||
|
tychoRouter.singleSwapPermit2(
|
||||||
|
amountIn,
|
||||||
|
WETH_ADDR,
|
||||||
|
DAI_ADDR,
|
||||||
|
2659881924818443699786,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
ALICE,
|
||||||
|
permitSingle,
|
||||||
|
signature,
|
||||||
|
swap
|
||||||
|
);
|
||||||
|
|
||||||
|
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||||
|
assertEq(daiBalance, 2659881924818443699787);
|
||||||
|
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, tychoRouterAddr, false
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes memory swap =
|
||||||
|
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||||
|
|
||||||
|
uint256 minAmountOut = 2600 * 1e18;
|
||||||
|
uint256 amountOut = tychoRouter.singleSwap(
|
||||||
|
amountIn,
|
||||||
|
WETH_ADDR,
|
||||||
|
DAI_ADDR,
|
||||||
|
minAmountOut,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
ALICE,
|
||||||
|
swap
|
||||||
|
);
|
||||||
|
|
||||||
|
uint256 expectedAmount = 2659881924818443699787;
|
||||||
|
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, tychoRouterAddr, false
|
||||||
|
);
|
||||||
|
|
||||||
|
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, tychoRouterAddr, false
|
||||||
|
);
|
||||||
|
|
||||||
|
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, tychoRouterAddr, false
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes memory swap =
|
||||||
|
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||||
|
|
||||||
|
uint256 minAmountOut = 5600 * 1e18;
|
||||||
|
|
||||||
|
vm.expectRevert(
|
||||||
|
abi.encodeWithSelector(
|
||||||
|
TychoRouter__NegativeSlippage.selector,
|
||||||
|
2659881924818443699787, // actual amountOut
|
||||||
|
minAmountOut
|
||||||
|
)
|
||||||
|
);
|
||||||
|
tychoRouter.singleSwap(
|
||||||
|
amountIn,
|
||||||
|
WETH_ADDR,
|
||||||
|
DAI_ADDR,
|
||||||
|
minAmountOut,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
ALICE,
|
||||||
|
swap
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSingleSwapFee() public {
|
||||||
|
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
||||||
|
// Takes 1% fee at the end
|
||||||
|
|
||||||
|
vm.startPrank(FEE_SETTER);
|
||||||
|
tychoRouter.setFee(100);
|
||||||
|
tychoRouter.setFeeReceiver(FEE_RECEIVER);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
uint256 amountIn = 1 ether;
|
||||||
|
|
||||||
|
deal(WETH_ADDR, ALICE, amountIn);
|
||||||
|
vm.startPrank(ALICE);
|
||||||
|
// 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, tychoRouterAddr, false
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes memory swap =
|
||||||
|
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||||
|
|
||||||
|
uint256 minAmountOut = 2600 * 1e18;
|
||||||
|
uint256 amountOut = tychoRouter.singleSwap(
|
||||||
|
amountIn,
|
||||||
|
WETH_ADDR,
|
||||||
|
DAI_ADDR,
|
||||||
|
minAmountOut,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
ALICE,
|
||||||
|
swap
|
||||||
|
);
|
||||||
|
|
||||||
|
uint256 expectedAmount = 2633283105570259262790;
|
||||||
|
assertEq(amountOut, expectedAmount);
|
||||||
|
uint256 usdcBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||||
|
assertEq(usdcBalance, expectedAmount);
|
||||||
|
assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26598819248184436997);
|
||||||
|
|
||||||
|
vm.stopPrank();
|
||||||
|
}
|
||||||
|
|
||||||
|
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, tychoRouterAddr, false
|
||||||
|
);
|
||||||
|
|
||||||
|
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 = 2659881924818443699787;
|
||||||
|
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, amountIn);
|
||||||
|
|
||||||
|
bytes memory protocolData =
|
||||||
|
encodeUniswapV2Swap(DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true);
|
||||||
|
|
||||||
|
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 = 1120007305574805922;
|
||||||
|
assertEq(amountOut, expectedAmount);
|
||||||
|
assertEq(ALICE.balance, expectedAmount);
|
||||||
|
|
||||||
|
vm.stopPrank();
|
||||||
|
}
|
||||||
|
}
|
||||||
683
foundry/test/TychoRouterSplitSwap.t.sol
Normal file
683
foundry/test/TychoRouterSplitSwap.t.sol
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
// 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 {
|
||||||
|
bytes32 public constant FEE_SETTER_ROLE =
|
||||||
|
0xe6ad9a47fbda1dc18de1eb5eeb7d935e5e81b4748f3cfc61e233e64f88182060;
|
||||||
|
|
||||||
|
function _getSplitSwaps() 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);
|
||||||
|
// WETH -> WBTC (60%)
|
||||||
|
swaps[0] = encodeSplitSwap(
|
||||||
|
uint8(0),
|
||||||
|
uint8(1),
|
||||||
|
(0xffffff * 60) / 100, // 60%
|
||||||
|
address(usv2Executor),
|
||||||
|
encodeUniswapV2Swap(
|
||||||
|
WETH_ADDR, WETH_WBTC_POOL, tychoRouterAddr, false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// WBTC -> USDC
|
||||||
|
swaps[1] = encodeSplitSwap(
|
||||||
|
uint8(1),
|
||||||
|
uint8(2),
|
||||||
|
uint24(0),
|
||||||
|
address(usv2Executor),
|
||||||
|
encodeUniswapV2Swap(
|
||||||
|
WBTC_ADDR, USDC_WBTC_POOL, tychoRouterAddr, true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// WETH -> DAI
|
||||||
|
swaps[2] = encodeSplitSwap(
|
||||||
|
uint8(0),
|
||||||
|
uint8(3),
|
||||||
|
uint24(0),
|
||||||
|
address(usv2Executor),
|
||||||
|
encodeUniswapV2Swap(
|
||||||
|
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// DAI -> USDC
|
||||||
|
swaps[3] = encodeSplitSwap(
|
||||||
|
uint8(3),
|
||||||
|
uint8(2),
|
||||||
|
uint24(0),
|
||||||
|
address(usv2Executor),
|
||||||
|
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
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, tychoRouterAddr, amountIn);
|
||||||
|
bytes[] memory swaps = _getSplitSwaps();
|
||||||
|
tychoRouter.exposedSplitSwap(amountIn, 4, pleEncode(swaps));
|
||||||
|
|
||||||
|
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr);
|
||||||
|
assertEq(usdcBalance, 2615491639);
|
||||||
|
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, amountIn);
|
||||||
|
|
||||||
|
bytes[] memory swaps = _getSplitSwaps();
|
||||||
|
|
||||||
|
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, 2615491639);
|
||||||
|
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(address(tychoRouterAddr), amountIn);
|
||||||
|
|
||||||
|
bytes[] memory swaps = _getSplitSwaps();
|
||||||
|
|
||||||
|
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, 2615491639);
|
||||||
|
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 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();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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, amountIn);
|
||||||
|
|
||||||
|
bytes[] memory swaps = _getSplitSwaps();
|
||||||
|
|
||||||
|
uint256 minAmountOut = 3000 * 1e18;
|
||||||
|
|
||||||
|
vm.expectRevert(
|
||||||
|
abi.encodeWithSelector(
|
||||||
|
TychoRouter__NegativeSlippage.selector,
|
||||||
|
2615491639, // actual amountOut
|
||||||
|
minAmountOut
|
||||||
|
)
|
||||||
|
);
|
||||||
|
tychoRouter.splitSwapPermit2(
|
||||||
|
amountIn,
|
||||||
|
WETH_ADDR,
|
||||||
|
DAI_ADDR,
|
||||||
|
minAmountOut,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
4,
|
||||||
|
ALICE,
|
||||||
|
permitSingle,
|
||||||
|
signature,
|
||||||
|
pleEncode(swaps)
|
||||||
|
);
|
||||||
|
vm.stopPrank();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSplitSwapFee() public {
|
||||||
|
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
||||||
|
// Does permit2 token approval and transfer
|
||||||
|
// Takes fee at the end
|
||||||
|
|
||||||
|
vm.startPrank(FEE_SETTER);
|
||||||
|
tychoRouter.setFee(100);
|
||||||
|
tychoRouter.setFeeReceiver(FEE_RECEIVER);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
uint256 amountIn = 1 ether;
|
||||||
|
deal(WETH_ADDR, ALICE, amountIn);
|
||||||
|
|
||||||
|
vm.startPrank(ALICE);
|
||||||
|
|
||||||
|
(
|
||||||
|
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||||
|
bytes memory signature
|
||||||
|
) = handlePermit2Approval(WETH_ADDR, amountIn);
|
||||||
|
|
||||||
|
bytes memory protocolData = encodeUniswapV2Swap(
|
||||||
|
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes memory swap = encodeSplitSwap(
|
||||||
|
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
||||||
|
);
|
||||||
|
bytes[] memory swaps = new bytes[](1);
|
||||||
|
swaps[0] = swap;
|
||||||
|
|
||||||
|
uint256 amountOut = tychoRouter.splitSwapPermit2(
|
||||||
|
amountIn,
|
||||||
|
WETH_ADDR,
|
||||||
|
DAI_ADDR,
|
||||||
|
2633283105570259262780,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
2,
|
||||||
|
ALICE,
|
||||||
|
permitSingle,
|
||||||
|
signature,
|
||||||
|
pleEncode(swaps)
|
||||||
|
);
|
||||||
|
|
||||||
|
uint256 expectedAmount = 2633283105570259262790;
|
||||||
|
assertEq(amountOut, expectedAmount);
|
||||||
|
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||||
|
assertEq(daiBalance, expectedAmount);
|
||||||
|
assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26598819248184436997);
|
||||||
|
|
||||||
|
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, tychoRouterAddr, false
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
2659881924818443699780,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
2,
|
||||||
|
ALICE,
|
||||||
|
emptyPermitSingle,
|
||||||
|
"",
|
||||||
|
pleEncode(swaps)
|
||||||
|
);
|
||||||
|
uint256 expectedAmount = 2659881924818443699787;
|
||||||
|
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, amountIn);
|
||||||
|
|
||||||
|
bytes memory protocolData =
|
||||||
|
encodeUniswapV2Swap(DAI_ADDR, WETH_DAI_POOL, tychoRouterAddr, true);
|
||||||
|
|
||||||
|
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),
|
||||||
|
1120007305574805920,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
2,
|
||||||
|
ALICE,
|
||||||
|
permitSingle,
|
||||||
|
signature,
|
||||||
|
pleEncode(swaps)
|
||||||
|
);
|
||||||
|
|
||||||
|
uint256 expectedAmount = 1120007305574805922; // 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, amountIn);
|
||||||
|
|
||||||
|
uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI
|
||||||
|
bool zeroForOne = false;
|
||||||
|
bytes memory protocolData = encodeUniswapV3Swap(
|
||||||
|
WETH_ADDR, DAI_ADDR, tychoRouterAddr, DAI_WETH_USV3, zeroForOne
|
||||||
|
);
|
||||||
|
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 testSplitSwapAmountInNotFullySpent() public {
|
||||||
|
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
||||||
|
// Has invalid data as input! There is only one swap with 60% of the input amount
|
||||||
|
uint256 amountIn = 1 ether;
|
||||||
|
deal(WETH_ADDR, ALICE, amountIn);
|
||||||
|
|
||||||
|
vm.startPrank(ALICE);
|
||||||
|
|
||||||
|
(
|
||||||
|
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||||
|
bytes memory signature
|
||||||
|
) = handlePermit2Approval(WETH_ADDR, amountIn);
|
||||||
|
|
||||||
|
bytes memory protocolData = encodeUniswapV2Swap(
|
||||||
|
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes memory swap = encodeSplitSwap(
|
||||||
|
uint8(0),
|
||||||
|
uint8(1),
|
||||||
|
(0xffffff * 60) / 100, // 60%
|
||||||
|
address(usv2Executor),
|
||||||
|
protocolData
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes[] memory swaps = new bytes[](1);
|
||||||
|
swaps[0] = swap;
|
||||||
|
|
||||||
|
vm.expectRevert(
|
||||||
|
abi.encodeWithSelector(
|
||||||
|
TychoRouter__AmountInDiffersFromConsumed.selector,
|
||||||
|
1000000000000000000,
|
||||||
|
600000000000000000
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
tychoRouter.splitSwapPermit2(
|
||||||
|
amountIn,
|
||||||
|
WETH_ADDR,
|
||||||
|
DAI_ADDR,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
2,
|
||||||
|
ALICE,
|
||||||
|
permitSingle,
|
||||||
|
signature,
|
||||||
|
pleEncode(swaps)
|
||||||
|
);
|
||||||
|
|
||||||
|
vm.stopPrank();
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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, address(usv4Executor), 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), 99943852);
|
||||||
|
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, address(usv4Executor), 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(tychoRouterAddr), 102718);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, tychoRouterAddr, amountIn);
|
||||||
|
|
||||||
|
bytes memory usdcWethV3Pool1ZeroOneData = encodeUniswapV3Swap(
|
||||||
|
USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3, true
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes memory usdcWethV3Pool2ZeroOneData = encodeUniswapV3Swap(
|
||||||
|
USDC_ADDR, WETH_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, true
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes memory wethUsdcV2OneZeroData = encodeUniswapV2Swap(
|
||||||
|
WETH_ADDR, USDC_WETH_USV2, tychoRouterAddr, false
|
||||||
|
);
|
||||||
|
|
||||||
|
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));
|
||||||
|
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99574171);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes memory usdcWethV3Pool1OneZeroData = encodeUniswapV3Swap(
|
||||||
|
WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3, false
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes memory usdcWethV3Pool2OneZeroData = encodeUniswapV3Swap(
|
||||||
|
WETH_ADDR, USDC_ADDR, tychoRouterAddr, USDC_WETH_USV3_2, false
|
||||||
|
);
|
||||||
|
|
||||||
|
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), 99525908);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,13 @@ contract TychoRouterExposed is TychoRouter {
|
|||||||
) external returns (uint256) {
|
) external returns (uint256) {
|
||||||
return _splitSwap(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 Constants {
|
contract TychoRouterTestSetup is Constants {
|
||||||
@@ -181,6 +188,22 @@ contract TychoRouterTestSetup is Constants {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
function encodeSplitSwap(
|
||||||
uint8 tokenInIndex,
|
uint8 tokenInIndex,
|
||||||
uint8 tokenOutIndex,
|
uint8 tokenOutIndex,
|
||||||
|
|||||||
Reference in New Issue
Block a user