Merge pull request #102 from propeller-heads/router/hr/ENG-4290-cyclic-swap
feat: Support cyclic swaps
This commit is contained in:
@@ -266,7 +266,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
|
|
||||||
uint256 amountConsumed = initialBalance - currentBalance;
|
uint256 amountConsumed = initialBalance - currentBalance;
|
||||||
|
|
||||||
if (amountConsumed != amountIn) {
|
if (tokenIn != tokenOut && amountConsumed != amountIn) {
|
||||||
revert TychoRouter__AmountInDiffersFromConsumed(
|
revert TychoRouter__AmountInDiffersFromConsumed(
|
||||||
amountIn, amountConsumed
|
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 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.
|
* - 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
|
* - 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.
|
* 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).
|
* - 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,
|
* 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.
|
* 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 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.
|
* @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 remainingAmounts = new uint256[](nTokens);
|
||||||
uint256[] memory amounts = new uint256[](nTokens);
|
uint256[] memory amounts = new uint256[](nTokens);
|
||||||
|
uint256 cyclicSwapAmountOut = 0;
|
||||||
amounts[0] = amountIn;
|
amounts[0] = amountIn;
|
||||||
remainingAmounts[0] = amountIn;
|
remainingAmounts[0] = amountIn;
|
||||||
|
|
||||||
@@ -345,11 +349,16 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
currentAmountOut = _callExecutor(
|
currentAmountOut = _callExecutor(
|
||||||
swapData.executor(), currentAmountIn, swapData.protocolData()
|
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[tokenOutIndex] += currentAmountOut;
|
||||||
remainingAmounts[tokenInIndex] -= currentAmountIn;
|
remainingAmounts[tokenInIndex] -= currentAmountIn;
|
||||||
}
|
}
|
||||||
return amounts[tokenOutIndex];
|
return tokenOutIndex == 0 ? cyclicSwapAmountOut : amounts[tokenOutIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -45,11 +45,14 @@ contract Constants is Test, BaseConstants {
|
|||||||
address DAI_USDC_POOL = 0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5;
|
address DAI_USDC_POOL = 0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5;
|
||||||
address WETH_WBTC_POOL = 0xBb2b8038a1640196FbE3e38816F3e67Cba72D940;
|
address WETH_WBTC_POOL = 0xBb2b8038a1640196FbE3e38816F3e67Cba72D940;
|
||||||
address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416;
|
address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416;
|
||||||
|
address USDC_WETH_USV2 = 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc;
|
||||||
|
|
||||||
// Uniswap v3
|
// Uniswap v3
|
||||||
address USV3_FACTORY_ETHEREUM = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
|
address USV3_FACTORY_ETHEREUM = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
|
||||||
address USV2_FACTORY_ETHEREUM = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
|
address USV2_FACTORY_ETHEREUM = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
|
||||||
address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8;
|
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
|
// Uniswap universal router
|
||||||
address UNIVERSAL_ROUTER = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af;
|
address UNIVERSAL_ROUTER = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af;
|
||||||
|
|||||||
@@ -1192,6 +1192,146 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
|||||||
assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718);
|
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
|
// Base Network Tests
|
||||||
// Make sure to set the RPC_URL to base network
|
// Make sure to set the RPC_URL to base network
|
||||||
function testSwapSingleBase() public {
|
function testSwapSingleBase() public {
|
||||||
|
|||||||
Reference in New Issue
Block a user