feat: Decode single and sequential swaps in LibSwap
- The old way was useful when we just had split swaps. Unfortunately, we now have split, sequential, and single swaps, which don't always require token indices or split percentages, so we need to decode differently for each case.
This commit is contained in:
committed by
Diana Carvalho
parent
3ae9d3ad67
commit
1dad4afb6b
@@ -2,36 +2,42 @@
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
library LibSwap {
|
||||
/// Returns the InToken index into an array of tokens
|
||||
function tokenInIndex(
|
||||
bytes calldata swap
|
||||
) internal pure returns (uint8 res) {
|
||||
res = uint8(swap[0]);
|
||||
/**
|
||||
* @dev Returns arguments required to perform a single swap
|
||||
*/
|
||||
function decodeSingleSwap(bytes calldata swap)
|
||||
internal
|
||||
pure
|
||||
returns (address executor, bytes calldata protocolData)
|
||||
{
|
||||
executor = address(uint160(bytes20(swap[0:20])));
|
||||
protocolData = swap[20:];
|
||||
}
|
||||
|
||||
/// The OutToken index into an array of tokens
|
||||
function tokenOutIndex(
|
||||
bytes calldata swap
|
||||
) internal pure returns (uint8 res) {
|
||||
res = uint8(swap[1]);
|
||||
/**
|
||||
* @dev Returns arguments required to perform a sequential swap
|
||||
*/
|
||||
function decodeSequentialSwap(bytes calldata swap)
|
||||
internal
|
||||
pure
|
||||
returns (address executor, bytes calldata protocolData)
|
||||
{
|
||||
executor = address(uint160(bytes20(swap[0:20])));
|
||||
protocolData = swap[20:];
|
||||
}
|
||||
|
||||
/// The relative amount of token quantity routed into this swap
|
||||
function splitPercentage(
|
||||
bytes calldata swap
|
||||
) internal pure returns (uint24 res) {
|
||||
res = uint24(bytes3(swap[2:5]));
|
||||
}
|
||||
|
||||
/// The address of the executor contract
|
||||
function executor(bytes calldata swap) internal pure returns (address res) {
|
||||
res = address(uint160(bytes20(swap[5:25])));
|
||||
}
|
||||
|
||||
/// Remaining bytes are interpreted as protocol data
|
||||
function protocolData(
|
||||
bytes calldata swap
|
||||
) internal pure returns (bytes calldata res) {
|
||||
res = swap[25:];
|
||||
/**
|
||||
* @dev Returns arguments required to perform a split swap
|
||||
*/
|
||||
function decodeSplitSwap(bytes calldata swap)
|
||||
internal
|
||||
pure
|
||||
returns (uint8 tokenInIndex, uint8 tokenOutIndex, uint24 split, address executor, bytes calldata protocolData)
|
||||
{
|
||||
tokenInIndex = uint8(swap[0]);
|
||||
tokenOutIndex = uint8(swap[1]);
|
||||
split = uint24(bytes3(swap[2:5]));
|
||||
executor = address(uint160(bytes20(swap[5:25])));
|
||||
protocolData = swap[25:];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ 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.
|
||||
@@ -345,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 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 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 swapData Encoded swap details.
|
||||
*
|
||||
@@ -358,7 +357,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
uint256 minAmountOut,
|
||||
bool wrapEth,
|
||||
bool unwrapEth,
|
||||
uint256 nTokens,
|
||||
address receiver,
|
||||
bytes calldata swapData
|
||||
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
|
||||
@@ -370,7 +368,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
minAmountOut,
|
||||
wrapEth,
|
||||
unwrapEth,
|
||||
nTokens,
|
||||
receiver,
|
||||
swapData
|
||||
);
|
||||
@@ -394,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 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 nTokens The total number of tokens involved in the swap graph (used to initialize arrays for internal calculations).
|
||||
* @param receiver The address to receive the output tokens.
|
||||
* @param permitSingle A Permit2 structure containing token approval details for the input token. Ignored if `wrapEth` is true.
|
||||
* @param signature A valid signature authorizing the Permit2 approval. Ignored if `wrapEth` is true.
|
||||
@@ -409,7 +405,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
uint256 minAmountOut,
|
||||
bool wrapEth,
|
||||
bool unwrapEth,
|
||||
uint256 nTokens,
|
||||
address receiver,
|
||||
IAllowanceTransfer.PermitSingle calldata permitSingle,
|
||||
bytes calldata signature,
|
||||
@@ -433,7 +428,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
minAmountOut,
|
||||
wrapEth,
|
||||
unwrapEth,
|
||||
nTokens,
|
||||
receiver,
|
||||
swapData
|
||||
);
|
||||
@@ -523,7 +517,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
uint256 minAmountOut,
|
||||
bool wrapEth,
|
||||
bool unwrapEth,
|
||||
uint256 nTokens,
|
||||
address receiver,
|
||||
bytes calldata swap_
|
||||
) internal returns (uint256 amountOut) {
|
||||
@@ -544,8 +537,10 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
? address(this).balance
|
||||
: IERC20(tokenIn).balanceOf(address(this));
|
||||
|
||||
amountOut =
|
||||
_callExecutor(swap_.executor(), amountIn, swap_.protocolData());
|
||||
(address executor, bytes calldata protocolData) =
|
||||
swap_.decodeSingleSwap();
|
||||
|
||||
amountOut = _callExecutor(executor, amountIn, protocolData);
|
||||
uint256 currentBalance = tokenIn == address(0)
|
||||
? address(this).balance
|
||||
: IERC20(tokenIn).balanceOf(address(this));
|
||||
@@ -684,6 +679,8 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
uint8 tokenInIndex = 0;
|
||||
uint8 tokenOutIndex = 0;
|
||||
uint24 split;
|
||||
address executor;
|
||||
bytes calldata protocolData;
|
||||
bytes calldata swapData;
|
||||
|
||||
uint256[] memory remainingAmounts = new uint256[](nTokens);
|
||||
@@ -694,17 +691,16 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
|
||||
while (swaps_.length > 0) {
|
||||
(swapData, swaps_) = swaps_.next();
|
||||
tokenInIndex = swapData.tokenInIndex();
|
||||
tokenOutIndex = swapData.tokenOutIndex();
|
||||
split = swapData.splitPercentage();
|
||||
|
||||
(tokenInIndex, tokenOutIndex, split, executor, protocolData) =
|
||||
swapData.decodeSplitSwap();
|
||||
|
||||
currentAmountIn = split > 0
|
||||
? (amounts[tokenInIndex] * split) / 0xffffff
|
||||
: remainingAmounts[tokenInIndex];
|
||||
|
||||
currentAmountOut = _callExecutor(
|
||||
swapData.executor(), currentAmountIn, swapData.protocolData()
|
||||
);
|
||||
currentAmountOut =
|
||||
_callExecutor(executor, currentAmountIn, protocolData);
|
||||
// Checks if the output token is the same as the input token
|
||||
if (tokenOutIndex == 0) {
|
||||
cyclicSwapAmountOut += currentAmountOut;
|
||||
@@ -725,16 +721,20 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||
*
|
||||
* @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) {
|
||||
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(swap.executor(), calculatedAmount, swap.protocolData());
|
||||
_callExecutor(executor, calculatedAmount, protocolData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,45 @@ import "../lib/LibSwap.sol";
|
||||
contract LibSwapTest is Test {
|
||||
using LibSwap for bytes;
|
||||
|
||||
function testSwap() public view {
|
||||
function testSingleSwap() public view {
|
||||
address executor = 0x1234567890123456789012345678901234567890;
|
||||
bytes memory protocolData = abi.encodePacked(uint256(123));
|
||||
|
||||
bytes memory swap = abi.encodePacked(executor, protocolData);
|
||||
this.assertSingleSwap(swap, executor, protocolData);
|
||||
}
|
||||
|
||||
function assertSingleSwap(
|
||||
bytes calldata swap,
|
||||
address executor,
|
||||
bytes calldata protocolData
|
||||
) public pure {
|
||||
(address decodedExecutor, bytes memory decodedProtocolData) =
|
||||
swap.decodeSingleSwap();
|
||||
assertEq(decodedExecutor, executor);
|
||||
assertEq(decodedProtocolData, protocolData);
|
||||
}
|
||||
|
||||
function testSequentialSwap() public view {
|
||||
address executor = 0x1234567890123456789012345678901234567890;
|
||||
bytes memory protocolData = abi.encodePacked(uint256(234));
|
||||
|
||||
bytes memory swap = abi.encodePacked(executor, protocolData);
|
||||
this.assertSequentialSwap(swap, executor, protocolData);
|
||||
}
|
||||
|
||||
function assertSequentialSwap(
|
||||
bytes calldata swap,
|
||||
address executor,
|
||||
bytes calldata protocolData
|
||||
) public pure {
|
||||
(address decodedExecutor, bytes memory decodedProtocolData) =
|
||||
swap.decodeSequentialSwap();
|
||||
assertEq(decodedExecutor, executor);
|
||||
assertEq(decodedProtocolData, protocolData);
|
||||
}
|
||||
|
||||
function testSplitSwap() public view {
|
||||
uint8 tokenInIndex = 1;
|
||||
uint8 tokenOutIndex = 2;
|
||||
uint24 split = 3;
|
||||
@@ -17,20 +55,32 @@ contract LibSwapTest is Test {
|
||||
bytes memory swap = abi.encodePacked(
|
||||
tokenInIndex, tokenOutIndex, split, executor, protocolData
|
||||
);
|
||||
this.assertSwap(swap, tokenInIndex, tokenOutIndex, split, executor);
|
||||
this.assertSplitSwap(
|
||||
swap, tokenInIndex, tokenOutIndex, split, executor, protocolData
|
||||
);
|
||||
}
|
||||
|
||||
// This is necessary so that the compiler accepts bytes as a LibSwap.sol
|
||||
function assertSwap(
|
||||
// This is necessary so that the compiler accepts bytes as a LibSwap.sol for testing
|
||||
// This is because this function takes calldata as input
|
||||
function assertSplitSwap(
|
||||
bytes calldata swap,
|
||||
uint8 tokenInIndex,
|
||||
uint8 tokenOutIndex,
|
||||
uint24 split,
|
||||
address executor
|
||||
address executor,
|
||||
bytes calldata protocolData
|
||||
) public pure {
|
||||
assert(swap.tokenInIndex() == tokenInIndex);
|
||||
assert(swap.tokenOutIndex() == tokenOutIndex);
|
||||
assert(swap.splitPercentage() == split);
|
||||
assert(swap.executor() == executor);
|
||||
(
|
||||
uint8 decodedTokenInIndex,
|
||||
uint8 decodedTokenOutIndex,
|
||||
uint24 decodedSplit,
|
||||
address decodedExecutor,
|
||||
bytes memory decodedProtocolData
|
||||
) = swap.decodeSplitSwap();
|
||||
assertEq(decodedTokenInIndex, tokenInIndex);
|
||||
assertEq(decodedTokenOutIndex, tokenOutIndex);
|
||||
assertEq(decodedSplit, split);
|
||||
assertEq(decodedExecutor, executor);
|
||||
assertEq(decodedProtocolData, protocolData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
|
||||
}
|
||||
|
||||
function testSingleSwapSimplePermit2() public {
|
||||
function testSingleSwapPermit2() public {
|
||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2 using Permit2
|
||||
// 1 WETH -> DAI
|
||||
// (USV2)
|
||||
@@ -247,24 +247,20 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
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;
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||
|
||||
tychoRouter.splitSwapPermit2(
|
||||
tychoRouter.singleSwapPermit2(
|
||||
amountIn,
|
||||
WETH_ADDR,
|
||||
DAI_ADDR,
|
||||
2659881924818443699786,
|
||||
false,
|
||||
false,
|
||||
2,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
pleEncode(swaps)
|
||||
swap
|
||||
);
|
||||
|
||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
|
||||
@@ -274,7 +270,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testSequentialSwapMultipleHops() public {
|
||||
function testSequentialSwap() public {
|
||||
// Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2
|
||||
// 1 WETH -> DAI -> USDC
|
||||
// (univ2) (univ2)
|
||||
@@ -283,10 +279,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
|
||||
bytes[] memory swaps = new bytes[](2);
|
||||
// WETH -> DAI
|
||||
swaps[0] = encodeSplitSwap(
|
||||
uint8(0),
|
||||
uint8(1),
|
||||
uint24(0),
|
||||
swaps[0] = encodeSequentialSwap(
|
||||
address(usv2Executor),
|
||||
encodeUniswapV2Swap(
|
||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
||||
@@ -294,10 +287,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
);
|
||||
|
||||
// DAI -> USDC
|
||||
swaps[1] = encodeSplitSwap(
|
||||
uint8(1),
|
||||
uint8(2),
|
||||
uint24(0),
|
||||
swaps[1] = encodeSequentialSwap(
|
||||
address(usv2Executor),
|
||||
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true)
|
||||
);
|
||||
@@ -384,9 +374,8 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
||||
);
|
||||
|
||||
bytes memory swap = encodeSplitSwap(
|
||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
||||
);
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||
|
||||
uint256 minAmountOut = 2600 * 1e18;
|
||||
uint256 amountOut = tychoRouter.singleSwapPermit2(
|
||||
@@ -396,7 +385,6 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
minAmountOut,
|
||||
false,
|
||||
false,
|
||||
2,
|
||||
ALICE,
|
||||
permitSingle,
|
||||
signature,
|
||||
@@ -466,9 +454,8 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
|
||||
);
|
||||
|
||||
bytes memory swap = encodeSplitSwap(
|
||||
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
|
||||
);
|
||||
bytes memory swap =
|
||||
encodeSingleSwap(address(usv2Executor), protocolData);
|
||||
|
||||
uint256 minAmountOut = 2600 * 1e18;
|
||||
uint256 amountOut = tychoRouter.singleSwap(
|
||||
@@ -478,7 +465,6 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
minAmountOut,
|
||||
false,
|
||||
false,
|
||||
2,
|
||||
ALICE,
|
||||
swap
|
||||
);
|
||||
@@ -979,27 +965,18 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
|
||||
bytes[] memory swaps = new bytes[](2);
|
||||
// USDC -> WETH
|
||||
swaps[0] = encodeSplitSwap(
|
||||
uint8(0),
|
||||
uint8(1),
|
||||
uint24(0),
|
||||
address(usv3Executor),
|
||||
usdcWethV3Pool1ZeroOneData
|
||||
swaps[0] = encodeSequentialSwap(
|
||||
address(usv3Executor), usdcWethV3Pool1ZeroOneData
|
||||
);
|
||||
// WETH -> USDC
|
||||
swaps[1] = encodeSplitSwap(
|
||||
uint8(1),
|
||||
uint8(0),
|
||||
uint24(0),
|
||||
address(usv3Executor),
|
||||
usdcWethV3Pool2OneZeroData
|
||||
swaps[1] = encodeSequentialSwap(
|
||||
address(usv3Executor), usdcWethV3Pool2OneZeroData
|
||||
);
|
||||
|
||||
tychoRouter.exposedSequentialSwap(amountIn, 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:
|
||||
|
||||
@@ -30,10 +30,10 @@ contract TychoRouterExposed is TychoRouter {
|
||||
return _splitSwap(amountIn, nTokens, swaps);
|
||||
}
|
||||
|
||||
function exposedSequentialSwap(
|
||||
uint256 amountIn,
|
||||
bytes calldata swaps
|
||||
) external returns (uint256) {
|
||||
function exposedSequentialSwap(uint256 amountIn, bytes calldata swaps)
|
||||
external
|
||||
returns (uint256)
|
||||
{
|
||||
return _sequentialSwap(amountIn, swaps);
|
||||
}
|
||||
}
|
||||
@@ -188,19 +188,23 @@ contract TychoRouterTestSetup is Constants {
|
||||
}
|
||||
}
|
||||
|
||||
function encodeSplitSwap(
|
||||
uint8 tokenInIndex,
|
||||
uint8 tokenOutIndex,
|
||||
uint24 split,
|
||||
address executor,
|
||||
bytes memory protocolData
|
||||
) internal pure returns (bytes memory) {
|
||||
return abi.encodePacked(
|
||||
tokenInIndex, tokenOutIndex, split, executor, protocolData
|
||||
);
|
||||
function encodeSingleSwap(address executor, bytes memory protocolData)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
return abi.encodePacked(executor, protocolData);
|
||||
}
|
||||
|
||||
function encodeSequentialSwap(
|
||||
function encodeSequentialSwap(address executor, bytes memory protocolData)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
return abi.encodePacked(executor, protocolData);
|
||||
}
|
||||
|
||||
function encodeSplitSwap(
|
||||
uint8 tokenInIndex,
|
||||
uint8 tokenOutIndex,
|
||||
uint24 split,
|
||||
|
||||
Reference in New Issue
Block a user