From 21a8c1a27a8370bf7471b206e78b6a2fcf38ce00 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 14 Feb 2025 11:32:38 +0000 Subject: [PATCH] feat(univ4): Implement swapping with multiple hops --- don't change below this line --- ENG-4222 Took 47 minutes --- foundry/src/executors/UniswapV4Executor.sol | 45 ++++++++++++++--- foundry/test/TychoRouter.t.sol | 50 ++++++++++++++++++- .../test/executors/UniswapV4Executor.t.sol | 2 +- foundry/test/executors/UniswapV4Utils.sol | 2 +- 4 files changed, 88 insertions(+), 11 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 493a4fe..57ba690 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -19,7 +19,6 @@ import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol"; import "lib/forge-std/src/console.sol"; error UniswapV4Executor__InvalidDataLength(); -error UniswapV4Executor__SwapFailed(); contract UniswapV4Executor is IExecutor, V4Router { using SafeERC20 for IERC20; @@ -49,9 +48,7 @@ contract UniswapV4Executor is IExecutor, V4Router { ) = _decodeData(data); bytes memory fullData; - if (pools.length == 0) { - console.log("problem"); // raise error - } else if (pools.length == 1) { + if (pools.length == 1) { PoolKey memory key = PoolKey({ currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut), currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), @@ -77,12 +74,44 @@ contract UniswapV4Executor is IExecutor, V4Router { }) ); params[1] = abi.encode(key.currency0, amountIn); - params[2] = abi.encode(key.currency1, 0); + params[2] = abi.encode(key.currency1, amountOutMin); bytes memory swapData = abi.encode(actions, params); fullData = abi.encodePacked(callbackExecutor, callbackSelector, swapData); } else { - console.log("do later"); + PathKey[] memory path = new PathKey[](pools.length); + for (uint256 i = 0; i < pools.length; i++) { + path[i] = PathKey({ + intermediateCurrency: Currency.wrap(pools[i].intermediaryToken), + fee: pools[i].fee, + tickSpacing: pools[i].tickSpacing, + hooks: IHooks(address(0)), + hookData: bytes("") + }); + } + + bytes memory actions = abi.encodePacked( + uint8(Actions.SWAP_EXACT_IN), + uint8(Actions.SETTLE_ALL), + uint8(Actions.TAKE_ALL) + ); + + bytes[] memory params = new bytes[](3); + + Currency currencyIn = Currency.wrap(tokenIn); + params[0] = abi.encode( + IV4Router.ExactInputParams({ + currencyIn: currencyIn, + path: path, + amountIn: uint128(amountIn), + amountOutMinimum: uint128(amountOutMin) + }) + ); + params[1] = abi.encode(currencyIn, amountIn); + params[2] = abi.encode(Currency.wrap(tokenOut), amountOutMin); + bytes memory swapData = abi.encode(actions, params); + fullData = + abi.encodePacked(callbackExecutor, callbackSelector, swapData); } uint256 tokenOutBalanceBefore; @@ -122,7 +151,9 @@ contract UniswapV4Executor is IExecutor, V4Router { UniswapV4Pool[] memory pools ) { - require(data.length >= 97, "Invalid data length"); + if(data.length < 123) { + revert UniswapV4Executor__InvalidDataLength(); + } tokenIn = address(bytes20(data[0:20])); tokenOut = address(bytes20(data[20:40])); diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index d2d1d13..28eed5c 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -865,7 +865,7 @@ contract TychoRouterTest is TychoRouterTestSetup { tickSpacing: int24(1) }); - bytes memory protocolData = UniswapV4Utils.encodeExactInputSingle( + bytes memory protocolData = UniswapV4Utils.encodeExactInput( USDE_ADDR, USDT_ADDR, uint256(1), @@ -889,6 +889,52 @@ contract TychoRouterTest is TychoRouterTestSetup { tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); - assertTrue(IERC20(USDT_ADDR).balanceOf(tychoRouterAddr) == 99943852); + assertEq(IERC20(USDT_ADDR).balanceOf(tychoRouterAddr), 99943852); + } + + function testSwapMultipleUSV4Callback() public { + // This test has two uniswap v4 hops that will be executed inside of the V4 pool manager + // USDE -> USDT -> WBTC + uint256 amountIn = 100 ether; + deal(USDE_ADDR, tychoRouterAddr, amountIn); + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](2); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: uint24(100), + tickSpacing: int24(1) + }); + pools[1] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: WBTC_ADDR, + fee: uint24(3000), + tickSpacing: int24(60) + }); + + bytes memory protocolData = UniswapV4Utils.encodeExactInput( + USDE_ADDR, + WBTC_ADDR, + uint256(1), + true, + address(usv4Executor), + SafeCallback.unlockCallback.selector, + pools + ); + + bytes memory swap = encodeSwap( + uint8(0), + uint8(1), + uint24(0), + address(usv4Executor), + bytes4(0), + protocolData + ); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + + assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718); } } diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 265af31..7aa9db9 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -64,7 +64,7 @@ contract UniswapV4ExecutorTest is Test, Constants { tickSpacing: tickSpacing2 }); - bytes memory data = UniswapV4Utils.encodeExactInputSingle( + bytes memory data = UniswapV4Utils.encodeExactInput( USDE_ADDR, USDT_ADDR, minAmountOut, diff --git a/foundry/test/executors/UniswapV4Utils.sol b/foundry/test/executors/UniswapV4Utils.sol index fba64d9..ffa0b21 100644 --- a/foundry/test/executors/UniswapV4Utils.sol +++ b/foundry/test/executors/UniswapV4Utils.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.26; import "@src/executors/UniswapV4Executor.sol"; library UniswapV4Utils { - function encodeExactInputSingle( + function encodeExactInput( address tokenIn, address tokenOut, uint256 amountOutMin,