feat(univ4): Implement swapping with multiple hops

--- don't change below this line ---
ENG-4222 Took 47 minutes
This commit is contained in:
Diana Carvalho
2025-02-14 11:32:38 +00:00
parent be7883affc
commit 21a8c1a27a
4 changed files with 88 additions and 11 deletions

View File

@@ -19,7 +19,6 @@ import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
import "lib/forge-std/src/console.sol"; import "lib/forge-std/src/console.sol";
error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__InvalidDataLength();
error UniswapV4Executor__SwapFailed();
contract UniswapV4Executor is IExecutor, V4Router { contract UniswapV4Executor is IExecutor, V4Router {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
@@ -49,9 +48,7 @@ contract UniswapV4Executor is IExecutor, V4Router {
) = _decodeData(data); ) = _decodeData(data);
bytes memory fullData; bytes memory fullData;
if (pools.length == 0) { if (pools.length == 1) {
console.log("problem"); // raise error
} else if (pools.length == 1) {
PoolKey memory key = PoolKey({ PoolKey memory key = PoolKey({
currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut), currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut),
currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn),
@@ -77,12 +74,44 @@ contract UniswapV4Executor is IExecutor, V4Router {
}) })
); );
params[1] = abi.encode(key.currency0, amountIn); 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); bytes memory swapData = abi.encode(actions, params);
fullData = fullData =
abi.encodePacked(callbackExecutor, callbackSelector, swapData); abi.encodePacked(callbackExecutor, callbackSelector, swapData);
} else { } 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; uint256 tokenOutBalanceBefore;
@@ -122,7 +151,9 @@ contract UniswapV4Executor is IExecutor, V4Router {
UniswapV4Pool[] memory pools UniswapV4Pool[] memory pools
) )
{ {
require(data.length >= 97, "Invalid data length"); if(data.length < 123) {
revert UniswapV4Executor__InvalidDataLength();
}
tokenIn = address(bytes20(data[0:20])); tokenIn = address(bytes20(data[0:20]));
tokenOut = address(bytes20(data[20:40])); tokenOut = address(bytes20(data[20:40]));

View File

@@ -865,7 +865,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
tickSpacing: int24(1) tickSpacing: int24(1)
}); });
bytes memory protocolData = UniswapV4Utils.encodeExactInputSingle( bytes memory protocolData = UniswapV4Utils.encodeExactInput(
USDE_ADDR, USDE_ADDR,
USDT_ADDR, USDT_ADDR,
uint256(1), uint256(1),
@@ -889,6 +889,52 @@ contract TychoRouterTest is TychoRouterTestSetup {
tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); 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);
} }
} }

View File

@@ -64,7 +64,7 @@ contract UniswapV4ExecutorTest is Test, Constants {
tickSpacing: tickSpacing2 tickSpacing: tickSpacing2
}); });
bytes memory data = UniswapV4Utils.encodeExactInputSingle( bytes memory data = UniswapV4Utils.encodeExactInput(
USDE_ADDR, USDE_ADDR,
USDT_ADDR, USDT_ADDR,
minAmountOut, minAmountOut,

View File

@@ -4,7 +4,7 @@ pragma solidity ^0.8.26;
import "@src/executors/UniswapV4Executor.sol"; import "@src/executors/UniswapV4Executor.sol";
library UniswapV4Utils { library UniswapV4Utils {
function encodeExactInputSingle( function encodeExactInput(
address tokenIn, address tokenIn,
address tokenOut, address tokenOut,
uint256 amountOutMin, uint256 amountOutMin,