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:
TAMARA LIPOWSKI
2025-03-18 01:37:14 -04:00
committed by Diana Carvalho
parent 3ae9d3ad67
commit 1dad4afb6b
5 changed files with 148 additions and 111 deletions

View File

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

View File

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

View File

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

View File

@@ -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:

View File

@@ -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,