From be7883affc2e481fce76dcd762215efb83905478 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Thu, 13 Feb 2025 18:40:10 +0000 Subject: [PATCH 1/5] feat(univ4): Refactor input and handle single swap case Construct the uniswap v4 specific objects inside the executor The swap test in the executor alone doesn't work anymore because of the callback data prepended to the rest of he calldata --- don't change below this line --- ENG-4222 Took 4 hours 0 minutes Took 40 seconds --- foundry/src/TychoRouter.sol | 6 +- foundry/src/executors/UniswapV4Executor.sol | 157 ++++++++++-------- foundry/test/TychoRouter.t.sol | 23 ++- .../test/executors/UniswapV4Executor.t.sol | 83 +++++---- foundry/test/executors/UniswapV4Utils.sol | 53 +++--- 5 files changed, 182 insertions(+), 140 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 9d77dec..205a6a4 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -449,9 +449,9 @@ contract TychoRouter is returns (bytes memory) { require(data.length >= 20, "Invalid data length"); - bytes4 selector = bytes4(data[data.length - 24:data.length - 20]); - address executor = address(uint160(bytes20(data[data.length - 20:]))); - bytes memory protocolData = data[:data.length - 24]; + address executor = address(uint160(bytes20(data[0:20]))); + bytes4 selector = bytes4(data[20:24]); + bytes memory protocolData = data[24:]; if (!executors[executor]) { revert ExecutionDispatcher__UnapprovedExecutor(); diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 4a67727..493a4fe 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -16,6 +16,7 @@ import {V4Router} from "@uniswap/v4-periphery/src/V4Router.sol"; import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol"; import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol"; import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol"; +import "lib/forge-std/src/console.sol"; error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__SwapFailed(); @@ -24,108 +25,128 @@ contract UniswapV4Executor is IExecutor, V4Router { using SafeERC20 for IERC20; using CurrencyLibrary for Currency; + struct UniswapV4Pool { + address intermediaryToken; + uint24 fee; + int24 tickSpacing; + } + constructor(IPoolManager _poolManager) V4Router(_poolManager) {} - function swap(uint256, bytes calldata data) + function swap(uint256 amountIn, bytes calldata data) external payable returns (uint256 calculatedAmount) { - (address tokenIn, address tokenOut, bool isExactInput, uint256 amount) = - _decodeData(data); + ( + address tokenIn, + address tokenOut, + uint256 amountOutMin, + bool zeroForOne, + address callbackExecutor, + bytes4 callbackSelector, + UniswapV4Executor.UniswapV4Pool[] memory pools + ) = _decodeData(data); + + bytes memory fullData; + if (pools.length == 0) { + console.log("problem"); // raise error + } else if (pools.length == 1) { + PoolKey memory key = PoolKey({ + currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut), + currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), + fee: pools[0].fee, + tickSpacing: pools[0].tickSpacing, + hooks: IHooks(address(0)) + }); + bytes memory actions = abi.encodePacked( + uint8(Actions.SWAP_EXACT_IN_SINGLE), + uint8(Actions.SETTLE_ALL), + uint8(Actions.TAKE_ALL) + ); + + bytes[] memory params = new bytes[](3); + + params[0] = abi.encode( + IV4Router.ExactInputSingleParams({ + poolKey: key, + zeroForOne: zeroForOne, + amountIn: uint128(amountIn), + amountOutMinimum: uint128(amountOutMin), + hookData: bytes("") + }) + ); + params[1] = abi.encode(key.currency0, amountIn); + params[2] = abi.encode(key.currency1, 0); + bytes memory swapData = abi.encode(actions, params); + fullData = + abi.encodePacked(callbackExecutor, callbackSelector, swapData); + } else { + console.log("do later"); + } uint256 tokenOutBalanceBefore; - uint256 tokenInBalanceBefore; tokenOutBalanceBefore = tokenOut == address(0) ? address(this).balance : IERC20(tokenOut).balanceOf(address(this)); - tokenInBalanceBefore = tokenIn == address(0) - ? address(this).balance - : IERC20(tokenIn).balanceOf(address(this)); - - _executeActions(data); + executeActions(fullData); uint256 tokenOutBalanceAfter; - uint256 tokenInBalanceAfter; tokenOutBalanceAfter = tokenOut == address(0) ? address(this).balance : IERC20(tokenOut).balanceOf(address(this)); - tokenInBalanceAfter = tokenIn == address(0) - ? address(this).balance - : IERC20(tokenIn).balanceOf(address(this)); - - if (isExactInput) { - calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore; - } else { - calculatedAmount = tokenInBalanceBefore - tokenInBalanceAfter; - } + calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore; return calculatedAmount; } + // necessary to convert bytes memory to bytes calldata + function executeActions(bytes memory unlockData) public { + poolManager.unlock(unlockData); + } + function _decodeData(bytes calldata data) - internal + public pure returns ( address tokenIn, address tokenOut, - bool isExactInput, - uint256 amount + uint256 amountOutMin, + bool zeroForOne, + address callbackExecutor, + bytes4 callbackSelector, + UniswapV4Pool[] memory pools ) { - (bytes memory actions, bytes[] memory params) = - abi.decode(data, (bytes, bytes[])); + require(data.length >= 97, "Invalid data length"); - // First byte of actions determines the swap type - uint8 action = uint8(bytes1(actions[0])); + tokenIn = address(bytes20(data[0:20])); + tokenOut = address(bytes20(data[20:40])); + amountOutMin = uint256(bytes32(data[40:72])); + zeroForOne = (data[72] != 0); + callbackExecutor = address(bytes20(data[73:93])); + callbackSelector = bytes4(data[93:97]); - if (action == uint8(Actions.SWAP_EXACT_IN_SINGLE)) { - IV4Router.ExactInputSingleParams memory swapParams = - abi.decode(params[0], (IV4Router.ExactInputSingleParams)); + uint256 poolsLength = (data.length - 97) / 26; // 26 bytes per pool object + pools = new UniswapV4Pool[](poolsLength); + bytes memory poolsData = data[97:]; + uint256 offset = 0; + for (uint256 i = 0; i < poolsLength; i++) { + address intermediaryToken; + uint24 fee; + int24 tickSpacing; - tokenIn = swapParams.zeroForOne - ? address(uint160(swapParams.poolKey.currency0.toId())) - : address(uint160(swapParams.poolKey.currency1.toId())); - tokenOut = swapParams.zeroForOne - ? address(uint160(swapParams.poolKey.currency1.toId())) - : address(uint160(swapParams.poolKey.currency0.toId())); - isExactInput = true; - amount = swapParams.amountIn; - } else if (action == uint8(Actions.SWAP_EXACT_OUT_SINGLE)) { - IV4Router.ExactOutputSingleParams memory swapParams = - abi.decode(params[0], (IV4Router.ExactOutputSingleParams)); - - tokenIn = swapParams.zeroForOne - ? address(uint160(swapParams.poolKey.currency0.toId())) - : address(uint160(swapParams.poolKey.currency1.toId())); - tokenOut = swapParams.zeroForOne - ? address(uint160(swapParams.poolKey.currency1.toId())) - : address(uint160(swapParams.poolKey.currency0.toId())); - isExactInput = false; - amount = swapParams.amountOut; - } else if (action == uint8(Actions.SWAP_EXACT_IN)) { - IV4Router.ExactInputParams memory swapParams = - abi.decode(params[0], (IV4Router.ExactInputParams)); - - tokenIn = address(uint160(swapParams.currencyIn.toId())); - PathKey memory lastPath = - swapParams.path[swapParams.path.length - 1]; - tokenOut = address(uint160(lastPath.intermediateCurrency.toId())); - isExactInput = true; - amount = swapParams.amountIn; - } else if (action == uint8(Actions.SWAP_EXACT_OUT)) { - IV4Router.ExactOutputParams memory swapParams = - abi.decode(params[0], (IV4Router.ExactOutputParams)); - - PathKey memory firstPath = swapParams.path[0]; - tokenIn = address(uint160(firstPath.intermediateCurrency.toId())); - tokenOut = address(uint160(swapParams.currencyOut.toId())); - isExactInput = false; - amount = swapParams.amountOut; + assembly { + intermediaryToken := mload(add(poolsData, add(offset, 20))) + fee := shr(232, mload(add(poolsData, add(offset, 52)))) + tickSpacing := shr(232, mload(add(poolsData, add(offset, 55)))) + } + pools[i] = UniswapV4Pool(intermediaryToken, fee, tickSpacing); + offset += 26; } } diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 960e7b9..d2d1d13 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -857,15 +857,22 @@ contract TychoRouterTest is TychoRouterTestSetup { uint256 amountIn = 100 ether; deal(USDE_ADDR, tychoRouterAddr, amountIn); - bytes memory protocolData = UniswapV4Utils.encodeExactInputSingle( - USDE_ADDR, USDT_ADDR, 100, true, 1, uint128(amountIn) - ); + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](1); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: uint24(100), + tickSpacing: int24(1) + }); - // add executor and selector for callback - bytes memory protocolDataWithCallBack = abi.encodePacked( - protocolData, + bytes memory protocolData = UniswapV4Utils.encodeExactInputSingle( + USDE_ADDR, + USDT_ADDR, + uint256(1), + true, + address(usv4Executor), SafeCallback.unlockCallback.selector, - address(usv4Executor) + pools ); bytes memory swap = encodeSwap( @@ -874,7 +881,7 @@ contract TychoRouterTest is TychoRouterTestSetup { uint24(0), address(usv4Executor), bytes4(0), - protocolDataWithCallBack + protocolData ); bytes[] memory swaps = new bytes[](1); diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 57f4156..265af31 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; +import "../../src/executors/UniswapV4Executor.sol"; import "./UniswapV4Utils.sol"; import "@src/executors/UniswapV4Executor.sol"; import {Constants} from "../Constants.sol"; import {Test} from "../../lib/forge-std/src/Test.sol"; -import {console} from "forge-std/console.sol"; +import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol"; contract UniswapV4ExecutorExposed is UniswapV4Executor { constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {} @@ -16,8 +17,11 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor { returns ( address tokenIn, address tokenOut, - bool isExactInput, - uint256 amount + uint256 amountOutMin, + bool zeroForOne, + address callbackExecutor, + bytes4 callbackSelector, + UniswapV4Pool[] memory pools ) { return _decodeData(data); @@ -40,39 +44,58 @@ contract UniswapV4ExecutorTest is Test, Constants { } function testDecodeParams() public view { - uint24 expectedPoolFee = 500; - uint128 expectedAmount = 100; + uint256 minAmountOut = 100; + bool zeroForOne = true; + uint24 pool1Fee = 500; + int24 tickSpacing1 = 60; + uint24 pool2Fee = 1000; + int24 tickSpacing2 = -10; + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](2); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: pool1Fee, + tickSpacing: tickSpacing1 + }); + pools[1] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDE_ADDR, + fee: pool2Fee, + tickSpacing: tickSpacing2 + }); bytes memory data = UniswapV4Utils.encodeExactInputSingle( - USDE_ADDR, USDT_ADDR, expectedPoolFee, false, 1, expectedAmount + USDE_ADDR, + USDT_ADDR, + minAmountOut, + zeroForOne, + address(uniswapV4Exposed), + SafeCallback.unlockCallback.selector, + pools ); - (address tokenIn, address tokenOut, bool isExactInput, uint256 amount) = - uniswapV4Exposed.decodeData(data); + ( + address tokenIn, + address tokenOut, + uint256 amountOutMin, + bool zeroForOneDecoded, + address callbackExecutor, + bytes4 callbackSelector, + UniswapV4Executor.UniswapV4Pool[] memory decodedPools + ) = uniswapV4Exposed.decodeData(data); assertEq(tokenIn, USDE_ADDR); assertEq(tokenOut, USDT_ADDR); - assertTrue(isExactInput); - assertEq(amount, expectedAmount); - } - - function testSwap() public { - uint256 amountIn = 100 ether; - deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); - uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); - uint256 usdeBalanceBeforeSwapExecutor = - USDE.balanceOf(address(uniswapV4Exposed)); - - bytes memory data = UniswapV4Utils.encodeExactInputSingle( - USDE_ADDR, USDT_ADDR, 100, true, 1, uint128(amountIn) - ); - - uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); - assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); - assertEq( - USDE.balanceOf(address(uniswapV4Exposed)), - usdeBalanceBeforeSwapExecutor - amountIn - ); - assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut); + assertEq(amountOutMin, minAmountOut); + assertEq(zeroForOneDecoded, zeroForOne); + assertEq(callbackExecutor, address(uniswapV4Exposed)); + assertEq(callbackSelector, SafeCallback.unlockCallback.selector); + assertEq(decodedPools.length, 2); + assertEq(decodedPools[0].intermediaryToken, USDT_ADDR); + assertEq(decodedPools[0].fee, pool1Fee); + assertEq(decodedPools[0].tickSpacing, tickSpacing1); + assertEq(decodedPools[1].intermediaryToken, USDE_ADDR); + assertEq(decodedPools[1].fee, pool2Fee); + assertEq(decodedPools[1].tickSpacing, tickSpacing2); } } diff --git a/foundry/test/executors/UniswapV4Utils.sol b/foundry/test/executors/UniswapV4Utils.sol index f4d021c..fba64d9 100644 --- a/foundry/test/executors/UniswapV4Utils.sol +++ b/foundry/test/executors/UniswapV4Utils.sol @@ -7,40 +7,31 @@ library UniswapV4Utils { function encodeExactInputSingle( address tokenIn, address tokenOut, - uint24 fee, + uint256 amountOutMin, bool zeroForOne, - uint24 tickSpacing, - uint128 amountIn + address callbackExecutor, + bytes4 callbackSelector, + UniswapV4Executor.UniswapV4Pool[] memory pools ) public pure returns (bytes memory) { - PoolKey memory key = PoolKey({ - currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut), - currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), - fee: fee, - tickSpacing: int24(tickSpacing), - hooks: IHooks(address(0)) - }); + bytes memory encodedPools; - bytes memory actions = abi.encodePacked( - uint8(Actions.SWAP_EXACT_IN_SINGLE), - uint8(Actions.SETTLE_ALL), - uint8(Actions.TAKE_ALL) + for (uint256 i = 0; i < pools.length; i++) { + encodedPools = abi.encodePacked( + encodedPools, + pools[i].intermediaryToken, + bytes3(pools[i].fee), + pools[i].tickSpacing + ); + } + + return abi.encodePacked( + tokenIn, + tokenOut, + amountOutMin, + zeroForOne, + callbackExecutor, + bytes4(callbackSelector), + encodedPools ); - - bytes[] memory params = new bytes[](3); - - params[0] = abi.encode( - IV4Router.ExactInputSingleParams({ - poolKey: key, - zeroForOne: zeroForOne, - amountIn: amountIn, - amountOutMinimum: 0, - hookData: bytes("") - }) - ); - - params[1] = abi.encode(key.currency0, amountIn); - params[2] = abi.encode(key.currency1, 0); - - return abi.encode(actions, params); } } From 21a8c1a27a8370bf7471b206e78b6a2fcf38ce00 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 14 Feb 2025 11:32:38 +0000 Subject: [PATCH 2/5] 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, From 4d0f5cec64af9c65f5a03685d4c89bb0dd0a897c Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 14 Feb 2025 11:43:59 +0000 Subject: [PATCH 3/5] fix(univ4): Append callback data instead of prepending Bring back tests on the executor level. This way the executor can actually be used alone --- don't change below this line --- ENG-4222 Took 12 minutes --- foundry/src/TychoRouter.sol | 6 +- foundry/src/executors/UniswapV4Executor.sol | 15 ++-- .../test/executors/UniswapV4Executor.t.sol | 76 +++++++++++++++++++ 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 205a6a4..c26e8cb 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -449,9 +449,9 @@ contract TychoRouter is returns (bytes memory) { require(data.length >= 20, "Invalid data length"); - address executor = address(uint160(bytes20(data[0:20]))); - bytes4 selector = bytes4(data[20:24]); - bytes memory protocolData = data[24:]; + bytes4 selector = bytes4(data[data.length - 4:]); + address executor = address(uint160(bytes20(data[data.length - 24:data.length - 4]))); + bytes memory protocolData = data[:data.length - 24]; if (!executors[executor]) { revert ExecutionDispatcher__UnapprovedExecutor(); diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 57ba690..545729a 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -16,7 +16,6 @@ import {V4Router} from "@uniswap/v4-periphery/src/V4Router.sol"; import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol"; import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol"; import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol"; -import "lib/forge-std/src/console.sol"; error UniswapV4Executor__InvalidDataLength(); @@ -47,7 +46,7 @@ contract UniswapV4Executor is IExecutor, V4Router { UniswapV4Executor.UniswapV4Pool[] memory pools ) = _decodeData(data); - bytes memory fullData; + bytes memory swapData; if (pools.length == 1) { PoolKey memory key = PoolKey({ currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut), @@ -75,9 +74,8 @@ contract UniswapV4Executor is IExecutor, V4Router { ); params[1] = abi.encode(key.currency0, amountIn); params[2] = abi.encode(key.currency1, amountOutMin); - bytes memory swapData = abi.encode(actions, params); - fullData = - abi.encodePacked(callbackExecutor, callbackSelector, swapData); + swapData = abi.encode(actions, params); + } else { PathKey[] memory path = new PathKey[](pools.length); for (uint256 i = 0; i < pools.length; i++) { @@ -109,11 +107,10 @@ contract UniswapV4Executor is IExecutor, V4Router { ); 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); + swapData = abi.encode(actions, params); } - + bytes memory fullData = + abi.encodePacked( swapData, callbackExecutor, callbackSelector); uint256 tokenOutBalanceBefore; tokenOutBalanceBefore = tokenOut == address(0) diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 7aa9db9..c9afd56 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -98,4 +98,80 @@ contract UniswapV4ExecutorTest is Test, Constants { assertEq(decodedPools[1].fee, pool2Fee); assertEq(decodedPools[1].tickSpacing, tickSpacing2); } + + function testSingleSwap() public { + uint256 amountIn = 100 ether; + deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); + uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); + uint256 usdeBalanceBeforeSwapExecutor = + USDE.balanceOf(address(uniswapV4Exposed)); + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](1); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: uint24(100), + tickSpacing: int24(1) + }); + + bytes memory data = UniswapV4Utils.encodeExactInput( + USDE_ADDR, + USDT_ADDR, + uint256(1), + true, + address(uniswapV4Exposed), + SafeCallback.unlockCallback.selector, + pools + ); + + uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); + assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); + assertEq( + USDE.balanceOf(address(uniswapV4Exposed)), + usdeBalanceBeforeSwapExecutor - amountIn + ); + assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut); + } + + function testMultipleSwap() public { + // USDE -> USDT -> WBTC + uint256 amountIn = 100 ether; + deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); + uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); + uint256 usdeBalanceBeforeSwapExecutor = + USDE.balanceOf(address(uniswapV4Exposed)); + + + 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 data = UniswapV4Utils.encodeExactInput( + USDE_ADDR, + WBTC_ADDR, + uint256(1), + true, + address(uniswapV4Exposed), + SafeCallback.unlockCallback.selector, + pools + ); + + uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); + assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); + assertEq( + USDE.balanceOf(address(uniswapV4Exposed)), + usdeBalanceBeforeSwapExecutor - amountIn + ); + assertTrue(IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut); + } + } From 8a8bc697eb68308aedf74bd605d2f555328df99c Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 14 Feb 2025 12:01:29 +0000 Subject: [PATCH 4/5] fix(univ4): Make slither happy --- don't change below this line --- ENG-4222 Took 17 minutes Took 47 seconds Took 14 seconds --- foundry/src/TychoRouter.sol | 3 ++- foundry/src/executors/UniswapV4Executor.sol | 16 +++++++--------- foundry/test/executors/UniswapV4Executor.t.sol | 14 +++++++------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index c26e8cb..844507e 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -450,7 +450,8 @@ contract TychoRouter is { require(data.length >= 20, "Invalid data length"); bytes4 selector = bytes4(data[data.length - 4:]); - address executor = address(uint160(bytes20(data[data.length - 24:data.length - 4]))); + address executor = + address(uint160(bytes20(data[data.length - 24:data.length - 4]))); bytes memory protocolData = data[:data.length - 24]; if (!executors[executor]) { diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 545729a..57fe796 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -75,7 +75,6 @@ contract UniswapV4Executor is IExecutor, V4Router { params[1] = abi.encode(key.currency0, amountIn); params[2] = abi.encode(key.currency1, amountOutMin); swapData = abi.encode(actions, params); - } else { PathKey[] memory path = new PathKey[](pools.length); for (uint256 i = 0; i < pools.length; i++) { @@ -109,8 +108,8 @@ contract UniswapV4Executor is IExecutor, V4Router { params[2] = abi.encode(Currency.wrap(tokenOut), amountOutMin); swapData = abi.encode(actions, params); } - bytes memory fullData = - abi.encodePacked( swapData, callbackExecutor, callbackSelector); + bytes memory fullData = + abi.encodePacked(swapData, callbackExecutor, callbackSelector); uint256 tokenOutBalanceBefore; tokenOutBalanceBefore = tokenOut == address(0) @@ -132,11 +131,12 @@ contract UniswapV4Executor is IExecutor, V4Router { // necessary to convert bytes memory to bytes calldata function executeActions(bytes memory unlockData) public { + // slither-disable-next-line unused-return poolManager.unlock(unlockData); } function _decodeData(bytes calldata data) - public + internal pure returns ( address tokenIn, @@ -148,7 +148,7 @@ contract UniswapV4Executor is IExecutor, V4Router { UniswapV4Pool[] memory pools ) { - if(data.length < 123) { + if (data.length < 123) { revert UniswapV4Executor__InvalidDataLength(); } @@ -168,6 +168,7 @@ contract UniswapV4Executor is IExecutor, V4Router { uint24 fee; int24 tickSpacing; + // slither-disable-next-line assembly assembly { intermediaryToken := mload(add(poolsData, add(offset, 20))) fee := shr(232, mload(add(poolsData, add(offset, 52)))) @@ -178,10 +179,7 @@ contract UniswapV4Executor is IExecutor, V4Router { } } - function _pay(Currency token, address payer, uint256 amount) - internal - override - { + function _pay(Currency token, address, uint256 amount) internal override { IERC20(Currency.unwrap(token)).safeTransfer( address(poolManager), amount ); diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index c9afd56..e35b676 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -104,10 +104,10 @@ contract UniswapV4ExecutorTest is Test, Constants { deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); uint256 usdeBalanceBeforeSwapExecutor = - USDE.balanceOf(address(uniswapV4Exposed)); + USDE.balanceOf(address(uniswapV4Exposed)); UniswapV4Executor.UniswapV4Pool[] memory pools = - new UniswapV4Executor.UniswapV4Pool[](1); + new UniswapV4Executor.UniswapV4Pool[](1); pools[0] = UniswapV4Executor.UniswapV4Pool({ intermediaryToken: USDT_ADDR, fee: uint24(100), @@ -139,11 +139,10 @@ contract UniswapV4ExecutorTest is Test, Constants { deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); uint256 usdeBalanceBeforeSwapExecutor = - USDE.balanceOf(address(uniswapV4Exposed)); - + USDE.balanceOf(address(uniswapV4Exposed)); UniswapV4Executor.UniswapV4Pool[] memory pools = - new UniswapV4Executor.UniswapV4Pool[](2); + new UniswapV4Executor.UniswapV4Pool[](2); pools[0] = UniswapV4Executor.UniswapV4Pool({ intermediaryToken: USDT_ADDR, fee: uint24(100), @@ -171,7 +170,8 @@ contract UniswapV4ExecutorTest is Test, Constants { USDE.balanceOf(address(uniswapV4Exposed)), usdeBalanceBeforeSwapExecutor - amountIn ); - assertTrue(IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut); + assertTrue( + IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut + ); } - } From 69d03f060872bf9cce313f2420dc95c6d7554dec Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 14 Feb 2025 12:28:49 +0000 Subject: [PATCH 5/5] fix: Fix PLE tests that break after foundry update --- don't change below this line --- ENG-4222 Took 12 minutes Took 15 seconds --- foundry/test/LibPrefixLengthEncodedByteArray.t.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol index 123f565..811dcc1 100644 --- a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol +++ b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol @@ -51,14 +51,16 @@ contract LibPrefixLengthEncodedByteArrayTest is Test { assertEq(this.size(multiple), 3); } - function testFailInvalidLength() public view { + function test_RevertIf_InvalidLength() public { // Length prefix larger than remaining data + vm.expectRevert(); bytes memory invalid = hex"0004414243"; this.next(invalid); } - function testFailIncompletePrefix() public view { + function test_RevertIf_IncompletePrefix() public { // Only 1 byte instead of 2 bytes prefix + vm.expectRevert(); bytes memory invalid = hex"01"; this.next(invalid); }