Merge pull request #102 from propeller-heads/router/hr/ENG-4290-cyclic-swap

feat: Support cyclic swaps
This commit is contained in:
Harsh Vardhan Roy
2025-03-10 23:37:27 +05:30
committed by GitHub
3 changed files with 156 additions and 4 deletions

View File

@@ -266,7 +266,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
uint256 amountConsumed = initialBalance - currentBalance;
if (amountConsumed != amountIn) {
if (tokenIn != tokenOut && amountConsumed != amountIn) {
revert TychoRouter__AmountInDiffersFromConsumed(
amountIn, amountConsumed
);
@@ -299,12 +299,15 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
* - The indices of the input and output tokens (via `tokenInIndex()` and `tokenOutIndex()`).
* - The portion of the available amount to be used for the swap, indicated by the `split` value.
*
* Two important notes:
* Three important notes:
* - The contract assumes that token indexes follow a specific order: the sell token is at index 0, followed by any
* intermediary tokens, and finally the buy token.
* - A `split` value of 0 is interpreted as 100% of the available amount (i.e., the entire remaining balance).
* This means that in scenarios without explicit splits the value should be 0, and when splits are present,
* the last swap should also have a split value of 0.
* - In case of cyclic swaps, the output token is the same as the input token.
* `cyclicSwapAmountOut` is used to track the amount of the output token, and is updated when
* the `tokenOutIndex` is 0.
*
* @param amountIn The initial amount of the sell token to be swapped.
* @param nTokens The total number of tokens involved in the swap path, used to initialize arrays for internal tracking.
@@ -329,6 +332,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
uint256[] memory remainingAmounts = new uint256[](nTokens);
uint256[] memory amounts = new uint256[](nTokens);
uint256 cyclicSwapAmountOut = 0;
amounts[0] = amountIn;
remainingAmounts[0] = amountIn;
@@ -345,11 +349,16 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
currentAmountOut = _callExecutor(
swapData.executor(), currentAmountIn, swapData.protocolData()
);
amounts[tokenOutIndex] += currentAmountOut;
// Checks if the output token is the same as the input token
if (tokenOutIndex == 0) {
cyclicSwapAmountOut += currentAmountOut;
} else {
amounts[tokenOutIndex] += currentAmountOut;
}
remainingAmounts[tokenOutIndex] += currentAmountOut;
remainingAmounts[tokenInIndex] -= currentAmountIn;
}
return amounts[tokenOutIndex];
return tokenOutIndex == 0 ? cyclicSwapAmountOut : amounts[tokenOutIndex];
}
/**

View File

@@ -45,11 +45,14 @@ contract Constants is Test, BaseConstants {
address DAI_USDC_POOL = 0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5;
address WETH_WBTC_POOL = 0xBb2b8038a1640196FbE3e38816F3e67Cba72D940;
address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416;
address USDC_WETH_USV2 = 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc;
// Uniswap v3
address USV3_FACTORY_ETHEREUM = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
address USV2_FACTORY_ETHEREUM = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8;
address USDC_WETH_USV3 = 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640; // 0.05% fee
address USDC_WETH_USV3_2 = 0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8; // 0.3% fee
// Uniswap universal router
address UNIVERSAL_ROUTER = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af;

View File

@@ -1192,6 +1192,146 @@ contract TychoRouterTest is TychoRouterTestSetup {
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] = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv3Executor),
usdcWethV3Pool1ZeroOneData
);
// WETH -> USDC
swaps[1] = encodeSwap(
uint8(1),
uint8(0),
uint24(0),
address(usv3Executor),
usdcWethV3Pool2OneZeroData
);
tychoRouter.exposedSwap(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] = encodeSwap(
uint8(0),
uint8(1),
(0xffffff * 60) / 100, // 60%
address(usv3Executor),
usdcWethV3Pool1ZeroOneData
);
// USDC -> WETH (40% remainder)
swaps[1] = encodeSwap(
uint8(0),
uint8(1),
uint24(0),
address(usv3Executor),
usdcWethV3Pool2ZeroOneData
);
// WETH -> USDC
swaps[2] = encodeSwap(
uint8(1),
uint8(0),
uint24(0),
address(usv2Executor),
wethUsdcV2OneZeroData
);
tychoRouter.exposedSwap(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] = encodeSwap(
uint8(0), uint8(1), uint24(0), address(usv2Executor), usdcWethV2Data
);
// WETH -> USDC
swaps[1] = encodeSwap(
uint8(1),
uint8(0),
(0xffffff * 60) / 100,
address(usv3Executor),
usdcWethV3Pool1OneZeroData
);
// WETH -> USDC
swaps[2] = encodeSwap(
uint8(1),
uint8(0),
uint24(0),
address(usv3Executor),
usdcWethV3Pool2OneZeroData
);
tychoRouter.exposedSwap(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 testSwapSingleBase() public {