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

View File

@@ -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. * @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 * This function enables multi-step swaps, optional ETH wrapping/unwrapping, and validates the output amount
* against a user-specified minimum. This function performs a transferFrom to retrieve tokens from the caller. * against a user-specified minimum. 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 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.
* *
@@ -358,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) {
@@ -370,7 +368,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
minAmountOut, minAmountOut,
wrapEth, wrapEth,
unwrapEth, unwrapEth,
nTokens,
receiver, receiver,
swapData 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 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.
@@ -409,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,
@@ -433,7 +428,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
minAmountOut, minAmountOut,
wrapEth, wrapEth,
unwrapEth, unwrapEth,
nTokens,
receiver, receiver,
swapData swapData
); );
@@ -523,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) {
@@ -544,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));
@@ -684,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);
@@ -694,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;
@@ -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. * @return calculatedAmount The total amount of the buy token obtained after all swaps have been executed.
*/ */
function _sequentialSwap( function _sequentialSwap(uint256 amountIn, bytes calldata swaps_)
uint256 amountIn, internal
bytes calldata swaps_ returns (uint256 calculatedAmount)
) internal returns (uint256 calculatedAmount) { {
bytes calldata swap; bytes calldata swap;
calculatedAmount = amountIn; calculatedAmount = amountIn;
while (swaps_.length > 0) { while (swaps_.length > 0) {
(swap, swaps_) = swaps_.next(); (swap, swaps_) = swaps_.next();
(address executor, bytes calldata protocolData) =
swap.decodeSingleSwap();
calculatedAmount = 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 { contract LibSwapTest is Test {
using LibSwap for bytes; using LibSwap for bytes;
function testSwap() public view { function testSingleSwap() public view {
address executor = 0x1234567890123456789012345678901234567890;
bytes memory protocolData = abi.encodePacked(uint256(123));
bytes memory swap = abi.encodePacked(executor, protocolData);
this.assertSingleSwap(swap, executor, protocolData);
}
function assertSingleSwap(
bytes calldata swap,
address executor,
bytes calldata protocolData
) public pure {
(address decodedExecutor, bytes memory decodedProtocolData) =
swap.decodeSingleSwap();
assertEq(decodedExecutor, executor);
assertEq(decodedProtocolData, protocolData);
}
function testSequentialSwap() public view {
address executor = 0x1234567890123456789012345678901234567890;
bytes memory protocolData = abi.encodePacked(uint256(234));
bytes memory swap = abi.encodePacked(executor, protocolData);
this.assertSequentialSwap(swap, executor, protocolData);
}
function assertSequentialSwap(
bytes calldata swap,
address executor,
bytes calldata protocolData
) public pure {
(address decodedExecutor, bytes memory decodedProtocolData) =
swap.decodeSequentialSwap();
assertEq(decodedExecutor, executor);
assertEq(decodedProtocolData, protocolData);
}
function testSplitSwap() public view {
uint8 tokenInIndex = 1; uint8 tokenInIndex = 1;
uint8 tokenOutIndex = 2; uint8 tokenOutIndex = 2;
uint24 split = 3; uint24 split = 3;
@@ -17,20 +55,32 @@ contract LibSwapTest is Test {
bytes memory swap = abi.encodePacked( bytes memory swap = abi.encodePacked(
tokenInIndex, tokenOutIndex, split, executor, protocolData tokenInIndex, tokenOutIndex, split, executor, protocolData
); );
this.assertSwap(swap, tokenInIndex, tokenOutIndex, split, executor); this.assertSplitSwap(
swap, tokenInIndex, tokenOutIndex, split, executor, protocolData
);
} }
// This is necessary so that the compiler accepts bytes as a LibSwap.sol // This is necessary so that the compiler accepts bytes as a LibSwap.sol for testing
function assertSwap( // This is because this function takes calldata as input
function assertSplitSwap(
bytes calldata swap, bytes calldata swap,
uint8 tokenInIndex, uint8 tokenInIndex,
uint8 tokenOutIndex, uint8 tokenOutIndex,
uint24 split, uint24 split,
address executor address executor,
bytes calldata protocolData
) public pure { ) public pure {
assert(swap.tokenInIndex() == tokenInIndex); (
assert(swap.tokenOutIndex() == tokenOutIndex); uint8 decodedTokenInIndex,
assert(swap.splitPercentage() == split); uint8 decodedTokenOutIndex,
assert(swap.executor() == executor); uint24 decodedSplit,
address decodedExecutor,
bytes memory decodedProtocolData
) = swap.decodeSplitSwap();
assertEq(decodedTokenInIndex, tokenInIndex);
assertEq(decodedTokenOutIndex, tokenOutIndex);
assertEq(decodedSplit, split);
assertEq(decodedExecutor, executor);
assertEq(decodedProtocolData, protocolData);
} }
} }

View File

@@ -230,7 +230,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); 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 // Trade 1 WETH for DAI with 1 swap on Uniswap V2 using Permit2
// 1 WETH -> DAI // 1 WETH -> DAI
// (USV2) // (USV2)
@@ -247,24 +247,20 @@ contract TychoRouterTest is TychoRouterTestSetup {
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
); );
bytes memory swap = encodeSplitSwap( bytes memory swap =
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData encodeSingleSwap(address(usv2Executor), protocolData);
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
tychoRouter.splitSwapPermit2( tychoRouter.singleSwapPermit2(
amountIn, amountIn,
WETH_ADDR, WETH_ADDR,
DAI_ADDR, DAI_ADDR,
2659881924818443699786, 2659881924818443699786,
false, false,
false, false,
2,
ALICE, ALICE,
permitSingle, permitSingle,
signature, signature,
pleEncode(swaps) swap
); );
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
@@ -274,7 +270,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
vm.stopPrank(); vm.stopPrank();
} }
function testSequentialSwapMultipleHops() public { function testSequentialSwap() public {
// Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2 // Trade 1 WETH for USDC through DAI with 2 swaps on Uniswap V2
// 1 WETH -> DAI -> USDC // 1 WETH -> DAI -> USDC
// (univ2) (univ2) // (univ2) (univ2)
@@ -283,10 +279,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
bytes[] memory swaps = new bytes[](2); bytes[] memory swaps = new bytes[](2);
// WETH -> DAI // WETH -> DAI
swaps[0] = encodeSplitSwap( swaps[0] = encodeSequentialSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv2Executor), address(usv2Executor),
encodeUniswapV2Swap( encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
@@ -294,10 +287,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
); );
// DAI -> USDC // DAI -> USDC
swaps[1] = encodeSplitSwap( swaps[1] = encodeSequentialSwap(
uint8(1),
uint8(2),
uint24(0),
address(usv2Executor), address(usv2Executor),
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true) encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true)
); );
@@ -384,9 +374,8 @@ contract TychoRouterTest is TychoRouterTestSetup {
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
); );
bytes memory swap = encodeSplitSwap( bytes memory swap =
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData encodeSingleSwap(address(usv2Executor), protocolData);
);
uint256 minAmountOut = 2600 * 1e18; uint256 minAmountOut = 2600 * 1e18;
uint256 amountOut = tychoRouter.singleSwapPermit2( uint256 amountOut = tychoRouter.singleSwapPermit2(
@@ -396,7 +385,6 @@ contract TychoRouterTest is TychoRouterTestSetup {
minAmountOut, minAmountOut,
false, false,
false, false,
2,
ALICE, ALICE,
permitSingle, permitSingle,
signature, signature,
@@ -466,9 +454,8 @@ contract TychoRouterTest is TychoRouterTestSetup {
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
); );
bytes memory swap = encodeSplitSwap( bytes memory swap =
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData encodeSingleSwap(address(usv2Executor), protocolData);
);
uint256 minAmountOut = 2600 * 1e18; uint256 minAmountOut = 2600 * 1e18;
uint256 amountOut = tychoRouter.singleSwap( uint256 amountOut = tychoRouter.singleSwap(
@@ -478,7 +465,6 @@ contract TychoRouterTest is TychoRouterTestSetup {
minAmountOut, minAmountOut,
false, false,
false, false,
2,
ALICE, ALICE,
swap swap
); );
@@ -979,27 +965,18 @@ contract TychoRouterTest is TychoRouterTestSetup {
bytes[] memory swaps = new bytes[](2); bytes[] memory swaps = new bytes[](2);
// USDC -> WETH // USDC -> WETH
swaps[0] = encodeSplitSwap( swaps[0] = encodeSequentialSwap(
uint8(0), address(usv3Executor), usdcWethV3Pool1ZeroOneData
uint8(1),
uint24(0),
address(usv3Executor),
usdcWethV3Pool1ZeroOneData
); );
// WETH -> USDC // WETH -> USDC
swaps[1] = encodeSplitSwap( swaps[1] = encodeSequentialSwap(
uint8(1), address(usv3Executor), usdcWethV3Pool2OneZeroData
uint8(0),
uint24(0),
address(usv3Executor),
usdcWethV3Pool2OneZeroData
); );
tychoRouter.exposedSequentialSwap(amountIn, pleEncode(swaps)); tychoRouter.exposedSequentialSwap(amountIn, pleEncode(swaps));
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99889294); assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99889294);
} }
function testSplitInputCyclicSwap() public { function testSplitInputCyclicSwap() public {
// This test has start and end tokens that are the same // This test has start and end tokens that are the same
// The flow is: // The flow is:

View File

@@ -30,10 +30,10 @@ contract TychoRouterExposed is TychoRouter {
return _splitSwap(amountIn, nTokens, swaps); return _splitSwap(amountIn, nTokens, swaps);
} }
function exposedSequentialSwap( function exposedSequentialSwap(uint256 amountIn, bytes calldata swaps)
uint256 amountIn, external
bytes calldata swaps returns (uint256)
) external returns (uint256) { {
return _sequentialSwap(amountIn, swaps); return _sequentialSwap(amountIn, swaps);
} }
} }
@@ -188,19 +188,23 @@ contract TychoRouterTestSetup is Constants {
} }
} }
function encodeSplitSwap( function encodeSingleSwap(address executor, bytes memory protocolData)
uint8 tokenInIndex, internal
uint8 tokenOutIndex, pure
uint24 split, returns (bytes memory)
address executor, {
bytes memory protocolData return abi.encodePacked(executor, protocolData);
) internal pure returns (bytes memory) {
return abi.encodePacked(
tokenInIndex, tokenOutIndex, split, 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 tokenInIndex,
uint8 tokenOutIndex, uint8 tokenOutIndex,
uint24 split, uint24 split,