From 582533fa31b1c2096566df00b7e07350f677a647 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 11 Feb 2025 16:33:34 +0000 Subject: [PATCH 01/35] fix: Miscellaneous audit remarks - Move pause functions together - Add missing zero checks - Use openzepplin's sendValues instead of transfer --- don't change below this line --- ENG-4226 Took 25 minutes Took 2 minutes --- foundry/src/TychoRouter.sol | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 4b8aa1f..c2e0b52 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -9,6 +9,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; import "@permit2/src/interfaces/IAllowanceTransfer.sol"; import "@uniswap/v3-updated/CallbackValidationV2.sol"; import "./ExecutionDispatcher.sol"; @@ -65,23 +66,18 @@ contract TychoRouter is address private immutable _usv3Factory; constructor(address _permit2, address weth, address usv3Factory) { + if ( + _permit2 == address(0) || weth == address(0) + || usv3Factory == address(0) + ) { + revert TychoRouter__AddressZero(); + } permit2 = IAllowanceTransfer(_permit2); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _weth = IWETH(weth); - - if (usv3Factory == address(0)) { - revert TychoRouter__AddressZero(); - } _usv3Factory = usv3Factory; } - /** - * @dev Unpauses the contract - */ - function unpause() external onlyRole(UNPAUSER_ROLE) { - _unpause(); - } - /** * @notice Executes a swap operation based on a predefined swap graph, supporting internal token amount splits. * This function enables multi-step swaps, optional ETH wrapping/unwrapping, and validates the output amount @@ -161,8 +157,7 @@ contract TychoRouter is _unwrapETH(amountOut); } if (tokenOut == address(0)) { - // slither-disable-next-line arbitrary-send-eth - payable(receiver).transfer(amountOut); + Address.sendValue(payable(receiver), amountOut); } else { IERC20(tokenOut).safeTransfer(receiver, amountOut); } @@ -257,6 +252,13 @@ contract TychoRouter is _pause(); } + /** + * @dev Unpauses the contract + */ + function unpause() external onlyRole(UNPAUSER_ROLE) { + _unpause(); + } + /** * @dev Allows granting roles to multiple accounts in a single call. */ From 00fe1429521e2aff96760f8d54a9ce93876b0946 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 12 Feb 2025 09:54:29 +0000 Subject: [PATCH 02/35] chore(release): 0.36.2 [skip ci] ## [0.36.2](https://github.com/propeller-heads/tycho-execution/compare/0.36.1...0.36.2) (2025-02-12) ### Bug Fixes * Miscellaneous audit remarks ([582533f](https://github.com/propeller-heads/tycho-execution/commit/582533fa31b1c2096566df00b7e07350f677a647)) --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcd440d..ce82249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [0.36.2](https://github.com/propeller-heads/tycho-execution/compare/0.36.1...0.36.2) (2025-02-12) + + +### Bug Fixes + +* Miscellaneous audit remarks ([582533f](https://github.com/propeller-heads/tycho-execution/commit/582533fa31b1c2096566df00b7e07350f677a647)) + ## [0.36.1](https://github.com/propeller-heads/tycho-execution/compare/0.36.0...0.36.1) (2025-02-11) ## [0.36.0](https://github.com/propeller-heads/tycho-execution/compare/0.35.1...0.36.0) (2025-02-11) diff --git a/Cargo.lock b/Cargo.lock index 0774d3a..1b94f76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4340,7 +4340,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.36.1" +version = "0.36.2" dependencies = [ "alloy", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 77d4746..3c76338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.36.1" +version = "0.36.2" edition = "2021" [[bin]] From cb4c8f4e51d4f1900149288339c6a6fc75a515b1 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Feb 2025 20:21:24 +0530 Subject: [PATCH 03/35] feat: add univ4 executor --- .gitmodules | 3 + foundry/lib/v4-core | 1 + foundry/remappings.txt | 3 +- foundry/src/executors/UniswapV4Executor.sol | 100 ++++++++++++++++++++ 4 files changed, 106 insertions(+), 1 deletion(-) create mode 160000 foundry/lib/v4-core create mode 100644 foundry/src/executors/UniswapV4Executor.sol diff --git a/.gitmodules b/.gitmodules index 0c139fa..ef3de4b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "foundry/lib/v3-core"] path = foundry/lib/v3-core url = https://github.com/Uniswap/v3-core +[submodule "foundry/lib/v4-core"] + path = foundry/lib/v4-core + url = https://github.com/Uniswap/v4-core diff --git a/foundry/lib/v4-core b/foundry/lib/v4-core new file mode 160000 index 0000000..e50237c --- /dev/null +++ b/foundry/lib/v4-core @@ -0,0 +1 @@ +Subproject commit e50237c43811bd9b526eff40f26772152a42daba diff --git a/foundry/remappings.txt b/foundry/remappings.txt index 2d2e7e6..74a44ca 100644 --- a/foundry/remappings.txt +++ b/foundry/remappings.txt @@ -5,4 +5,5 @@ @uniswap-v2/=lib/v2-core/ @balancer-labs/v2-interfaces=lib/balancer-v2-monorepo/pkg/interfaces @uniswap/v3-updated/=lib/v3-updated/ -@uniswap/v3-core/=lib/v3-core/ \ No newline at end of file +@uniswap/v3-core/=lib/v3-core/ +@uniswap/v4-core/=lib/v4-core/src/ \ No newline at end of file diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol new file mode 100644 index 0000000..393c049 --- /dev/null +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import "@interfaces/IExecutor.sol"; +import { + IERC20, + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IPoolManager} from "@uniswap/v4-core/interfaces/IPoolManager.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/types/BalanceDelta.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {IHooks} from "@uniswap/v4-core/interfaces/IHooks.sol"; + +error UniswapV4Executor__InvalidDataLength(); +error UniswapV4Executor__SwapFailed(); + +contract UniswapV4Executor is IExecutor { + using SafeERC20 for IERC20; + using CurrencyLibrary for Currency; + using SafeCast for int128; + using SafeCast for int256; + + uint256 private constant MIN_SQRT_RATIO = 4295128739; + uint256 private constant MAX_SQRT_RATIO = + 1461446703485210103287273052203988822378723970342; + + // slither-disable-next-line locked-ether + function swap(uint256 amountIn, bytes calldata data) + external + payable + returns (uint256 amountOut) + { + ( + address tokenIn, + address tokenOut, + uint24 fee, + address receiver, + address target, + bool zeroForOne + ) = _decodeData(data); + + PoolKey memory key = PoolKey({ + currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut), + currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), + fee: fee, + tickSpacing: 60, // Standard tick spacing + hooks: IHooks(address(0)) // No hooks needed for basic swaps + }); + + IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: int256(amountIn), + sqrtPriceLimitX96: uint160(zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1) + }); + + IPoolManager poolManager = IPoolManager(target); + + try poolManager.swap(key, params, abi.encode(key)) returns (BalanceDelta delta) { + if (zeroForOne) { + amountOut = delta.amount1() < 0 ? (-delta.amount1()).toUint256() : 0; + } else { + amountOut = delta.amount0() < 0 ? (-delta.amount0()).toUint256() : 0; + } + + if (receiver != address(this)) { + IERC20(tokenOut).safeTransfer(receiver, amountOut); + } + } catch { + revert UniswapV4Executor__SwapFailed(); + } + } + + function _decodeData(bytes calldata data) + internal + pure + returns ( + address tokenIn, + address tokenOut, + uint24 fee, + address receiver, + address target, + bool zeroForOne + ) + { + if (data.length != 84) { + revert UniswapV4Executor__InvalidDataLength(); + } + + tokenIn = address(bytes20(data[0:20])); + tokenOut = address(bytes20(data[20:40])); + fee = uint24(bytes3(data[40:43])); + receiver = address(bytes20(data[43:63])); + target = address(bytes20(data[63:83])); + zeroForOne = uint8(data[83]) > 0; + } + + +} From ed90cb4ef1d43e09a7cd6f824ef4214598851b9f Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Feb 2025 21:50:07 +0530 Subject: [PATCH 04/35] feat: add callback --- foundry/src/executors/UniswapV4Executor.sol | 65 +++++++++++++++++---- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 393c049..cc81acc 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -12,11 +12,13 @@ import {PoolKey} from "@uniswap/v4-core/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/types/BalanceDelta.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IHooks} from "@uniswap/v4-core/interfaces/IHooks.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/interfaces/callback/IUnlockCallback.sol"; error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__SwapFailed(); +error UniswapV4Executor__InsufficientOutput(); -contract UniswapV4Executor is IExecutor { +contract UniswapV4Executor is IExecutor, IUnlockCallback { using SafeERC20 for IERC20; using CurrencyLibrary for Currency; using SafeCast for int128; @@ -26,10 +28,16 @@ contract UniswapV4Executor is IExecutor { uint256 private constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; - // slither-disable-next-line locked-ether + struct SwapCallbackData { + PoolKey key; + IPoolManager.SwapParams params; + address tokenIn; + address tokenOut; + address receiver; + } + function swap(uint256 amountIn, bytes calldata data) - external - payable + external payable returns (uint256 amountOut) { ( @@ -55,15 +63,28 @@ contract UniswapV4Executor is IExecutor { sqrtPriceLimitX96: uint160(zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1) }); + // Transfer tokens from sender to this contract + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + + // Approve PoolManager to spend tokens + IERC20(tokenIn).approve(target, amountIn); + + SwapCallbackData memory callbackData = SwapCallbackData({ + key: key, + params: params, + tokenIn: tokenIn, + tokenOut: tokenOut, + receiver: receiver + }); + IPoolManager poolManager = IPoolManager(target); - try poolManager.swap(key, params, abi.encode(key)) returns (BalanceDelta delta) { - if (zeroForOne) { - amountOut = delta.amount1() < 0 ? (-delta.amount1()).toUint256() : 0; - } else { - amountOut = delta.amount0() < 0 ? (-delta.amount0()).toUint256() : 0; - } + try poolManager.unlock(abi.encode(callbackData)) returns (bytes memory result) { + amountOut = abi.decode(result, (uint256)); + + if (amountOut == 0) revert UniswapV4Executor__InsufficientOutput(); + // Transfer output tokens to receiver if not this contract if (receiver != address(this)) { IERC20(tokenOut).safeTransfer(receiver, amountOut); } @@ -72,6 +93,28 @@ contract UniswapV4Executor is IExecutor { } } + function unlockCallback(bytes calldata rawData) external returns (bytes memory) { + SwapCallbackData memory data = abi.decode(rawData, (SwapCallbackData)); + + IPoolManager poolManager = IPoolManager(msg.sender); + + + BalanceDelta delta = poolManager.swap(data.key, data.params, ""); + + + uint256 amountOut; + if (data.params.zeroForOne) { + amountOut = delta.amount1() < 0 ? (-delta.amount1()).toUint256() : 0; + } else { + amountOut = delta.amount0() < 0 ? (-delta.amount0()).toUint256() : 0; + } + + + poolManager.settle(); + + return abi.encode(amountOut); + } + function _decodeData(bytes calldata data) internal pure @@ -95,6 +138,4 @@ contract UniswapV4Executor is IExecutor { target = address(bytes20(data[63:83])); zeroForOne = uint8(data[83]) > 0; } - - } From b2097ca4a5600161166636c7f2b58f845540ed9a Mon Sep 17 00:00:00 2001 From: royvardhan Date: Fri, 7 Feb 2025 19:20:56 +0530 Subject: [PATCH 05/35] feat: handle amounts in unlockCallback --- foundry/foundry.toml | 2 +- foundry/src/executors/UniswapV4Executor.sol | 156 +++++++++++++++++--- 2 files changed, 138 insertions(+), 20 deletions(-) diff --git a/foundry/foundry.toml b/foundry/foundry.toml index a3c8d7b..3d55450 100644 --- a/foundry/foundry.toml +++ b/foundry/foundry.toml @@ -3,7 +3,7 @@ src = 'src' out = 'out' libs = ['lib'] solc = "0.8.28" -evm_version = 'shanghai' +evm_version = 'cancun' optimizer = true optimizer_runs = 1000 via_ir = true diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index cc81acc..99f9e0d 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -12,17 +12,22 @@ import {PoolKey} from "@uniswap/v4-core/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/types/BalanceDelta.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IHooks} from "@uniswap/v4-core/interfaces/IHooks.sol"; -import {IUnlockCallback} from "@uniswap/v4-core/interfaces/callback/IUnlockCallback.sol"; +import {IUnlockCallback} from + "@uniswap/v4-core/interfaces/callback/IUnlockCallback.sol"; +import {TransientStateLibrary} from + "@uniswap/v4-core/libraries/TransientStateLibrary.sol"; error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__SwapFailed(); error UniswapV4Executor__InsufficientOutput(); +error UniswapV4Executor__ManagerMismatch(); contract UniswapV4Executor is IExecutor, IUnlockCallback { using SafeERC20 for IERC20; using CurrencyLibrary for Currency; using SafeCast for int128; using SafeCast for int256; + using TransientStateLibrary for IPoolManager; uint256 private constant MIN_SQRT_RATIO = 4295128739; uint256 private constant MAX_SQRT_RATIO = @@ -37,7 +42,8 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { } function swap(uint256 amountIn, bytes calldata data) - external payable + external + payable returns (uint256 amountOut) { ( @@ -54,19 +60,19 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), fee: fee, tickSpacing: 60, // Standard tick spacing - hooks: IHooks(address(0)) // No hooks needed for basic swaps + hooks: IHooks(address(0)) // No hooks needed for basic swaps }); IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ zeroForOne: zeroForOne, amountSpecified: int256(amountIn), - sqrtPriceLimitX96: uint160(zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1) + sqrtPriceLimitX96: uint160( + zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1 + ) }); - // Transfer tokens from sender to this contract + // TODO: Find a better place IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); - - // Approve PoolManager to spend tokens IERC20(tokenIn).approve(target, amountIn); SwapCallbackData memory callbackData = SwapCallbackData({ @@ -78,10 +84,12 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { }); IPoolManager poolManager = IPoolManager(target); - - try poolManager.unlock(abi.encode(callbackData)) returns (bytes memory result) { + + try poolManager.unlock(abi.encode(callbackData)) returns ( + bytes memory result + ) { amountOut = abi.decode(result, (uint256)); - + if (amountOut == 0) revert UniswapV4Executor__InsufficientOutput(); // Transfer output tokens to receiver if not this contract @@ -93,24 +101,119 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { } } - function unlockCallback(bytes calldata rawData) external returns (bytes memory) { + // Dev notes: This is inspired by the Uniswap V4 PoolSwapTest.sol + function unlockCallback(bytes calldata rawData) + external + returns (bytes memory) + { SwapCallbackData memory data = abi.decode(rawData, (SwapCallbackData)); - + IPoolManager poolManager = IPoolManager(msg.sender); - - + + // Check initial balances + (,, int256 deltaBefore0) = _fetchBalances( + data.key.currency0, data.receiver, address(this), poolManager + ); + (,, int256 deltaBefore1) = _fetchBalances( + data.key.currency1, data.receiver, address(this), poolManager + ); + + require(deltaBefore0 == 0, "deltaBefore0 not zero"); + require(deltaBefore1 == 0, "deltaBefore1 not zero"); + BalanceDelta delta = poolManager.swap(data.key, data.params, ""); - + // Check final balances and validate based on swap direction + (,, int256 deltaAfter0) = _fetchBalances( + data.key.currency0, data.receiver, address(this), poolManager + ); + (,, int256 deltaAfter1) = _fetchBalances( + data.key.currency1, data.receiver, address(this), poolManager + ); + uint256 amountOut; if (data.params.zeroForOne) { - amountOut = delta.amount1() < 0 ? (-delta.amount1()).toUint256() : 0; + if (data.params.amountSpecified < 0) { + // exact input, 0 for 1 + require( + deltaAfter0 >= data.params.amountSpecified, + "insufficient input amount" + ); + require(delta.amount0() == deltaAfter0, "delta mismatch"); + require(deltaAfter1 >= 0, "negative output amount"); + amountOut = deltaAfter1 > 0 ? uint256(deltaAfter1) : 0; + } else { + // exact output, 0 for 1 + require(deltaAfter0 <= 0, "positive input amount"); + require(delta.amount1() == deltaAfter1, "delta mismatch"); + require( + deltaAfter1 <= data.params.amountSpecified, + "excessive output amount" + ); + amountOut = uint256((-delta.amount1()).toUint256()); + } } else { - amountOut = delta.amount0() < 0 ? (-delta.amount0()).toUint256() : 0; + if (data.params.amountSpecified < 0) { + // exact input, 1 for 0 + require( + deltaAfter1 >= data.params.amountSpecified, + "insufficient input amount" + ); + require(delta.amount1() == deltaAfter1, "delta mismatch"); + require(deltaAfter0 >= 0, "negative output amount"); + amountOut = deltaAfter0 > 0 ? uint256(deltaAfter0) : 0; + } else { + // exact output, 1 for 0 + require(deltaAfter1 <= 0, "positive input amount"); + require(delta.amount0() == deltaAfter0, "delta mismatch"); + require( + deltaAfter0 <= data.params.amountSpecified, + "excessive output amount" + ); + amountOut = uint256((-delta.amount0()).toUint256()); + } } - - poolManager.settle(); + if (deltaAfter0 < 0) { + poolManager.settle{ + value: data.key.currency0.isAddressZero() + ? uint256(-deltaAfter0) + : 0 + }(); + if (!data.key.currency0.isAddressZero()) { + IERC20(Currency.unwrap(data.key.currency0)).transfer( + address(poolManager), uint256(-deltaAfter0) + ); + } + } + if (deltaAfter1 < 0) { + poolManager.settle{ + value: data.key.currency1.isAddressZero() + ? uint256(-deltaAfter1) + : 0 + }(); + if (!data.key.currency1.isAddressZero()) { + IERC20(Currency.unwrap(data.key.currency1)).transfer( + address(poolManager), uint256(-deltaAfter1) + ); + } + } + if (deltaAfter0 > 0) { + poolManager.take( + data.key.currency0, data.receiver, uint256(deltaAfter0) + ); + } + if (deltaAfter1 > 0) { + poolManager.take( + data.key.currency1, data.receiver, uint256(deltaAfter1) + ); + } + + // Handle any remaining ETH balance + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) { + CurrencyLibrary.ADDRESS_ZERO.transfer(data.receiver, ethBalance); + } return abi.encode(amountOut); } @@ -138,4 +241,19 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { target = address(bytes20(data[63:83])); zeroForOne = uint8(data[83]) > 0; } + + function _fetchBalances( + Currency currency, + address user, + address deltaHolder, + IPoolManager manager + ) + internal + view + returns (uint256 userBalance, uint256 poolBalance, int256 delta) + { + userBalance = currency.balanceOf(user); + poolBalance = currency.balanceOf(address(manager)); + delta = manager.currencyDelta(deltaHolder, currency); + } } From 1a36c33bc614d744cfa161dd85d6cccc671e592e Mon Sep 17 00:00:00 2001 From: royvardhan Date: Mon, 10 Feb 2025 11:13:49 +0530 Subject: [PATCH 06/35] fix: rm callback fn --- foundry/src/executors/UniswapV4Executor.sol | 162 ++------------------ 1 file changed, 11 insertions(+), 151 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 99f9e0d..1eb4921 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -2,27 +2,22 @@ pragma solidity ^0.8.28; import "@interfaces/IExecutor.sol"; -import { - IERC20, - SafeERC20 -} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IPoolManager} from "@uniswap/v4-core/interfaces/IPoolManager.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/types/Currency.sol"; import {PoolKey} from "@uniswap/v4-core/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/types/BalanceDelta.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IHooks} from "@uniswap/v4-core/interfaces/IHooks.sol"; -import {IUnlockCallback} from - "@uniswap/v4-core/interfaces/callback/IUnlockCallback.sol"; -import {TransientStateLibrary} from - "@uniswap/v4-core/libraries/TransientStateLibrary.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/interfaces/callback/IUnlockCallback.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/libraries/TransientStateLibrary.sol"; error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__SwapFailed(); error UniswapV4Executor__InsufficientOutput(); error UniswapV4Executor__ManagerMismatch(); -contract UniswapV4Executor is IExecutor, IUnlockCallback { +contract UniswapV4Executor is IExecutor { using SafeERC20 for IERC20; using CurrencyLibrary for Currency; using SafeCast for int128; @@ -41,11 +36,10 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { address receiver; } - function swap(uint256 amountIn, bytes calldata data) - external - payable - returns (uint256 amountOut) - { + function swap( + uint256 amountIn, + bytes calldata data + ) external payable returns (uint256 amountOut) { ( address tokenIn, address tokenOut, @@ -71,10 +65,6 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { ) }); - // TODO: Find a better place - IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); - IERC20(tokenIn).approve(target, amountIn); - SwapCallbackData memory callbackData = SwapCallbackData({ key: key, params: params, @@ -101,124 +91,9 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { } } - // Dev notes: This is inspired by the Uniswap V4 PoolSwapTest.sol - function unlockCallback(bytes calldata rawData) - external - returns (bytes memory) - { - SwapCallbackData memory data = abi.decode(rawData, (SwapCallbackData)); - - IPoolManager poolManager = IPoolManager(msg.sender); - - // Check initial balances - (,, int256 deltaBefore0) = _fetchBalances( - data.key.currency0, data.receiver, address(this), poolManager - ); - (,, int256 deltaBefore1) = _fetchBalances( - data.key.currency1, data.receiver, address(this), poolManager - ); - - require(deltaBefore0 == 0, "deltaBefore0 not zero"); - require(deltaBefore1 == 0, "deltaBefore1 not zero"); - - BalanceDelta delta = poolManager.swap(data.key, data.params, ""); - - // Check final balances and validate based on swap direction - (,, int256 deltaAfter0) = _fetchBalances( - data.key.currency0, data.receiver, address(this), poolManager - ); - (,, int256 deltaAfter1) = _fetchBalances( - data.key.currency1, data.receiver, address(this), poolManager - ); - - uint256 amountOut; - if (data.params.zeroForOne) { - if (data.params.amountSpecified < 0) { - // exact input, 0 for 1 - require( - deltaAfter0 >= data.params.amountSpecified, - "insufficient input amount" - ); - require(delta.amount0() == deltaAfter0, "delta mismatch"); - require(deltaAfter1 >= 0, "negative output amount"); - amountOut = deltaAfter1 > 0 ? uint256(deltaAfter1) : 0; - } else { - // exact output, 0 for 1 - require(deltaAfter0 <= 0, "positive input amount"); - require(delta.amount1() == deltaAfter1, "delta mismatch"); - require( - deltaAfter1 <= data.params.amountSpecified, - "excessive output amount" - ); - amountOut = uint256((-delta.amount1()).toUint256()); - } - } else { - if (data.params.amountSpecified < 0) { - // exact input, 1 for 0 - require( - deltaAfter1 >= data.params.amountSpecified, - "insufficient input amount" - ); - require(delta.amount1() == deltaAfter1, "delta mismatch"); - require(deltaAfter0 >= 0, "negative output amount"); - amountOut = deltaAfter0 > 0 ? uint256(deltaAfter0) : 0; - } else { - // exact output, 1 for 0 - require(deltaAfter1 <= 0, "positive input amount"); - require(delta.amount0() == deltaAfter0, "delta mismatch"); - require( - deltaAfter0 <= data.params.amountSpecified, - "excessive output amount" - ); - amountOut = uint256((-delta.amount0()).toUint256()); - } - } - - if (deltaAfter0 < 0) { - poolManager.settle{ - value: data.key.currency0.isAddressZero() - ? uint256(-deltaAfter0) - : 0 - }(); - if (!data.key.currency0.isAddressZero()) { - IERC20(Currency.unwrap(data.key.currency0)).transfer( - address(poolManager), uint256(-deltaAfter0) - ); - } - } - if (deltaAfter1 < 0) { - poolManager.settle{ - value: data.key.currency1.isAddressZero() - ? uint256(-deltaAfter1) - : 0 - }(); - if (!data.key.currency1.isAddressZero()) { - IERC20(Currency.unwrap(data.key.currency1)).transfer( - address(poolManager), uint256(-deltaAfter1) - ); - } - } - if (deltaAfter0 > 0) { - poolManager.take( - data.key.currency0, data.receiver, uint256(deltaAfter0) - ); - } - if (deltaAfter1 > 0) { - poolManager.take( - data.key.currency1, data.receiver, uint256(deltaAfter1) - ); - } - - // Handle any remaining ETH balance - uint256 ethBalance = address(this).balance; - if (ethBalance > 0) { - CurrencyLibrary.ADDRESS_ZERO.transfer(data.receiver, ethBalance); - } - - return abi.encode(amountOut); - } - - function _decodeData(bytes calldata data) + function _decodeData( + bytes calldata data + ) internal pure returns ( @@ -241,19 +116,4 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { target = address(bytes20(data[63:83])); zeroForOne = uint8(data[83]) > 0; } - - function _fetchBalances( - Currency currency, - address user, - address deltaHolder, - IPoolManager manager - ) - internal - view - returns (uint256 userBalance, uint256 poolBalance, int256 delta) - { - userBalance = currency.balanceOf(user); - poolBalance = currency.balanceOf(address(manager)); - delta = manager.currencyDelta(deltaHolder, currency); - } } From 24d4e762a2841909245d7a4434c13f37398ae482 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Mon, 10 Feb 2025 11:16:31 +0530 Subject: [PATCH 07/35] fix: rm redundant transfer --- foundry/src/executors/UniswapV4Executor.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 1eb4921..7b1556e 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -81,11 +81,6 @@ contract UniswapV4Executor is IExecutor { amountOut = abi.decode(result, (uint256)); if (amountOut == 0) revert UniswapV4Executor__InsufficientOutput(); - - // Transfer output tokens to receiver if not this contract - if (receiver != address(this)) { - IERC20(tokenOut).safeTransfer(receiver, amountOut); - } } catch { revert UniswapV4Executor__SwapFailed(); } From bdd3daffba3853ad084f7d3454e3c72fd6a1679c Mon Sep 17 00:00:00 2001 From: royvardhan Date: Mon, 10 Feb 2025 20:21:59 +0530 Subject: [PATCH 08/35] feat: update solc and add V4Router into UniswapV4Executor --- .gitmodules | 3 ++ foundry/foundry.toml | 2 +- foundry/interfaces/ICallbackVerifier.sol | 12 ++--- foundry/interfaces/IExecutor.sol | 10 ++-- foundry/lib/IWETH.sol | 2 +- foundry/lib/LibSwap.sol | 48 +++++++------------ .../bytes/LibPrefixLengthEncodedByteArray.sol | 24 +++++----- foundry/lib/v4-periphery | 1 + foundry/remappings.txt | 3 +- .../src/CallbackVerificationDispatcher.sol | 2 +- foundry/src/ExecutionDispatcher.sol | 2 +- foundry/src/TychoRouter.sol | 2 +- foundry/src/executors/BalancerV2Executor.sol | 2 +- foundry/src/executors/UniswapV2Executor.sol | 2 +- foundry/src/executors/UniswapV3Executor.sol | 2 +- foundry/src/executors/UniswapV4Executor.sol | 31 ++++++++---- .../test/CallbackVerificationDispatcher.t.sol | 2 +- foundry/test/Constants.sol | 2 +- foundry/test/ExecutionDispatcher.t.sol | 2 +- .../LibPrefixLengthEncodedByteArray.t.sol | 2 +- foundry/test/LibSwap.t.sol | 2 +- foundry/test/TychoRouter.t.sol | 2 +- foundry/test/TychoRouterTestSetup.sol | 2 +- .../test/executors/BalancerV2Executor.t.sol | 2 +- .../test/executors/UniswapV2Executor.t.sol | 2 +- .../test/executors/UniswapV3Executor.t.sol | 2 +- foundry/test/mock/MockERC20.sol | 2 +- 27 files changed, 86 insertions(+), 84 deletions(-) create mode 160000 foundry/lib/v4-periphery diff --git a/.gitmodules b/.gitmodules index ef3de4b..a1835d6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "foundry/lib/v4-core"] path = foundry/lib/v4-core url = https://github.com/Uniswap/v4-core +[submodule "foundry/lib/universal-router"] + path = foundry/lib/universal-router + url = https://github.com/uniswap/universal-router diff --git a/foundry/foundry.toml b/foundry/foundry.toml index 3d55450..e573137 100644 --- a/foundry/foundry.toml +++ b/foundry/foundry.toml @@ -2,7 +2,7 @@ src = 'src' out = 'out' libs = ['lib'] -solc = "0.8.28" +auto_detect_sol = true evm_version = 'cancun' optimizer = true optimizer_runs = 1000 diff --git a/foundry/interfaces/ICallbackVerifier.sol b/foundry/interfaces/ICallbackVerifier.sol index 6161e55..612279c 100644 --- a/foundry/interfaces/ICallbackVerifier.sol +++ b/foundry/interfaces/ICallbackVerifier.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; interface ICallbackVerifier { error UnauthorizedCaller(string exchange, address sender); @@ -7,10 +7,8 @@ interface ICallbackVerifier { /** * @dev This method should revert if the sender is not a verified sender of the exchange. */ - function verifyCallback(address sender, bytes calldata data) - external - returns ( - uint256 amountOwed, - address tokenOwed - ); + function verifyCallback( + address sender, + bytes calldata data + ) external returns (uint256 amountOwed, address tokenOwed); } diff --git a/foundry/interfaces/IExecutor.sol b/foundry/interfaces/IExecutor.sol index 9c5a55c..0a60022 100644 --- a/foundry/interfaces/IExecutor.sol +++ b/foundry/interfaces/IExecutor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -20,10 +20,10 @@ interface IExecutor { * @return calculatedAmount The amount of the output token swapped, depending on * the givenAmount inputted. */ - function swap(uint256 givenAmount, bytes calldata data) - external - payable - returns (uint256 calculatedAmount); + function swap( + uint256 givenAmount, + bytes calldata data + ) external payable returns (uint256 calculatedAmount); } interface IExecutorErrors { diff --git a/foundry/lib/IWETH.sol b/foundry/lib/IWETH.sol index 3b4a359..8ab98fa 100644 --- a/foundry/lib/IWETH.sol +++ b/foundry/lib/IWETH.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/foundry/lib/LibSwap.sol b/foundry/lib/LibSwap.sol index f581d8c..1770005 100644 --- a/foundry/lib/LibSwap.sol +++ b/foundry/lib/LibSwap.sol @@ -1,58 +1,44 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; library LibSwap { /// Returns the InToken index into an array of tokens - function tokenInIndex(bytes calldata swap) - internal - pure - returns (uint8 res) - { + function tokenInIndex( + bytes calldata swap + ) internal pure returns (uint8 res) { res = uint8(swap[0]); } /// The OutToken index into an array of tokens - function tokenOutIndex(bytes calldata swap) - internal - pure - returns (uint8 res) - { + function tokenOutIndex( + bytes calldata swap + ) internal pure returns (uint8 res) { res = uint8(swap[1]); } /// The relative amount of token quantity routed into this swap - function splitPercentage(bytes calldata swap) - internal - pure - returns (uint24 res) - { + function splitPercentage( + bytes calldata swap + ) internal pure returns (uint24 res) { res = uint24(bytes3(swap[2:5])); } /// The address of the executor contract - function executor(bytes calldata swap) - internal - pure - returns (address res) - { + function executor(bytes calldata swap) internal pure returns (address res) { res = address(uint160(bytes20(swap[5:25]))); } /// The selector to be used of the executor contract - function executorSelector(bytes calldata swap) - internal - pure - returns (bytes4 res) - { + function executorSelector( + bytes calldata swap + ) internal pure returns (bytes4 res) { res = bytes4(swap[25:29]); } /// Remaining bytes are interpreted as protocol data - function protocolData(bytes calldata swap) - internal - pure - returns (bytes calldata res) - { + function protocolData( + bytes calldata swap + ) internal pure returns (bytes calldata res) { res = swap[29:]; } } diff --git a/foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol b/foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol index b5773fd..7368b58 100644 --- a/foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol +++ b/foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; /** * @title Propellerheads PrefixLengthEncoded Byte Array Library @@ -16,11 +16,9 @@ library LibPrefixLengthEncodedByteArray { /** * @dev Pop the first element of an array and returns it with the remaining data. */ - function next(bytes calldata encoded) - internal - pure - returns (bytes calldata elem, bytes calldata res) - { + function next( + bytes calldata encoded + ) internal pure returns (bytes calldata elem, bytes calldata res) { assembly { switch iszero(encoded.length) case 1 { @@ -46,7 +44,11 @@ library LibPrefixLengthEncodedByteArray { assembly { let offset := encoded.offset let end := add(encoded.offset, encoded.length) - for {} lt(offset, end) {} { + for { + + } lt(offset, end) { + + } { offset := add(offset, add(shr(240, calldataload(offset)), 2)) s := add(s, 1) } @@ -56,11 +58,9 @@ library LibPrefixLengthEncodedByteArray { /** * @dev Cast an encoded array into a Solidity array. */ - function toArray(bytes calldata encoded) - internal - pure - returns (bytes[] memory arr) - { + function toArray( + bytes calldata encoded + ) internal pure returns (bytes[] memory arr) { bytes calldata elem; uint256 idx = 0; arr = new bytes[](LibPrefixLengthEncodedByteArray.size(encoded)); diff --git a/foundry/lib/v4-periphery b/foundry/lib/v4-periphery new file mode 160000 index 0000000..cf451c4 --- /dev/null +++ b/foundry/lib/v4-periphery @@ -0,0 +1 @@ +Subproject commit cf451c4f55f36ea64c2007d331e3a3574225fc8b diff --git a/foundry/remappings.txt b/foundry/remappings.txt index 74a44ca..04240ad 100644 --- a/foundry/remappings.txt +++ b/foundry/remappings.txt @@ -6,4 +6,5 @@ @balancer-labs/v2-interfaces=lib/balancer-v2-monorepo/pkg/interfaces @uniswap/v3-updated/=lib/v3-updated/ @uniswap/v3-core/=lib/v3-core/ -@uniswap/v4-core/=lib/v4-core/src/ \ No newline at end of file +@uniswap/v4-core/=lib/v4-core/ +@uniswap/v4-periphery/=lib/v4-periphery/ \ No newline at end of file diff --git a/foundry/src/CallbackVerificationDispatcher.sol b/foundry/src/CallbackVerificationDispatcher.sol index 87ccc8e..8df5848 100644 --- a/foundry/src/CallbackVerificationDispatcher.sol +++ b/foundry/src/CallbackVerificationDispatcher.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@interfaces/ICallbackVerifier.sol"; diff --git a/foundry/src/ExecutionDispatcher.sol b/foundry/src/ExecutionDispatcher.sol index 9e084c0..73bd554 100644 --- a/foundry/src/ExecutionDispatcher.sol +++ b/foundry/src/ExecutionDispatcher.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index c2e0b52..311c916 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "../lib/IWETH.sol"; import "../lib/bytes/LibPrefixLengthEncodedByteArray.sol"; diff --git a/foundry/src/executors/BalancerV2Executor.sol b/foundry/src/executors/BalancerV2Executor.sol index 79777cb..14340cf 100644 --- a/foundry/src/executors/BalancerV2Executor.sol +++ b/foundry/src/executors/BalancerV2Executor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; import { diff --git a/foundry/src/executors/UniswapV2Executor.sol b/foundry/src/executors/UniswapV2Executor.sol index 9f447f1..7239a8a 100644 --- a/foundry/src/executors/UniswapV2Executor.sol +++ b/foundry/src/executors/UniswapV2Executor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/foundry/src/executors/UniswapV3Executor.sol b/foundry/src/executors/UniswapV3Executor.sol index 64ae64d..ac986c5 100644 --- a/foundry/src/executors/UniswapV3Executor.sol +++ b/foundry/src/executors/UniswapV3Executor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 7b1556e..618d3f2 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -1,23 +1,24 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IPoolManager} from "@uniswap/v4-core/interfaces/IPoolManager.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/types/Currency.sol"; -import {PoolKey} from "@uniswap/v4-core/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/types/BalanceDelta.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IHooks} from "@uniswap/v4-core/interfaces/IHooks.sol"; -import {IUnlockCallback} from "@uniswap/v4-core/interfaces/callback/IUnlockCallback.sol"; -import {TransientStateLibrary} from "@uniswap/v4-core/libraries/TransientStateLibrary.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; +import {V4Router} from "@uniswap/v4-periphery/src/V4Router.sol"; error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__SwapFailed(); error UniswapV4Executor__InsufficientOutput(); error UniswapV4Executor__ManagerMismatch(); -contract UniswapV4Executor is IExecutor { +contract UniswapV4Executor is IExecutor, V4Router { using SafeERC20 for IERC20; using CurrencyLibrary for Currency; using SafeCast for int128; @@ -36,6 +37,8 @@ contract UniswapV4Executor is IExecutor { address receiver; } + constructor(IPoolManager _poolManager) V4Router(_poolManager) {} + function swap( uint256 amountIn, bytes calldata data @@ -111,4 +114,14 @@ contract UniswapV4Executor is IExecutor { target = address(bytes20(data[63:83])); zeroForOne = uint8(data[83]) > 0; } + + function _pay( + Currency token, + address payer, + uint256 amount + ) internal override {} + + function msgSender() public view override returns (address) { + return msg.sender; + } } diff --git a/foundry/test/CallbackVerificationDispatcher.t.sol b/foundry/test/CallbackVerificationDispatcher.t.sol index 7a99f13..fd2577e 100644 --- a/foundry/test/CallbackVerificationDispatcher.t.sol +++ b/foundry/test/CallbackVerificationDispatcher.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@src/CallbackVerificationDispatcher.sol"; import "./TychoRouterTestSetup.sol"; diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 835f4d6..f526d64 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "forge-std/Test.sol"; diff --git a/foundry/test/ExecutionDispatcher.t.sol b/foundry/test/ExecutionDispatcher.t.sol index a88dc44..7f2fb81 100644 --- a/foundry/test/ExecutionDispatcher.t.sol +++ b/foundry/test/ExecutionDispatcher.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@src/ExecutionDispatcher.sol"; import "./TychoRouterTestSetup.sol"; diff --git a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol index d56b298..123f565 100644 --- a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol +++ b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.26; import {Test} from "forge-std/Test.sol"; import {LibPrefixLengthEncodedByteArray} from diff --git a/foundry/test/LibSwap.t.sol b/foundry/test/LibSwap.t.sol index eedc931..0627740 100644 --- a/foundry/test/LibSwap.t.sol +++ b/foundry/test/LibSwap.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "forge-std/Test.sol"; import "../lib/LibSwap.sol"; diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 489d366..f7df52a 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import {TychoRouter} from "@src/TychoRouter.sol"; import "./TychoRouterTestSetup.sol"; diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 839e3ee..2d63fe2 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.26; import "../src/executors/UniswapV2Executor.sol"; import "./Constants.sol"; diff --git a/foundry/test/executors/BalancerV2Executor.t.sol b/foundry/test/executors/BalancerV2Executor.t.sol index f537cbf..117b153 100644 --- a/foundry/test/executors/BalancerV2Executor.t.sol +++ b/foundry/test/executors/BalancerV2Executor.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@src/executors/BalancerV2Executor.sol"; import {Test} from "../../lib/forge-std/src/Test.sol"; diff --git a/foundry/test/executors/UniswapV2Executor.t.sol b/foundry/test/executors/UniswapV2Executor.t.sol index 765d79c..dbcffbc 100644 --- a/foundry/test/executors/UniswapV2Executor.t.sol +++ b/foundry/test/executors/UniswapV2Executor.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@src/executors/UniswapV2Executor.sol"; import {Test} from "../../lib/forge-std/src/Test.sol"; diff --git a/foundry/test/executors/UniswapV3Executor.t.sol b/foundry/test/executors/UniswapV3Executor.t.sol index b73d580..4c9c1e9 100644 --- a/foundry/test/executors/UniswapV3Executor.t.sol +++ b/foundry/test/executors/UniswapV3Executor.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@src/executors/UniswapV3Executor.sol"; import {Test} from "../../lib/forge-std/src/Test.sol"; diff --git a/foundry/test/mock/MockERC20.sol b/foundry/test/mock/MockERC20.sol index 013c439..1ba6525 100644 --- a/foundry/test/mock/MockERC20.sol +++ b/foundry/test/mock/MockERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; From e62c332451d7cf0d2fc471faa7af7b26fd1a000d Mon Sep 17 00:00:00 2001 From: royvardhan Date: Mon, 10 Feb 2025 21:24:35 +0530 Subject: [PATCH 09/35] feat: add router params --- .gitmodules | 5 +- foundry/lib/Constants.sol | 29 ++++++ foundry/lib/Payments.sol | 110 ++++++++++++++++++++ foundry/lib/PaymentsImmutables.sol | 23 ++++ foundry/lib/Permit2Payments.sol | 57 ++++++++++ foundry/src/executors/UniswapV4Executor.sol | 79 +++++++------- 6 files changed, 262 insertions(+), 41 deletions(-) create mode 100644 foundry/lib/Constants.sol create mode 100644 foundry/lib/Payments.sol create mode 100644 foundry/lib/PaymentsImmutables.sol create mode 100644 foundry/lib/Permit2Payments.sol diff --git a/.gitmodules b/.gitmodules index a1835d6..5360e9e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/OpenZeppelin/openzeppelin-contracts [submodule "foundry/lib/permit2"] path = foundry/lib/permit2 - url = https://github.com/Uniswap/permit2 + url = https://github.com/uniswap/permit2 [submodule "foundry/lib/v2-core"] path = foundry/lib/v2-core url = https://github.com/uniswap/v2-core @@ -16,6 +16,3 @@ [submodule "foundry/lib/v4-core"] path = foundry/lib/v4-core url = https://github.com/Uniswap/v4-core -[submodule "foundry/lib/universal-router"] - path = foundry/lib/universal-router - url = https://github.com/uniswap/universal-router diff --git a/foundry/lib/Constants.sol b/foundry/lib/Constants.sol new file mode 100644 index 0000000..c801b58 --- /dev/null +++ b/foundry/lib/Constants.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +/// @title Constant state +/// @notice Constant state used by the Universal Router +library Constants { + /// @dev Used for identifying cases when a v2 pair has already received input tokens + uint256 internal constant ALREADY_PAID = 0; + + /// @dev Used as a flag for identifying the transfer of ETH instead of a token + address internal constant ETH = address(0); + + /// @dev The length of the bytes encoded address + uint256 internal constant ADDR_SIZE = 20; + + /// @dev The length of the bytes encoded fee + uint256 internal constant V3_FEE_SIZE = 3; + + /// @dev The offset of a single token address (20) and pool fee (3) + uint256 internal constant NEXT_V3_POOL_OFFSET = ADDR_SIZE + V3_FEE_SIZE; + + /// @dev The offset of an encoded pool key + /// Token (20) + Fee (3) + Token (20) = 43 + uint256 internal constant V3_POP_OFFSET = NEXT_V3_POOL_OFFSET + ADDR_SIZE; + + /// @dev The minimum length of an encoding that contains 2 or more pools + uint256 internal constant MULTIPLE_V3_POOLS_MIN_LENGTH = + V3_POP_OFFSET + NEXT_V3_POOL_OFFSET; +} diff --git a/foundry/lib/Payments.sol b/foundry/lib/Payments.sol new file mode 100644 index 0000000..8027dbf --- /dev/null +++ b/foundry/lib/Payments.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import {Constants} from "./Constants.sol"; +import {ActionConstants} from "@uniswap/v4-periphery/src/libraries/ActionConstants.sol"; +import {BipsLibrary} from "@uniswap/v4-periphery/src/libraries/BipsLibrary.sol"; +import {PaymentsImmutables} from "./PaymentsImmutables.sol"; +import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol"; +import {ERC20} from "solmate/src/tokens/ERC20.sol"; + +/// @title Payments contract +/// @notice Performs various operations around the payment of ETH and tokens +abstract contract Payments is PaymentsImmutables { + using SafeTransferLib for ERC20; + using SafeTransferLib for address; + using BipsLibrary for uint256; + + error InsufficientToken(); + error InsufficientETH(); + + /// @notice Pays an amount of ETH or ERC20 to a recipient + /// @param token The token to pay (can be ETH using Constants.ETH) + /// @param recipient The address that will receive the payment + /// @param value The amount to pay + function pay(address token, address recipient, uint256 value) internal { + if (token == Constants.ETH) { + recipient.safeTransferETH(value); + } else { + if (value == ActionConstants.CONTRACT_BALANCE) { + value = ERC20(token).balanceOf(address(this)); + } + + ERC20(token).safeTransfer(recipient, value); + } + } + + /// @notice Pays a proportion of the contract's ETH or ERC20 to a recipient + /// @param token The token to pay (can be ETH using Constants.ETH) + /// @param recipient The address that will receive payment + /// @param bips Portion in bips of whole balance of the contract + function payPortion( + address token, + address recipient, + uint256 bips + ) internal { + if (token == Constants.ETH) { + uint256 balance = address(this).balance; + uint256 amount = balance.calculatePortion(bips); + recipient.safeTransferETH(amount); + } else { + uint256 balance = ERC20(token).balanceOf(address(this)); + uint256 amount = balance.calculatePortion(bips); + ERC20(token).safeTransfer(recipient, amount); + } + } + + /// @notice Sweeps all of the contract's ERC20 or ETH to an address + /// @param token The token to sweep (can be ETH using Constants.ETH) + /// @param recipient The address that will receive payment + /// @param amountMinimum The minimum desired amount + function sweep( + address token, + address recipient, + uint256 amountMinimum + ) internal { + uint256 balance; + if (token == Constants.ETH) { + balance = address(this).balance; + if (balance < amountMinimum) revert InsufficientETH(); + if (balance > 0) recipient.safeTransferETH(balance); + } else { + balance = ERC20(token).balanceOf(address(this)); + if (balance < amountMinimum) revert InsufficientToken(); + if (balance > 0) ERC20(token).safeTransfer(recipient, balance); + } + } + + /// @notice Wraps an amount of ETH into WETH + /// @param recipient The recipient of the WETH + /// @param amount The amount to wrap (can be CONTRACT_BALANCE) + function wrapETH(address recipient, uint256 amount) internal { + if (amount == ActionConstants.CONTRACT_BALANCE) { + amount = address(this).balance; + } else if (amount > address(this).balance) { + revert InsufficientETH(); + } + if (amount > 0) { + WETH9.deposit{value: amount}(); + if (recipient != address(this)) { + WETH9.transfer(recipient, amount); + } + } + } + + /// @notice Unwraps all of the contract's WETH into ETH + /// @param recipient The recipient of the ETH + /// @param amountMinimum The minimum amount of ETH desired + function unwrapWETH9(address recipient, uint256 amountMinimum) internal { + uint256 value = WETH9.balanceOf(address(this)); + if (value < amountMinimum) { + revert InsufficientETH(); + } + if (value > 0) { + WETH9.withdraw(value); + if (recipient != address(this)) { + recipient.safeTransferETH(value); + } + } + } +} diff --git a/foundry/lib/PaymentsImmutables.sol b/foundry/lib/PaymentsImmutables.sol new file mode 100644 index 0000000..1894169 --- /dev/null +++ b/foundry/lib/PaymentsImmutables.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import {IWETH9} from "@uniswap/v4-periphery/src/interfaces/external/IWETH9.sol"; +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; + +struct PaymentsParameters { + address permit2; + address weth9; +} + +contract PaymentsImmutables { + /// @notice WETH9 address + IWETH9 internal immutable WETH9; + + /// @notice Permit2 address + IPermit2 internal immutable PERMIT2; + + constructor(PaymentsParameters memory params) { + WETH9 = IWETH9(params.weth9); + PERMIT2 = IPermit2(params.permit2); + } +} diff --git a/foundry/lib/Permit2Payments.sol b/foundry/lib/Permit2Payments.sol new file mode 100644 index 0000000..edb09e2 --- /dev/null +++ b/foundry/lib/Permit2Payments.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {SafeCast160} from "permit2/src/libraries/SafeCast160.sol"; +import {Payments} from "./Payments.sol"; + +/// @title Payments through Permit2 +/// @notice Performs interactions with Permit2 to transfer tokens +abstract contract Permit2Payments is Payments { + using SafeCast160 for uint256; + + error FromAddressIsNotOwner(); + + /// @notice Performs a transferFrom on Permit2 + /// @param token The token to transfer + /// @param from The address to transfer from + /// @param to The recipient of the transfer + /// @param amount The amount to transfer + function permit2TransferFrom( + address token, + address from, + address to, + uint160 amount + ) internal { + PERMIT2.transferFrom(from, to, amount, token); + } + + /// @notice Performs a batch transferFrom on Permit2 + /// @param batchDetails An array detailing each of the transfers that should occur + /// @param owner The address that should be the owner of all transfers + function permit2TransferFrom( + IAllowanceTransfer.AllowanceTransferDetails[] calldata batchDetails, + address owner + ) internal { + uint256 batchLength = batchDetails.length; + for (uint256 i = 0; i < batchLength; ++i) { + if (batchDetails[i].from != owner) revert FromAddressIsNotOwner(); + } + PERMIT2.transferFrom(batchDetails); + } + + /// @notice Either performs a regular payment or transferFrom on Permit2, depending on the payer address + /// @param token The token to transfer + /// @param payer The address to pay for the transfer + /// @param recipient The recipient of the transfer + /// @param amount The amount to transfer + function payOrPermit2Transfer( + address token, + address payer, + address recipient, + uint256 amount + ) internal { + if (payer == address(this)) pay(token, recipient, amount); + else permit2TransferFrom(token, payer, recipient, amount.toUint160()); + } +} diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 618d3f2..5144803 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -12,6 +12,9 @@ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; 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 {Permit2Payments} from "../../lib/Permit2Payments.sol"; error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__SwapFailed(); @@ -29,14 +32,6 @@ contract UniswapV4Executor is IExecutor, V4Router { uint256 private constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; - struct SwapCallbackData { - PoolKey key; - IPoolManager.SwapParams params; - address tokenIn; - address tokenOut; - address receiver; - } - constructor(IPoolManager _poolManager) V4Router(_poolManager) {} function swap( @@ -47,11 +42,12 @@ contract UniswapV4Executor is IExecutor, V4Router { address tokenIn, address tokenOut, uint24 fee, - address receiver, - address target, + address receiver, // TODO: Investigate bool zeroForOne ) = _decodeData(data); + uint128 amountIn128 = uint128(amountIn); + uint128 amountOut128 = uint128(amountOut); PoolKey memory key = PoolKey({ currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut), currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), @@ -60,33 +56,42 @@ contract UniswapV4Executor is IExecutor, V4Router { hooks: IHooks(address(0)) // No hooks needed for basic swaps }); - IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ - zeroForOne: zeroForOne, - amountSpecified: int256(amountIn), - sqrtPriceLimitX96: uint160( - zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1 - ) - }); + bytes memory actions = abi.encodePacked( + uint8(Actions.SWAP_EXACT_IN_SINGLE), + uint8(Actions.SETTLE_ALL), + uint8(Actions.TAKE_ALL) + ); - SwapCallbackData memory callbackData = SwapCallbackData({ - key: key, - params: params, - tokenIn: tokenIn, - tokenOut: tokenOut, - receiver: receiver - }); + bytes[] memory params = new bytes[](3); - IPoolManager poolManager = IPoolManager(target); + params[0] = abi.encode( + IV4Router.ExactInputSingleParams({ + poolKey: key, + zeroForOne: zeroForOne, + amountIn: amountIn128, + amountOutMinimum: amountOut128, + hookData: bytes("") + }) + ); - try poolManager.unlock(abi.encode(callbackData)) returns ( - bytes memory result - ) { - amountOut = abi.decode(result, (uint256)); + params[1] = abi.encode(key.currency0, amountIn128); + params[2] = abi.encode(key.currency1, amountOut128); - if (amountOut == 0) revert UniswapV4Executor__InsufficientOutput(); - } catch { + // Convert the encoded parameters to calldata format + bytes memory encodedActions = abi.encode(actions, params); + (bool success, ) = address(this).call( + abi.encodeWithSelector(this.executeActions.selector, encodedActions) + ); + + if (!success) { revert UniswapV4Executor__SwapFailed(); } + + return amountOut; + } + + function executeActions(bytes calldata actions) external { + _executeActions(actions); } function _decodeData( @@ -99,27 +104,27 @@ contract UniswapV4Executor is IExecutor, V4Router { address tokenOut, uint24 fee, address receiver, - address target, bool zeroForOne ) { - if (data.length != 84) { + if (data.length != 64) { revert UniswapV4Executor__InvalidDataLength(); } - tokenIn = address(bytes20(data[0:20])); + tokenIn = address(bytes20(data[:20])); tokenOut = address(bytes20(data[20:40])); fee = uint24(bytes3(data[40:43])); receiver = address(bytes20(data[43:63])); - target = address(bytes20(data[63:83])); - zeroForOne = uint8(data[83]) > 0; + zeroForOne = uint8(bytes1(data[63])) > 0; } function _pay( Currency token, address payer, uint256 amount - ) internal override {} + ) internal override { + // TODO: Implement + } function msgSender() public view override returns (address) { return msg.sender; From 4599f07df0d4c4131f87cd41ec7bcb8b1dd47bde Mon Sep 17 00:00:00 2001 From: royvardhan Date: Mon, 10 Feb 2025 23:08:29 +0530 Subject: [PATCH 10/35] feat: add test for UniswapV4Executor --- foundry/src/executors/UniswapV4Executor.sol | 44 +++------ foundry/test/Constants.sol | 3 + .../test/executors/UniswapV4Executor.t.sol | 96 +++++++++++++++++++ 3 files changed, 112 insertions(+), 31 deletions(-) create mode 100644 foundry/test/executors/UniswapV4Executor.t.sol diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 5144803..00ca24b 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -6,31 +6,17 @@ import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeE import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; -import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; 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 {Permit2Payments} from "../../lib/Permit2Payments.sol"; error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__SwapFailed(); -error UniswapV4Executor__InsufficientOutput(); -error UniswapV4Executor__ManagerMismatch(); contract UniswapV4Executor is IExecutor, V4Router { using SafeERC20 for IERC20; using CurrencyLibrary for Currency; - using SafeCast for int128; - using SafeCast for int256; - using TransientStateLibrary for IPoolManager; - - uint256 private constant MIN_SQRT_RATIO = 4295128739; - uint256 private constant MAX_SQRT_RATIO = - 1461446703485210103287273052203988822378723970342; constructor(IPoolManager _poolManager) V4Router(_poolManager) {} @@ -42,8 +28,9 @@ contract UniswapV4Executor is IExecutor, V4Router { address tokenIn, address tokenOut, uint24 fee, - address receiver, // TODO: Investigate - bool zeroForOne + address receiver, // TODO: This is not used right now + bool zeroForOne, + uint24 tickSpacing ) = _decodeData(data); uint128 amountIn128 = uint128(amountIn); @@ -52,8 +39,8 @@ contract UniswapV4Executor is IExecutor, V4Router { currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut), currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), fee: fee, - tickSpacing: 60, // Standard tick spacing - hooks: IHooks(address(0)) // No hooks needed for basic swaps + tickSpacing: int24(tickSpacing), + hooks: IHooks(address(0)) }); bytes memory actions = abi.encodePacked( @@ -77,20 +64,13 @@ contract UniswapV4Executor is IExecutor, V4Router { params[1] = abi.encode(key.currency0, amountIn128); params[2] = abi.encode(key.currency1, amountOut128); - // Convert the encoded parameters to calldata format - bytes memory encodedActions = abi.encode(actions, params); - (bool success, ) = address(this).call( - abi.encodeWithSelector(this.executeActions.selector, encodedActions) - ); - - if (!success) { - revert UniswapV4Executor__SwapFailed(); - } + this.executeActions(abi.encode(actions, params)); + // TODO: This is still hardcode to zero, find a way to return the actual amount out return amountOut; } - function executeActions(bytes calldata actions) external { + function executeActions(bytes calldata actions) public { _executeActions(actions); } @@ -104,10 +84,11 @@ contract UniswapV4Executor is IExecutor, V4Router { address tokenOut, uint24 fee, address receiver, - bool zeroForOne + bool zeroForOne, + uint24 tickSpacing ) { - if (data.length != 64) { + if (data.length != 67) { revert UniswapV4Executor__InvalidDataLength(); } @@ -116,6 +97,7 @@ contract UniswapV4Executor is IExecutor, V4Router { fee = uint24(bytes3(data[40:43])); receiver = address(bytes20(data[43:63])); zeroForOne = uint8(bytes1(data[63])) > 0; + tickSpacing = uint24(bytes3(data[64:67])); } function _pay( @@ -123,7 +105,7 @@ contract UniswapV4Executor is IExecutor, V4Router { address payer, uint256 amount ) internal override { - // TODO: Implement + token.transfer(payer, amount); } function msgSender() public view override returns (address) { diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index f526d64..ba8fbef 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -27,6 +27,9 @@ contract Constants is Test { address BAL_ADDR = address(0xba100000625a3754423978a60c9317c58a424e3D); address USDC_ADDR = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); address WBTC_ADDR = address(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + address INCH_ADDR = address(0x111111111117dC0aa78b770fA6A738034120C302); + address USDE_ADDR = address(0x4c9EDD5852cd905f086C759E8383e09bff1E68B3); + address USDT_ADDR = address(0xdAC17F958D2ee523a2206206994597C13D831ec7); // uniswap v2 address WETH_DAI_POOL = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11; diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol new file mode 100644 index 0000000..dad6c2f --- /dev/null +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import "@src/executors/UniswapV4Executor.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "../Constants.sol"; + +contract UniswapV4ExecutorExposed is UniswapV4Executor { + constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {} + + function decodeData( + bytes calldata data + ) + external + pure + returns ( + address tokenIn, + address tokenOut, + uint24 fee, + address receiver, + bool zeroForOne, + uint24 tickSpacing + ) + { + return _decodeData(data); + } +} + +contract UniswapV4ExecutorTest is Test, Constants { + using SafeERC20 for IERC20; + + UniswapV4ExecutorExposed uniswapV4Exposed; + IERC20 USDE = IERC20(USDE_ADDR); + IERC20 USDT = IERC20(USDT_ADDR); + + function setUp() public { + uint256 forkBlock = 21817316; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + uniswapV4Exposed = new UniswapV4ExecutorExposed( + IPoolManager(0x000000000004444c5dc75cB358380D2e3dE08A90) + ); + } + + function testDecodeParamsUniswapV4() public view { + uint24 expectedPoolFee = 500; + bytes memory data = abi.encodePacked( + USDE_ADDR, + USDT_ADDR, + expectedPoolFee, + address(2), + false, + int24(1) + ); + + ( + address tokenIn, + address tokenOut, + uint24 fee, + address receiver, + bool zeroForOne, + uint24 tickSpacing + ) = uniswapV4Exposed.decodeData(data); + + assertEq(tokenIn, USDE_ADDR); + assertEq(tokenOut, USDT_ADDR); + assertEq(fee, expectedPoolFee); + assertEq(receiver, address(2)); + assertEq(zeroForOne, false); + assertEq(tickSpacing, 1); + } + + function testDecodeParamsInvalidDataLength() public { + bytes memory data = abi.encodePacked(USDE_ADDR, USDT_ADDR); + + vm.expectRevert(UniswapV4Executor__InvalidDataLength.selector); + uniswapV4Exposed.decodeData(data); + } + + function testSwapUniswapV4() public { + uint256 amountIn = 1 ether; + deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); + assertEq(USDE.balanceOf(address(uniswapV4Exposed)), amountIn); + + bytes memory data = abi.encodePacked( + USDE_ADDR, + USDT_ADDR, + uint24(100), // 0.01% fee tier + address(this), + true, + int24(1) + ); + + uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); + assertEq(USDE.balanceOf(address(uniswapV4Exposed)), 0); + } +} From 7ca647f009ad8cb71c6e8a08e64ee02285c9ae08 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 11 Feb 2025 00:36:56 +0530 Subject: [PATCH 11/35] feat: add new pair test --- .../test/executors/UniswapV4Executor.t.sol | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index dad6c2f..7439367 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -77,6 +77,7 @@ contract UniswapV4ExecutorTest is Test, Constants { } function testSwapUniswapV4() public { + vm.startPrank(BOB); uint256 amountIn = 1 ether; deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); assertEq(USDE.balanceOf(address(uniswapV4Exposed)), amountIn); @@ -92,5 +93,28 @@ contract UniswapV4ExecutorTest is Test, Constants { uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); assertEq(USDE.balanceOf(address(uniswapV4Exposed)), 0); + vm.stopPrank(); + } + + function testSwapUniswapV4With1Inch() public { + vm.startPrank(BOB); + uint256 amountIn = 1 ether; + deal(INCH_ADDR, address(uniswapV4Exposed), amountIn); + assertEq( + IERC20(INCH_ADDR).balanceOf(address(uniswapV4Exposed)), + amountIn + ); + + bytes memory data = abi.encodePacked( + INCH_ADDR, + USDC_ADDR, + uint24(10000), // 0.01% fee tier + address(this), + true, + int24(200) + ); + + uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); + assertEq(IERC20(INCH_ADDR).balanceOf(address(uniswapV4Exposed)), 0); } } From ae0b07b2a47b93430841ce8bf437215d2f94e3bb Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 11 Feb 2025 18:39:12 +0530 Subject: [PATCH 12/35] fix: reciever issue --- foundry/src/executors/UniswapV4Executor.sol | 48 +++++++++------- .../test/executors/UniswapV4Executor.t.sol | 55 ++++++------------- 2 files changed, 46 insertions(+), 57 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 00ca24b..bee3dcb 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -2,14 +2,20 @@ pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; -import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { + IERC20, + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import { + Currency, CurrencyLibrary +} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; 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 "forge-std/console.sol"; error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__SwapFailed(); @@ -20,21 +26,23 @@ contract UniswapV4Executor is IExecutor, V4Router { constructor(IPoolManager _poolManager) V4Router(_poolManager) {} - function swap( - uint256 amountIn, - bytes calldata data - ) external payable returns (uint256 amountOut) { + function swap(uint256 amountIn, bytes calldata data) + external + payable + returns (uint256 amountOut) + { ( address tokenIn, address tokenOut, uint24 fee, - address receiver, // TODO: This is not used right now + address receiver, bool zeroForOne, uint24 tickSpacing ) = _decodeData(data); uint128 amountIn128 = uint128(amountIn); - uint128 amountOut128 = uint128(amountOut); + uint256 balanceBefore = IERC20(tokenOut).balanceOf(receiver); + PoolKey memory key = PoolKey({ currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut), currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), @@ -46,7 +54,7 @@ contract UniswapV4Executor is IExecutor, V4Router { bytes memory actions = abi.encodePacked( uint8(Actions.SWAP_EXACT_IN_SINGLE), uint8(Actions.SETTLE_ALL), - uint8(Actions.TAKE_ALL) + uint8(Actions.TAKE) ); bytes[] memory params = new bytes[](3); @@ -56,17 +64,20 @@ contract UniswapV4Executor is IExecutor, V4Router { poolKey: key, zeroForOne: zeroForOne, amountIn: amountIn128, - amountOutMinimum: amountOut128, + amountOutMinimum: 0, hookData: bytes("") }) ); params[1] = abi.encode(key.currency0, amountIn128); - params[2] = abi.encode(key.currency1, amountOut128); + params[2] = abi.encode(key.currency1, receiver, 0); this.executeActions(abi.encode(actions, params)); - // TODO: This is still hardcode to zero, find a way to return the actual amount out + amountOut = IERC20(tokenOut).balanceOf(receiver) - balanceBefore; + + if (amountOut == 0) revert UniswapV4Executor__SwapFailed(); + return amountOut; } @@ -74,9 +85,7 @@ contract UniswapV4Executor is IExecutor, V4Router { _executeActions(actions); } - function _decodeData( - bytes calldata data - ) + function _decodeData(bytes calldata data) internal pure returns ( @@ -100,11 +109,10 @@ contract UniswapV4Executor is IExecutor, V4Router { tickSpacing = uint24(bytes3(data[64:67])); } - function _pay( - Currency token, - address payer, - uint256 amount - ) internal override { + function _pay(Currency token, address payer, uint256 amount) + internal + override + { token.transfer(payer, amount); } diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 7439367..c0eca39 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -4,13 +4,12 @@ pragma solidity ^0.8.26; import "@src/executors/UniswapV4Executor.sol"; import {Test} from "../../lib/forge-std/src/Test.sol"; import {Constants} from "../Constants.sol"; +import {console} from "forge-std/console.sol"; contract UniswapV4ExecutorExposed is UniswapV4Executor { constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {} - function decodeData( - bytes calldata data - ) + function decodeData(bytes calldata data) external pure returns ( @@ -32,24 +31,19 @@ contract UniswapV4ExecutorTest is Test, Constants { UniswapV4ExecutorExposed uniswapV4Exposed; IERC20 USDE = IERC20(USDE_ADDR); IERC20 USDT = IERC20(USDT_ADDR); + address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90; function setUp() public { uint256 forkBlock = 21817316; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - uniswapV4Exposed = new UniswapV4ExecutorExposed( - IPoolManager(0x000000000004444c5dc75cB358380D2e3dE08A90) - ); + uniswapV4Exposed = + new UniswapV4ExecutorExposed(IPoolManager(poolManager)); } function testDecodeParamsUniswapV4() public view { uint24 expectedPoolFee = 500; bytes memory data = abi.encodePacked( - USDE_ADDR, - USDT_ADDR, - expectedPoolFee, - address(2), - false, - int24(1) + USDE_ADDR, USDT_ADDR, expectedPoolFee, address(2), false, int24(1) ); ( @@ -78,43 +72,30 @@ contract UniswapV4ExecutorTest is Test, Constants { function testSwapUniswapV4() public { vm.startPrank(BOB); - uint256 amountIn = 1 ether; + uint256 amountIn = 100 ether; deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); - assertEq(USDE.balanceOf(address(uniswapV4Exposed)), amountIn); + uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); + uint256 usdeBalanceBeforeSwapExecutor = + USDE.balanceOf(address(uniswapV4Exposed)); + assertEq(usdeBalanceBeforeSwapExecutor, amountIn); + uint256 usdtBalanceBeforeSwapBob = USDT.balanceOf(address(BOB)); + assertEq(usdtBalanceBeforeSwapBob, 0); bytes memory data = abi.encodePacked( USDE_ADDR, USDT_ADDR, uint24(100), // 0.01% fee tier - address(this), + BOB, true, int24(1) ); uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); - assertEq(USDE.balanceOf(address(uniswapV4Exposed)), 0); - vm.stopPrank(); - } - - function testSwapUniswapV4With1Inch() public { - vm.startPrank(BOB); - uint256 amountIn = 1 ether; - deal(INCH_ADDR, address(uniswapV4Exposed), amountIn); + assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); assertEq( - IERC20(INCH_ADDR).balanceOf(address(uniswapV4Exposed)), - amountIn + USDE.balanceOf(address(uniswapV4Exposed)), + usdeBalanceBeforeSwapExecutor - amountIn ); - - bytes memory data = abi.encodePacked( - INCH_ADDR, - USDC_ADDR, - uint24(10000), // 0.01% fee tier - address(this), - true, - int24(200) - ); - - uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); - assertEq(IERC20(INCH_ADDR).balanceOf(address(uniswapV4Exposed)), 0); + assertTrue(USDT.balanceOf(BOB) == amountOut && amountOut > 0); } } From c264084783561b3de4eeac413ed6155076ff11d5 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 11 Feb 2025 19:27:47 +0530 Subject: [PATCH 13/35] feat: move encoding to test --- foundry/src/executors/UniswapV4Executor.sol | 103 +++++++----------- .../test/executors/UniswapV4Executor.t.sol | 83 +++++++++++--- 2 files changed, 102 insertions(+), 84 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index bee3dcb..dda2e45 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -2,14 +2,9 @@ pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; -import { - IERC20, - SafeERC20 -} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import { - Currency, CurrencyLibrary -} from "@uniswap/v4-core/src/types/Currency.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {V4Router} from "@uniswap/v4-periphery/src/V4Router.sol"; @@ -26,56 +21,17 @@ contract UniswapV4Executor is IExecutor, V4Router { constructor(IPoolManager _poolManager) V4Router(_poolManager) {} - function swap(uint256 amountIn, bytes calldata data) - external - payable - returns (uint256 amountOut) - { - ( - address tokenIn, - address tokenOut, - uint24 fee, - address receiver, - bool zeroForOne, - uint24 tickSpacing - ) = _decodeData(data); + function swap( + uint256, + bytes calldata data + ) external payable returns (uint256 amountOut) { + (, address tokenOut, , address receiver, , ) = _decodeData(data); - uint128 amountIn128 = uint128(amountIn); uint256 balanceBefore = IERC20(tokenOut).balanceOf(receiver); - 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 actions = abi.encodePacked( - uint8(Actions.SWAP_EXACT_IN_SINGLE), - uint8(Actions.SETTLE_ALL), - uint8(Actions.TAKE) - ); - - bytes[] memory params = new bytes[](3); - - params[0] = abi.encode( - IV4Router.ExactInputSingleParams({ - poolKey: key, - zeroForOne: zeroForOne, - amountIn: amountIn128, - amountOutMinimum: 0, - hookData: bytes("") - }) - ); - - params[1] = abi.encode(key.currency0, amountIn128); - params[2] = abi.encode(key.currency1, receiver, 0); - - this.executeActions(abi.encode(actions, params)); + this.executeActions(data); amountOut = IERC20(tokenOut).balanceOf(receiver) - balanceBefore; - if (amountOut == 0) revert UniswapV4Executor__SwapFailed(); return amountOut; @@ -85,7 +41,9 @@ contract UniswapV4Executor is IExecutor, V4Router { _executeActions(actions); } - function _decodeData(bytes calldata data) + function _decodeData( + bytes calldata data + ) internal pure returns ( @@ -97,22 +55,35 @@ contract UniswapV4Executor is IExecutor, V4Router { uint24 tickSpacing ) { - if (data.length != 67) { - revert UniswapV4Executor__InvalidDataLength(); - } + (, bytes[] memory params) = abi.decode(data, (bytes, bytes[])); - tokenIn = address(bytes20(data[:20])); - tokenOut = address(bytes20(data[20:40])); - fee = uint24(bytes3(data[40:43])); - receiver = address(bytes20(data[43:63])); - zeroForOne = uint8(bytes1(data[63])) > 0; - tickSpacing = uint24(bytes3(data[64:67])); + IV4Router.ExactInputSingleParams memory swapParams = abi.decode( + params[0], + (IV4Router.ExactInputSingleParams) + ); + + (, address _receiver, ) = abi.decode( + params[2], + (Currency, address, uint256) + ); + + 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())); + fee = swapParams.poolKey.fee; + receiver = _receiver; + zeroForOne = swapParams.zeroForOne; + tickSpacing = uint24(swapParams.poolKey.tickSpacing); } - function _pay(Currency token, address payer, uint256 amount) - internal - override - { + function _pay( + Currency token, + address payer, + uint256 amount + ) internal override { token.transfer(payer, amount); } diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index c0eca39..d525148 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -9,7 +9,9 @@ import {console} from "forge-std/console.sol"; contract UniswapV4ExecutorExposed is UniswapV4Executor { constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {} - function decodeData(bytes calldata data) + function decodeData( + bytes calldata data + ) external pure returns ( @@ -36,14 +38,23 @@ contract UniswapV4ExecutorTest is Test, Constants { function setUp() public { uint256 forkBlock = 21817316; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - uniswapV4Exposed = - new UniswapV4ExecutorExposed(IPoolManager(poolManager)); + uniswapV4Exposed = new UniswapV4ExecutorExposed( + IPoolManager(poolManager) + ); } function testDecodeParamsUniswapV4() public view { uint24 expectedPoolFee = 500; - bytes memory data = abi.encodePacked( - USDE_ADDR, USDT_ADDR, expectedPoolFee, address(2), false, int24(1) + address expectedReceiver = address(2); + + bytes memory data = _encodeExactInputSingle( + USDE_ADDR, + USDT_ADDR, + expectedPoolFee, + expectedReceiver, + false, + 1, + 100 // amountIn doesn't matter for this test ); ( @@ -58,36 +69,31 @@ contract UniswapV4ExecutorTest is Test, Constants { assertEq(tokenIn, USDE_ADDR); assertEq(tokenOut, USDT_ADDR); assertEq(fee, expectedPoolFee); - assertEq(receiver, address(2)); + assertEq(receiver, expectedReceiver); assertEq(zeroForOne, false); assertEq(tickSpacing, 1); } - function testDecodeParamsInvalidDataLength() public { - bytes memory data = abi.encodePacked(USDE_ADDR, USDT_ADDR); - - vm.expectRevert(UniswapV4Executor__InvalidDataLength.selector); - uniswapV4Exposed.decodeData(data); - } - function testSwapUniswapV4() public { vm.startPrank(BOB); uint256 amountIn = 100 ether; deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); - uint256 usdeBalanceBeforeSwapExecutor = - USDE.balanceOf(address(uniswapV4Exposed)); + uint256 usdeBalanceBeforeSwapExecutor = USDE.balanceOf( + address(uniswapV4Exposed) + ); assertEq(usdeBalanceBeforeSwapExecutor, amountIn); uint256 usdtBalanceBeforeSwapBob = USDT.balanceOf(address(BOB)); assertEq(usdtBalanceBeforeSwapBob, 0); - bytes memory data = abi.encodePacked( + bytes memory data = _encodeExactInputSingle( USDE_ADDR, USDT_ADDR, - uint24(100), // 0.01% fee tier + 100, BOB, true, - int24(1) + 1, + uint128(amountIn) ); uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); @@ -98,4 +104,45 @@ contract UniswapV4ExecutorTest is Test, Constants { ); assertTrue(USDT.balanceOf(BOB) == amountOut && amountOut > 0); } + + function _encodeExactInputSingle( + address tokenIn, + address tokenOut, + uint24 fee, + address receiver, + bool zeroForOne, + uint24 tickSpacing, + uint128 amountIn + ) internal 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 actions = abi.encodePacked( + uint8(Actions.SWAP_EXACT_IN_SINGLE), + uint8(Actions.SETTLE_ALL), + uint8(Actions.TAKE) + ); + + 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, receiver, 0); + + return abi.encode(actions, params); + } } From d998c88cfef300e41714c6c3c6164e761d14e2de Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 11 Feb 2025 20:01:09 +0530 Subject: [PATCH 14/35] feat: support multi swap decoding --- foundry/src/executors/UniswapV4Executor.sol | 145 +++++++++++++----- .../test/executors/UniswapV4Executor.t.sol | 38 ++--- 2 files changed, 126 insertions(+), 57 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index dda2e45..53196b9 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -2,15 +2,20 @@ pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; -import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { + IERC20, + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import { + Currency, CurrencyLibrary +} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; 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 "forge-std/console.sol"; +import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol"; error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__SwapFailed(); @@ -21,19 +26,34 @@ contract UniswapV4Executor is IExecutor, V4Router { constructor(IPoolManager _poolManager) V4Router(_poolManager) {} - function swap( - uint256, - bytes calldata data - ) external payable returns (uint256 amountOut) { - (, address tokenOut, , address receiver, , ) = _decodeData(data); + function swap(uint256, bytes calldata data) + external + payable + returns (uint256 amountOut) + { + ( + , + address tokenOut, + , + address receiver, + , + , + bool isExactInput, + , + uint256 amount + ) = _decodeData(data); uint256 balanceBefore = IERC20(tokenOut).balanceOf(receiver); this.executeActions(data); - amountOut = IERC20(tokenOut).balanceOf(receiver) - balanceBefore; - if (amountOut == 0) revert UniswapV4Executor__SwapFailed(); + if (isExactInput) { + amountOut = IERC20(tokenOut).balanceOf(receiver) - balanceBefore; + } else { + amountOut = amount; + } + if (amountOut == 0) revert UniswapV4Executor__SwapFailed(); return amountOut; } @@ -41,9 +61,7 @@ contract UniswapV4Executor is IExecutor, V4Router { _executeActions(actions); } - function _decodeData( - bytes calldata data - ) + function _decodeData(bytes calldata data) internal pure returns ( @@ -52,38 +70,89 @@ contract UniswapV4Executor is IExecutor, V4Router { uint24 fee, address receiver, bool zeroForOne, - uint24 tickSpacing + uint24 tickSpacing, + bool isExactInput, + bool isSingle, + uint256 amount ) { - (, bytes[] memory params) = abi.decode(data, (bytes, bytes[])); + (bytes memory actions, bytes[] memory params) = + abi.decode(data, (bytes, bytes[])); - IV4Router.ExactInputSingleParams memory swapParams = abi.decode( - params[0], - (IV4Router.ExactInputSingleParams) - ); + // First byte of actions determines the swap type + uint8 action = uint8(bytes1(actions[0])); - (, address _receiver, ) = abi.decode( - params[2], - (Currency, address, uint256) - ); - - 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())); - fee = swapParams.poolKey.fee; + // Get receiver from params[2] for all cases + (, address _receiver,) = + abi.decode(params[2], (Currency, address, uint256)); receiver = _receiver; - zeroForOne = swapParams.zeroForOne; - tickSpacing = uint24(swapParams.poolKey.tickSpacing); + + if (action == uint8(Actions.SWAP_EXACT_IN_SINGLE)) { + IV4Router.ExactInputSingleParams memory swapParams = + abi.decode(params[0], (IV4Router.ExactInputSingleParams)); + + 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())); + fee = swapParams.poolKey.fee; + zeroForOne = swapParams.zeroForOne; + tickSpacing = uint24(swapParams.poolKey.tickSpacing); + isExactInput = true; + isSingle = 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())); + fee = swapParams.poolKey.fee; + zeroForOne = swapParams.zeroForOne; + tickSpacing = uint24(swapParams.poolKey.tickSpacing); + isExactInput = false; + isSingle = true; + 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())); + fee = lastPath.fee; + zeroForOne = tokenIn < tokenOut; + tickSpacing = uint24(lastPath.tickSpacing); + isExactInput = true; + isSingle = false; + amount = swapParams.amountIn; + } else if (action == uint8(Actions.SWAP_EXACT_OUT)) { + IV4Router.ExactOutputParams memory swapParams = + abi.decode(params[0], (IV4Router.ExactOutputParams)); + + tokenOut = address(uint160(swapParams.currencyOut.toId())); + PathKey memory firstPath = swapParams.path[0]; + tokenIn = address(uint160(firstPath.intermediateCurrency.toId())); + fee = firstPath.fee; + zeroForOne = tokenIn < tokenOut; + tickSpacing = uint24(firstPath.tickSpacing); + isExactInput = false; + isSingle = false; + amount = swapParams.amountOut; + } } - function _pay( - Currency token, - address payer, - uint256 amount - ) internal override { + function _pay(Currency token, address payer, uint256 amount) + internal + override + { token.transfer(payer, amount); } diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index d525148..4d232c2 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -9,9 +9,7 @@ import {console} from "forge-std/console.sol"; contract UniswapV4ExecutorExposed is UniswapV4Executor { constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {} - function decodeData( - bytes calldata data - ) + function decodeData(bytes calldata data) external pure returns ( @@ -20,7 +18,10 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor { uint24 fee, address receiver, bool zeroForOne, - uint24 tickSpacing + uint24 tickSpacing, + bool isExactInput, + bool isSingle, + uint256 amount ) { return _decodeData(data); @@ -38,14 +39,14 @@ contract UniswapV4ExecutorTest is Test, Constants { function setUp() public { uint256 forkBlock = 21817316; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - uniswapV4Exposed = new UniswapV4ExecutorExposed( - IPoolManager(poolManager) - ); + uniswapV4Exposed = + new UniswapV4ExecutorExposed(IPoolManager(poolManager)); } function testDecodeParamsUniswapV4() public view { uint24 expectedPoolFee = 500; address expectedReceiver = address(2); + uint128 expectedAmount = 100; bytes memory data = _encodeExactInputSingle( USDE_ADDR, @@ -54,7 +55,7 @@ contract UniswapV4ExecutorTest is Test, Constants { expectedReceiver, false, 1, - 100 // amountIn doesn't matter for this test + expectedAmount ); ( @@ -63,7 +64,10 @@ contract UniswapV4ExecutorTest is Test, Constants { uint24 fee, address receiver, bool zeroForOne, - uint24 tickSpacing + uint24 tickSpacing, + bool isExactInput, + bool isSingle, + uint256 amount ) = uniswapV4Exposed.decodeData(data); assertEq(tokenIn, USDE_ADDR); @@ -72,6 +76,9 @@ contract UniswapV4ExecutorTest is Test, Constants { assertEq(receiver, expectedReceiver); assertEq(zeroForOne, false); assertEq(tickSpacing, 1); + assertTrue(isExactInput); + assertTrue(isSingle); + assertEq(amount, expectedAmount); } function testSwapUniswapV4() public { @@ -79,21 +86,14 @@ contract UniswapV4ExecutorTest is Test, Constants { uint256 amountIn = 100 ether; deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); - uint256 usdeBalanceBeforeSwapExecutor = USDE.balanceOf( - address(uniswapV4Exposed) - ); + uint256 usdeBalanceBeforeSwapExecutor = + USDE.balanceOf(address(uniswapV4Exposed)); assertEq(usdeBalanceBeforeSwapExecutor, amountIn); uint256 usdtBalanceBeforeSwapBob = USDT.balanceOf(address(BOB)); assertEq(usdtBalanceBeforeSwapBob, 0); bytes memory data = _encodeExactInputSingle( - USDE_ADDR, - USDT_ADDR, - 100, - BOB, - true, - 1, - uint128(amountIn) + USDE_ADDR, USDT_ADDR, 100, BOB, true, 1, uint128(amountIn) ); uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); From a8cc84ddce7c90aa40d69090577ef15cc95d8edf Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 11 Feb 2025 21:52:18 +0530 Subject: [PATCH 15/35] fix: git submodules and strict equality check in v4 executor --- .gitmodules | 3 +++ foundry/src/executors/UniswapV4Executor.sol | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 5360e9e..eacb977 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "foundry/lib/v4-core"] path = foundry/lib/v4-core url = https://github.com/Uniswap/v4-core +[submodule "foundry/lib/v4-periphery"] + path = foundry/lib/v4-periphery + url = https://github.com/Uniswap/v4-periphery diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 53196b9..b56c2ed 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -47,13 +47,19 @@ contract UniswapV4Executor is IExecutor, V4Router { this.executeActions(data); + uint256 balanceAfter = IERC20(tokenOut).balanceOf(receiver); + if (isExactInput) { - amountOut = IERC20(tokenOut).balanceOf(receiver) - balanceBefore; + amountOut = balanceAfter - balanceBefore; } else { amountOut = amount; } - if (amountOut == 0) revert UniswapV4Executor__SwapFailed(); + // Checks if the amountOut is not 0. + // Slither does not allow strict equality checks. + if (amountOut < 1) { + revert UniswapV4Executor__SwapFailed(); + } return amountOut; } From 120c96215fbff073c870052500183327e23faec8 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 11 Feb 2025 22:02:59 +0530 Subject: [PATCH 16/35] chore: remove redundant data from _decodeData --- foundry/src/executors/UniswapV4Executor.sol | 43 +------------------ .../test/executors/UniswapV4Executor.t.sol | 23 +--------- 2 files changed, 4 insertions(+), 62 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index b56c2ed..8da2a8b 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -31,17 +31,8 @@ contract UniswapV4Executor is IExecutor, V4Router { payable returns (uint256 amountOut) { - ( - , - address tokenOut, - , - address receiver, - , - , - bool isExactInput, - , - uint256 amount - ) = _decodeData(data); + (address tokenOut, address receiver, bool isExactInput, uint256 amount) + = _decodeData(data); uint256 balanceBefore = IERC20(tokenOut).balanceOf(receiver); @@ -71,14 +62,9 @@ contract UniswapV4Executor is IExecutor, V4Router { internal pure returns ( - address tokenIn, address tokenOut, - uint24 fee, address receiver, - bool zeroForOne, - uint24 tickSpacing, bool isExactInput, - bool isSingle, uint256 amount ) { @@ -97,60 +83,35 @@ contract UniswapV4Executor is IExecutor, V4Router { IV4Router.ExactInputSingleParams memory swapParams = abi.decode(params[0], (IV4Router.ExactInputSingleParams)); - 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())); - fee = swapParams.poolKey.fee; - zeroForOne = swapParams.zeroForOne; - tickSpacing = uint24(swapParams.poolKey.tickSpacing); isExactInput = true; - isSingle = 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())); - fee = swapParams.poolKey.fee; - zeroForOne = swapParams.zeroForOne; - tickSpacing = uint24(swapParams.poolKey.tickSpacing); isExactInput = false; - isSingle = true; 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())); - fee = lastPath.fee; - zeroForOne = tokenIn < tokenOut; - tickSpacing = uint24(lastPath.tickSpacing); isExactInput = true; - isSingle = false; amount = swapParams.amountIn; } else if (action == uint8(Actions.SWAP_EXACT_OUT)) { IV4Router.ExactOutputParams memory swapParams = abi.decode(params[0], (IV4Router.ExactOutputParams)); tokenOut = address(uint160(swapParams.currencyOut.toId())); - PathKey memory firstPath = swapParams.path[0]; - tokenIn = address(uint160(firstPath.intermediateCurrency.toId())); - fee = firstPath.fee; - zeroForOne = tokenIn < tokenOut; - tickSpacing = uint24(firstPath.tickSpacing); isExactInput = false; - isSingle = false; amount = swapParams.amountOut; } } diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 4d232c2..c7658fb 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -13,14 +13,9 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor { external pure returns ( - address tokenIn, address tokenOut, - uint24 fee, address receiver, - bool zeroForOne, - uint24 tickSpacing, bool isExactInput, - bool isSingle, uint256 amount ) { @@ -58,26 +53,12 @@ contract UniswapV4ExecutorTest is Test, Constants { expectedAmount ); - ( - address tokenIn, - address tokenOut, - uint24 fee, - address receiver, - bool zeroForOne, - uint24 tickSpacing, - bool isExactInput, - bool isSingle, - uint256 amount - ) = uniswapV4Exposed.decodeData(data); + (address tokenOut, address receiver, bool isExactInput, uint256 amount) + = uniswapV4Exposed.decodeData(data); - assertEq(tokenIn, USDE_ADDR); assertEq(tokenOut, USDT_ADDR); - assertEq(fee, expectedPoolFee); assertEq(receiver, expectedReceiver); - assertEq(zeroForOne, false); - assertEq(tickSpacing, 1); assertTrue(isExactInput); - assertTrue(isSingle); assertEq(amount, expectedAmount); } From a55568325722161c9f4203d548de5226c0f7b710 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 11 Feb 2025 22:41:53 +0530 Subject: [PATCH 17/35] chore: consistent naming --- foundry/test/executors/UniswapV4Executor.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index c7658fb..45178df 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -38,7 +38,7 @@ contract UniswapV4ExecutorTest is Test, Constants { new UniswapV4ExecutorExposed(IPoolManager(poolManager)); } - function testDecodeParamsUniswapV4() public view { + function testDecodeParams() public view { uint24 expectedPoolFee = 500; address expectedReceiver = address(2); uint128 expectedAmount = 100; @@ -62,7 +62,7 @@ contract UniswapV4ExecutorTest is Test, Constants { assertEq(amount, expectedAmount); } - function testSwapUniswapV4() public { + function testSwap() public { vm.startPrank(BOB); uint256 amountIn = 100 ether; deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); From ff3209b1c861c015568c3daa691f74d95ef0c978 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 11 Feb 2025 22:58:34 +0530 Subject: [PATCH 18/35] fix: remove extra _receiver and redundant asserts --- foundry/src/executors/UniswapV4Executor.sol | 4 +--- foundry/test/executors/UniswapV4Executor.t.sol | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 8da2a8b..f72b0d9 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -75,9 +75,7 @@ contract UniswapV4Executor is IExecutor, V4Router { uint8 action = uint8(bytes1(actions[0])); // Get receiver from params[2] for all cases - (, address _receiver,) = - abi.decode(params[2], (Currency, address, uint256)); - receiver = _receiver; + (, receiver,) = abi.decode(params[2], (Currency, address, uint256)); if (action == uint8(Actions.SWAP_EXACT_IN_SINGLE)) { IV4Router.ExactInputSingleParams memory swapParams = diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 45178df..249c78d 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -69,9 +69,6 @@ contract UniswapV4ExecutorTest is Test, Constants { uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); uint256 usdeBalanceBeforeSwapExecutor = USDE.balanceOf(address(uniswapV4Exposed)); - assertEq(usdeBalanceBeforeSwapExecutor, amountIn); - uint256 usdtBalanceBeforeSwapBob = USDT.balanceOf(address(BOB)); - assertEq(usdtBalanceBeforeSwapBob, 0); bytes memory data = _encodeExactInputSingle( USDE_ADDR, USDT_ADDR, 100, BOB, true, 1, uint128(amountIn) @@ -83,7 +80,7 @@ contract UniswapV4ExecutorTest is Test, Constants { USDE.balanceOf(address(uniswapV4Exposed)), usdeBalanceBeforeSwapExecutor - amountIn ); - assertTrue(USDT.balanceOf(BOB) == amountOut && amountOut > 0); + assertTrue(USDT.balanceOf(BOB) == amountOut); } function _encodeExactInputSingle( From b47cff3fc915b8146d62b085a7a5239d85d9d993 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 12 Feb 2025 17:36:16 +0530 Subject: [PATCH 19/35] fix: add equality check, amountInOrOut check, update _decodeData --- foundry/src/executors/UniswapV4Executor.sol | 104 +++++++++++------- .../test/executors/UniswapV4Executor.t.sol | 34 ++++-- 2 files changed, 89 insertions(+), 49 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index f72b0d9..8627aa2 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -2,14 +2,9 @@ pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; -import { - IERC20, - SafeERC20 -} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import { - Currency, CurrencyLibrary -} from "@uniswap/v4-core/src/types/Currency.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {V4Router} from "@uniswap/v4-periphery/src/V4Router.sol"; @@ -26,98 +21,127 @@ contract UniswapV4Executor is IExecutor, V4Router { constructor(IPoolManager _poolManager) V4Router(_poolManager) {} - function swap(uint256, bytes calldata data) - external - payable - returns (uint256 amountOut) - { - (address tokenOut, address receiver, bool isExactInput, uint256 amount) - = _decodeData(data); + function swap( + uint256, + bytes calldata data + ) external payable returns (uint256 amountInOrOut) { + ( + address tokenIn, + address tokenOut, + address receiver, + bool isExactInput, + uint256 amount + ) = _decodeData(data); - uint256 balanceBefore = IERC20(tokenOut).balanceOf(receiver); + uint256 tokenOutBefore = IERC20(tokenOut).balanceOf(receiver); + uint256 tokenInBefore = IERC20(tokenIn).balanceOf(address(this)); this.executeActions(data); - uint256 balanceAfter = IERC20(tokenOut).balanceOf(receiver); + uint256 tokenOutAfter = IERC20(tokenOut).balanceOf(receiver); + uint256 tokenInAfter = IERC20(tokenIn).balanceOf(address(this)); if (isExactInput) { - amountOut = balanceAfter - balanceBefore; + amountInOrOut = tokenOutAfter - tokenOutBefore; } else { - amountOut = amount; + amountInOrOut = tokenInBefore - tokenInAfter; } - // Checks if the amountOut is not 0. - // Slither does not allow strict equality checks. - if (amountOut < 1) { + // slither-disable-next-line incorrect-equality + if (amountInOrOut == 0) { revert UniswapV4Executor__SwapFailed(); } - return amountOut; + return amountInOrOut; } function executeActions(bytes calldata actions) public { _executeActions(actions); } - function _decodeData(bytes calldata data) + function _decodeData( + bytes calldata data + ) internal pure returns ( + address tokenIn, address tokenOut, address receiver, bool isExactInput, uint256 amount ) { - (bytes memory actions, bytes[] memory params) = - abi.decode(data, (bytes, bytes[])); + (bytes memory actions, bytes[] memory params) = abi.decode( + data, + (bytes, bytes[]) + ); // First byte of actions determines the swap type uint8 action = uint8(bytes1(actions[0])); // Get receiver from params[2] for all cases - (, receiver,) = abi.decode(params[2], (Currency, address, uint256)); + (, receiver, ) = abi.decode(params[2], (Currency, address, uint256)); if (action == uint8(Actions.SWAP_EXACT_IN_SINGLE)) { - IV4Router.ExactInputSingleParams memory swapParams = - abi.decode(params[0], (IV4Router.ExactInputSingleParams)); + IV4Router.ExactInputSingleParams memory swapParams = abi.decode( + params[0], + (IV4Router.ExactInputSingleParams) + ); + 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)); + 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)); + IV4Router.ExactInputParams memory swapParams = abi.decode( + params[0], + (IV4Router.ExactInputParams) + ); - PathKey memory lastPath = - swapParams.path[swapParams.path.length - 1]; + 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)); + 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; } } - function _pay(Currency token, address payer, uint256 amount) - internal - override - { + function _pay( + Currency token, + address payer, + uint256 amount + ) internal override { token.transfer(payer, amount); } diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 249c78d..594b986 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -9,10 +9,13 @@ import {console} from "forge-std/console.sol"; contract UniswapV4ExecutorExposed is UniswapV4Executor { constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {} - function decodeData(bytes calldata data) + function decodeData( + bytes calldata data + ) external pure returns ( + address tokenIn, address tokenOut, address receiver, bool isExactInput, @@ -34,8 +37,9 @@ contract UniswapV4ExecutorTest is Test, Constants { function setUp() public { uint256 forkBlock = 21817316; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - uniswapV4Exposed = - new UniswapV4ExecutorExposed(IPoolManager(poolManager)); + uniswapV4Exposed = new UniswapV4ExecutorExposed( + IPoolManager(poolManager) + ); } function testDecodeParams() public view { @@ -53,9 +57,15 @@ contract UniswapV4ExecutorTest is Test, Constants { expectedAmount ); - (address tokenOut, address receiver, bool isExactInput, uint256 amount) - = uniswapV4Exposed.decodeData(data); + ( + address tokenIn, + address tokenOut, + address receiver, + bool isExactInput, + uint256 amount + ) = uniswapV4Exposed.decodeData(data); + assertEq(tokenIn, USDE_ADDR); assertEq(tokenOut, USDT_ADDR); assertEq(receiver, expectedReceiver); assertTrue(isExactInput); @@ -63,15 +73,21 @@ contract UniswapV4ExecutorTest is Test, Constants { } function testSwap() public { - vm.startPrank(BOB); uint256 amountIn = 100 ether; deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); - uint256 usdeBalanceBeforeSwapExecutor = - USDE.balanceOf(address(uniswapV4Exposed)); + uint256 usdeBalanceBeforeSwapExecutor = USDE.balanceOf( + address(uniswapV4Exposed) + ); bytes memory data = _encodeExactInputSingle( - USDE_ADDR, USDT_ADDR, 100, BOB, true, 1, uint128(amountIn) + USDE_ADDR, + USDT_ADDR, + 100, + BOB, + true, + 1, + uint128(amountIn) ); uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); From 2371ab2a1fb96164a54c796cb0557d64e50c2350 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 12 Feb 2025 20:32:46 +0530 Subject: [PATCH 20/35] fix: remove executeActions wrapper, strict equality checks and rename swap return --- foundry/src/executors/UniswapV4Executor.sol | 84 ++++++++----------- .../test/executors/UniswapV4Executor.t.sol | 22 ++--- 2 files changed, 40 insertions(+), 66 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 8627aa2..c0b157d 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -2,9 +2,14 @@ pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; -import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { + IERC20, + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import { + Currency, CurrencyLibrary +} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {V4Router} from "@uniswap/v4-periphery/src/V4Router.sol"; @@ -21,10 +26,11 @@ contract UniswapV4Executor is IExecutor, V4Router { constructor(IPoolManager _poolManager) V4Router(_poolManager) {} - function swap( - uint256, - bytes calldata data - ) external payable returns (uint256 amountInOrOut) { + function swap(uint256, bytes calldata data) + external + payable + returns (uint256 calculatedAmount) + { ( address tokenIn, address tokenOut, @@ -36,31 +42,21 @@ contract UniswapV4Executor is IExecutor, V4Router { uint256 tokenOutBefore = IERC20(tokenOut).balanceOf(receiver); uint256 tokenInBefore = IERC20(tokenIn).balanceOf(address(this)); - this.executeActions(data); + _executeActions(data); uint256 tokenOutAfter = IERC20(tokenOut).balanceOf(receiver); uint256 tokenInAfter = IERC20(tokenIn).balanceOf(address(this)); if (isExactInput) { - amountInOrOut = tokenOutAfter - tokenOutBefore; + calculatedAmount = tokenOutAfter - tokenOutBefore; } else { - amountInOrOut = tokenInBefore - tokenInAfter; + calculatedAmount = tokenInBefore - tokenInAfter; } - // slither-disable-next-line incorrect-equality - if (amountInOrOut == 0) { - revert UniswapV4Executor__SwapFailed(); - } - return amountInOrOut; + return calculatedAmount; } - function executeActions(bytes calldata actions) public { - _executeActions(actions); - } - - function _decodeData( - bytes calldata data - ) + function _decodeData(bytes calldata data) internal pure returns ( @@ -71,22 +67,18 @@ contract UniswapV4Executor is IExecutor, V4Router { uint256 amount ) { - (bytes memory actions, bytes[] memory params) = abi.decode( - data, - (bytes, bytes[]) - ); + (bytes memory actions, bytes[] memory params) = + abi.decode(data, (bytes, bytes[])); // First byte of actions determines the swap type uint8 action = uint8(bytes1(actions[0])); // Get receiver from params[2] for all cases - (, receiver, ) = abi.decode(params[2], (Currency, address, uint256)); + (, receiver,) = abi.decode(params[2], (Currency, address, uint256)); if (action == uint8(Actions.SWAP_EXACT_IN_SINGLE)) { - IV4Router.ExactInputSingleParams memory swapParams = abi.decode( - params[0], - (IV4Router.ExactInputSingleParams) - ); + IV4Router.ExactInputSingleParams memory swapParams = + abi.decode(params[0], (IV4Router.ExactInputSingleParams)); tokenIn = swapParams.zeroForOne ? address(uint160(swapParams.poolKey.currency0.toId())) @@ -97,10 +89,8 @@ contract UniswapV4Executor is IExecutor, V4Router { isExactInput = true; amount = swapParams.amountIn; } else if (action == uint8(Actions.SWAP_EXACT_OUT_SINGLE)) { - IV4Router.ExactOutputSingleParams memory swapParams = abi.decode( - params[0], - (IV4Router.ExactOutputSingleParams) - ); + IV4Router.ExactOutputSingleParams memory swapParams = + abi.decode(params[0], (IV4Router.ExactOutputSingleParams)); tokenIn = swapParams.zeroForOne ? address(uint160(swapParams.poolKey.currency0.toId())) @@ -111,23 +101,18 @@ contract UniswapV4Executor is IExecutor, V4Router { isExactInput = false; amount = swapParams.amountOut; } else if (action == uint8(Actions.SWAP_EXACT_IN)) { - IV4Router.ExactInputParams memory swapParams = abi.decode( - params[0], - (IV4Router.ExactInputParams) - ); + 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 - ]; + 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) - ); + IV4Router.ExactOutputParams memory swapParams = + abi.decode(params[0], (IV4Router.ExactOutputParams)); PathKey memory firstPath = swapParams.path[0]; tokenIn = address(uint160(firstPath.intermediateCurrency.toId())); @@ -137,11 +122,10 @@ contract UniswapV4Executor is IExecutor, V4Router { } } - function _pay( - Currency token, - address payer, - uint256 amount - ) internal override { + function _pay(Currency token, address payer, uint256 amount) + internal + override + { token.transfer(payer, amount); } diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 594b986..8f72e7f 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -9,9 +9,7 @@ import {console} from "forge-std/console.sol"; contract UniswapV4ExecutorExposed is UniswapV4Executor { constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {} - function decodeData( - bytes calldata data - ) + function decodeData(bytes calldata data) external pure returns ( @@ -37,9 +35,8 @@ contract UniswapV4ExecutorTest is Test, Constants { function setUp() public { uint256 forkBlock = 21817316; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - uniswapV4Exposed = new UniswapV4ExecutorExposed( - IPoolManager(poolManager) - ); + uniswapV4Exposed = + new UniswapV4ExecutorExposed(IPoolManager(poolManager)); } function testDecodeParams() public view { @@ -76,18 +73,11 @@ contract UniswapV4ExecutorTest is Test, Constants { uint256 amountIn = 100 ether; deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); - uint256 usdeBalanceBeforeSwapExecutor = USDE.balanceOf( - address(uniswapV4Exposed) - ); + uint256 usdeBalanceBeforeSwapExecutor = + USDE.balanceOf(address(uniswapV4Exposed)); bytes memory data = _encodeExactInputSingle( - USDE_ADDR, - USDT_ADDR, - 100, - BOB, - true, - 1, - uint128(amountIn) + USDE_ADDR, USDT_ADDR, 100, BOB, true, 1, uint128(amountIn) ); uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); From fe48c64e8ecd4f1062928fee70843e1a281b4a6a Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 12 Feb 2025 21:55:41 +0530 Subject: [PATCH 21/35] chore: change balance checks naming --- foundry/src/executors/UniswapV4Executor.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index c0b157d..9cafb93 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -39,18 +39,18 @@ contract UniswapV4Executor is IExecutor, V4Router { uint256 amount ) = _decodeData(data); - uint256 tokenOutBefore = IERC20(tokenOut).balanceOf(receiver); - uint256 tokenInBefore = IERC20(tokenIn).balanceOf(address(this)); + uint256 tokenOutBalanceBefore = IERC20(tokenOut).balanceOf(receiver); + uint256 tokenInBalanceBefore = IERC20(tokenIn).balanceOf(address(this)); _executeActions(data); - uint256 tokenOutAfter = IERC20(tokenOut).balanceOf(receiver); - uint256 tokenInAfter = IERC20(tokenIn).balanceOf(address(this)); + uint256 tokenOutBalanceAfter = IERC20(tokenOut).balanceOf(receiver); + uint256 tokenInBalanceAfter = IERC20(tokenIn).balanceOf(address(this)); if (isExactInput) { - calculatedAmount = tokenOutAfter - tokenOutBefore; + calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore; } else { - calculatedAmount = tokenInBefore - tokenInAfter; + calculatedAmount = tokenInBalanceBefore - tokenInBalanceAfter; } return calculatedAmount; From 0c40e9e97923d5bad61aa812ba739c2fe4260cf8 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 12 Feb 2025 22:30:13 +0530 Subject: [PATCH 22/35] fix: handle native token balance changes --- foundry/src/executors/UniswapV4Executor.sol | 24 +++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 9cafb93..0b31939 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -39,13 +39,29 @@ contract UniswapV4Executor is IExecutor, V4Router { uint256 amount ) = _decodeData(data); - uint256 tokenOutBalanceBefore = IERC20(tokenOut).balanceOf(receiver); - uint256 tokenInBalanceBefore = IERC20(tokenIn).balanceOf(address(this)); + uint256 tokenOutBalanceBefore; + uint256 tokenInBalanceBefore; + + tokenOutBalanceBefore = tokenOut == address(0) + ? receiver.balance + : IERC20(tokenOut).balanceOf(receiver); + + tokenInBalanceBefore = tokenIn == address(0) + ? address(this).balance + : IERC20(tokenIn).balanceOf(address(this)); _executeActions(data); - uint256 tokenOutBalanceAfter = IERC20(tokenOut).balanceOf(receiver); - uint256 tokenInBalanceAfter = IERC20(tokenIn).balanceOf(address(this)); + uint256 tokenOutBalanceAfter; + uint256 tokenInBalanceAfter; + + tokenOutBalanceAfter = tokenOut == address(0) + ? receiver.balance + : IERC20(tokenOut).balanceOf(receiver); + + tokenInBalanceAfter = tokenIn == address(0) + ? address(this).balance + : IERC20(tokenIn).balanceOf(address(this)); if (isExactInput) { calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore; From d79068282aebd1e65ae32e79ec3127da25f091af Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 13 Feb 2025 00:20:14 +0530 Subject: [PATCH 23/35] fix: _pay and msgSender --- foundry/src/executors/UniswapV4Executor.sol | 27 +++++++---------- .../test/executors/UniswapV4Executor.t.sol | 29 +++++-------------- 2 files changed, 17 insertions(+), 39 deletions(-) diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index 0b31939..4a67727 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -31,20 +31,15 @@ contract UniswapV4Executor is IExecutor, V4Router { payable returns (uint256 calculatedAmount) { - ( - address tokenIn, - address tokenOut, - address receiver, - bool isExactInput, - uint256 amount - ) = _decodeData(data); + (address tokenIn, address tokenOut, bool isExactInput, uint256 amount) = + _decodeData(data); uint256 tokenOutBalanceBefore; uint256 tokenInBalanceBefore; tokenOutBalanceBefore = tokenOut == address(0) - ? receiver.balance - : IERC20(tokenOut).balanceOf(receiver); + ? address(this).balance + : IERC20(tokenOut).balanceOf(address(this)); tokenInBalanceBefore = tokenIn == address(0) ? address(this).balance @@ -56,8 +51,8 @@ contract UniswapV4Executor is IExecutor, V4Router { uint256 tokenInBalanceAfter; tokenOutBalanceAfter = tokenOut == address(0) - ? receiver.balance - : IERC20(tokenOut).balanceOf(receiver); + ? address(this).balance + : IERC20(tokenOut).balanceOf(address(this)); tokenInBalanceAfter = tokenIn == address(0) ? address(this).balance @@ -78,7 +73,6 @@ contract UniswapV4Executor is IExecutor, V4Router { returns ( address tokenIn, address tokenOut, - address receiver, bool isExactInput, uint256 amount ) @@ -89,9 +83,6 @@ contract UniswapV4Executor is IExecutor, V4Router { // First byte of actions determines the swap type uint8 action = uint8(bytes1(actions[0])); - // Get receiver from params[2] for all cases - (, receiver,) = abi.decode(params[2], (Currency, address, uint256)); - if (action == uint8(Actions.SWAP_EXACT_IN_SINGLE)) { IV4Router.ExactInputSingleParams memory swapParams = abi.decode(params[0], (IV4Router.ExactInputSingleParams)); @@ -142,10 +133,12 @@ contract UniswapV4Executor is IExecutor, V4Router { internal override { - token.transfer(payer, amount); + IERC20(Currency.unwrap(token)).safeTransfer( + address(poolManager), amount + ); } function msgSender() public view override returns (address) { - return msg.sender; + return address(this); } } diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 8f72e7f..124c15f 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -15,7 +15,6 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor { returns ( address tokenIn, address tokenOut, - address receiver, bool isExactInput, uint256 amount ) @@ -41,30 +40,17 @@ contract UniswapV4ExecutorTest is Test, Constants { function testDecodeParams() public view { uint24 expectedPoolFee = 500; - address expectedReceiver = address(2); uint128 expectedAmount = 100; bytes memory data = _encodeExactInputSingle( - USDE_ADDR, - USDT_ADDR, - expectedPoolFee, - expectedReceiver, - false, - 1, - expectedAmount + USDE_ADDR, USDT_ADDR, expectedPoolFee, false, 1, expectedAmount ); - ( - address tokenIn, - address tokenOut, - address receiver, - bool isExactInput, - uint256 amount - ) = uniswapV4Exposed.decodeData(data); + (address tokenIn, address tokenOut, bool isExactInput, uint256 amount) = + uniswapV4Exposed.decodeData(data); assertEq(tokenIn, USDE_ADDR); assertEq(tokenOut, USDT_ADDR); - assertEq(receiver, expectedReceiver); assertTrue(isExactInput); assertEq(amount, expectedAmount); } @@ -77,7 +63,7 @@ contract UniswapV4ExecutorTest is Test, Constants { USDE.balanceOf(address(uniswapV4Exposed)); bytes memory data = _encodeExactInputSingle( - USDE_ADDR, USDT_ADDR, 100, BOB, true, 1, uint128(amountIn) + USDE_ADDR, USDT_ADDR, 100, true, 1, uint128(amountIn) ); uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); @@ -86,14 +72,13 @@ contract UniswapV4ExecutorTest is Test, Constants { USDE.balanceOf(address(uniswapV4Exposed)), usdeBalanceBeforeSwapExecutor - amountIn ); - assertTrue(USDT.balanceOf(BOB) == amountOut); + assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut); } function _encodeExactInputSingle( address tokenIn, address tokenOut, uint24 fee, - address receiver, bool zeroForOne, uint24 tickSpacing, uint128 amountIn @@ -109,7 +94,7 @@ contract UniswapV4ExecutorTest is Test, Constants { bytes memory actions = abi.encodePacked( uint8(Actions.SWAP_EXACT_IN_SINGLE), uint8(Actions.SETTLE_ALL), - uint8(Actions.TAKE) + uint8(Actions.TAKE_ALL) ); bytes[] memory params = new bytes[](3); @@ -125,7 +110,7 @@ contract UniswapV4ExecutorTest is Test, Constants { ); params[1] = abi.encode(key.currency0, amountIn); - params[2] = abi.encode(key.currency1, receiver, 0); + params[2] = abi.encode(key.currency1, 0); return abi.encode(actions, params); } From 28bfe2e32aa175d8165f58f147f2dfc63240c81c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 12 Feb 2025 20:01:08 +0000 Subject: [PATCH 24/35] chore(release): 0.37.0 [skip ci] ## [0.37.0](https://github.com/propeller-heads/tycho-execution/compare/0.36.2...0.37.0) (2025-02-12) ### Features * add callback ([ed90cb4](https://github.com/propeller-heads/tycho-execution/commit/ed90cb4ef1d43e09a7cd6f824ef4214598851b9f)) * add new pair test ([7ca647f](https://github.com/propeller-heads/tycho-execution/commit/7ca647f009ad8cb71c6e8a08e64ee02285c9ae08)) * add router params ([e62c332](https://github.com/propeller-heads/tycho-execution/commit/e62c332451d7cf0d2fc471faa7af7b26fd1a000d)) * add test for UniswapV4Executor ([4599f07](https://github.com/propeller-heads/tycho-execution/commit/4599f07df0d4c4131f87cd41ec7bcb8b1dd47bde)) * add univ4 executor ([cb4c8f4](https://github.com/propeller-heads/tycho-execution/commit/cb4c8f4e51d4f1900149288339c6a6fc75a515b1)) * handle amounts in unlockCallback ([b2097ca](https://github.com/propeller-heads/tycho-execution/commit/b2097ca4a5600161166636c7f2b58f845540ed9a)) * move encoding to test ([c264084](https://github.com/propeller-heads/tycho-execution/commit/c264084783561b3de4eeac413ed6155076ff11d5)) * support multi swap decoding ([d998c88](https://github.com/propeller-heads/tycho-execution/commit/d998c88cfef300e41714c6c3c6164e761d14e2de)) * update solc and add V4Router into UniswapV4Executor ([bdd3daf](https://github.com/propeller-heads/tycho-execution/commit/bdd3daffba3853ad084f7d3454e3c72fd6a1679c)) ### Bug Fixes * _pay and msgSender ([d790682](https://github.com/propeller-heads/tycho-execution/commit/d79068282aebd1e65ae32e79ec3127da25f091af)) * add equality check, amountInOrOut check, update _decodeData ([b47cff3](https://github.com/propeller-heads/tycho-execution/commit/b47cff3fc915b8146d62b085a7a5239d85d9d993)) * git submodules and strict equality check in v4 executor ([a8cc84d](https://github.com/propeller-heads/tycho-execution/commit/a8cc84ddce7c90aa40d69090577ef15cc95d8edf)) * handle native token balance changes ([0c40e9e](https://github.com/propeller-heads/tycho-execution/commit/0c40e9e97923d5bad61aa812ba739c2fe4260cf8)) * reciever issue ([ae0b07b](https://github.com/propeller-heads/tycho-execution/commit/ae0b07b2a47b93430841ce8bf437215d2f94e3bb)) * remove executeActions wrapper, strict equality checks and rename swap return ([2371ab2](https://github.com/propeller-heads/tycho-execution/commit/2371ab2a1fb96164a54c796cb0557d64e50c2350)) * remove extra _receiver and redundant asserts ([ff3209b](https://github.com/propeller-heads/tycho-execution/commit/ff3209b1c861c015568c3daa691f74d95ef0c978)) * rm callback fn ([1a36c33](https://github.com/propeller-heads/tycho-execution/commit/1a36c33bc614d744cfa161dd85d6cccc671e592e)) * rm redundant transfer ([24d4e76](https://github.com/propeller-heads/tycho-execution/commit/24d4e762a2841909245d7a4434c13f37398ae482)) --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce82249..efcc4cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## [0.37.0](https://github.com/propeller-heads/tycho-execution/compare/0.36.2...0.37.0) (2025-02-12) + + +### Features + +* add callback ([ed90cb4](https://github.com/propeller-heads/tycho-execution/commit/ed90cb4ef1d43e09a7cd6f824ef4214598851b9f)) +* add new pair test ([7ca647f](https://github.com/propeller-heads/tycho-execution/commit/7ca647f009ad8cb71c6e8a08e64ee02285c9ae08)) +* add router params ([e62c332](https://github.com/propeller-heads/tycho-execution/commit/e62c332451d7cf0d2fc471faa7af7b26fd1a000d)) +* add test for UniswapV4Executor ([4599f07](https://github.com/propeller-heads/tycho-execution/commit/4599f07df0d4c4131f87cd41ec7bcb8b1dd47bde)) +* add univ4 executor ([cb4c8f4](https://github.com/propeller-heads/tycho-execution/commit/cb4c8f4e51d4f1900149288339c6a6fc75a515b1)) +* handle amounts in unlockCallback ([b2097ca](https://github.com/propeller-heads/tycho-execution/commit/b2097ca4a5600161166636c7f2b58f845540ed9a)) +* move encoding to test ([c264084](https://github.com/propeller-heads/tycho-execution/commit/c264084783561b3de4eeac413ed6155076ff11d5)) +* support multi swap decoding ([d998c88](https://github.com/propeller-heads/tycho-execution/commit/d998c88cfef300e41714c6c3c6164e761d14e2de)) +* update solc and add V4Router into UniswapV4Executor ([bdd3daf](https://github.com/propeller-heads/tycho-execution/commit/bdd3daffba3853ad084f7d3454e3c72fd6a1679c)) + + +### Bug Fixes + +* _pay and msgSender ([d790682](https://github.com/propeller-heads/tycho-execution/commit/d79068282aebd1e65ae32e79ec3127da25f091af)) +* add equality check, amountInOrOut check, update _decodeData ([b47cff3](https://github.com/propeller-heads/tycho-execution/commit/b47cff3fc915b8146d62b085a7a5239d85d9d993)) +* git submodules and strict equality check in v4 executor ([a8cc84d](https://github.com/propeller-heads/tycho-execution/commit/a8cc84ddce7c90aa40d69090577ef15cc95d8edf)) +* handle native token balance changes ([0c40e9e](https://github.com/propeller-heads/tycho-execution/commit/0c40e9e97923d5bad61aa812ba739c2fe4260cf8)) +* reciever issue ([ae0b07b](https://github.com/propeller-heads/tycho-execution/commit/ae0b07b2a47b93430841ce8bf437215d2f94e3bb)) +* remove executeActions wrapper, strict equality checks and rename swap return ([2371ab2](https://github.com/propeller-heads/tycho-execution/commit/2371ab2a1fb96164a54c796cb0557d64e50c2350)) +* remove extra _receiver and redundant asserts ([ff3209b](https://github.com/propeller-heads/tycho-execution/commit/ff3209b1c861c015568c3daa691f74d95ef0c978)) +* rm callback fn ([1a36c33](https://github.com/propeller-heads/tycho-execution/commit/1a36c33bc614d744cfa161dd85d6cccc671e592e)) +* rm redundant transfer ([24d4e76](https://github.com/propeller-heads/tycho-execution/commit/24d4e762a2841909245d7a4434c13f37398ae482)) + ## [0.36.2](https://github.com/propeller-heads/tycho-execution/compare/0.36.1...0.36.2) (2025-02-12) diff --git a/Cargo.lock b/Cargo.lock index 1b94f76..c8276f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4340,7 +4340,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.36.2" +version = "0.37.0" dependencies = [ "alloy", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 3c76338..5dc05e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.36.2" +version = "0.37.0" edition = "2021" [[bin]] From 29eb50d0a1e9d9319146e4b937797e65612c962b Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Wed, 12 Feb 2025 18:00:18 +0000 Subject: [PATCH 25/35] chore: Create a UniswapV4Utils with encoding functions --- don't change below this line --- ENG-4223 Took 4 hours 33 minutes Took 2 minutes Took 28 minutes --- .../test/executors/UniswapV4Executor.t.sol | 47 ++----------------- foundry/test/executors/UniswapV4Utils.sol | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+), 43 deletions(-) create mode 100644 foundry/test/executors/UniswapV4Utils.sol diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 124c15f..57f4156 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; +import "./UniswapV4Utils.sol"; import "@src/executors/UniswapV4Executor.sol"; -import {Test} from "../../lib/forge-std/src/Test.sol"; import {Constants} from "../Constants.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; import {console} from "forge-std/console.sol"; contract UniswapV4ExecutorExposed is UniswapV4Executor { @@ -42,7 +43,7 @@ contract UniswapV4ExecutorTest is Test, Constants { uint24 expectedPoolFee = 500; uint128 expectedAmount = 100; - bytes memory data = _encodeExactInputSingle( + bytes memory data = UniswapV4Utils.encodeExactInputSingle( USDE_ADDR, USDT_ADDR, expectedPoolFee, false, 1, expectedAmount ); @@ -62,7 +63,7 @@ contract UniswapV4ExecutorTest is Test, Constants { uint256 usdeBalanceBeforeSwapExecutor = USDE.balanceOf(address(uniswapV4Exposed)); - bytes memory data = _encodeExactInputSingle( + bytes memory data = UniswapV4Utils.encodeExactInputSingle( USDE_ADDR, USDT_ADDR, 100, true, 1, uint128(amountIn) ); @@ -74,44 +75,4 @@ contract UniswapV4ExecutorTest is Test, Constants { ); assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut); } - - function _encodeExactInputSingle( - address tokenIn, - address tokenOut, - uint24 fee, - bool zeroForOne, - uint24 tickSpacing, - uint128 amountIn - ) internal 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 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: amountIn, - amountOutMinimum: 0, - hookData: bytes("") - }) - ); - - params[1] = abi.encode(key.currency0, amountIn); - params[2] = abi.encode(key.currency1, 0); - - return abi.encode(actions, params); - } } diff --git a/foundry/test/executors/UniswapV4Utils.sol b/foundry/test/executors/UniswapV4Utils.sol new file mode 100644 index 0000000..093a944 --- /dev/null +++ b/foundry/test/executors/UniswapV4Utils.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import "@src/executors/UniswapV4Executor.sol"; + +library UniswapV4Utils { + function encodeExactInputSingle( + address tokenIn, + address tokenOut, + uint24 fee, + address receiver, + bool zeroForOne, + uint24 tickSpacing, + uint128 amountIn + ) 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 actions = abi.encodePacked( + uint8(Actions.SWAP_EXACT_IN_SINGLE), + uint8(Actions.SETTLE_ALL), + uint8(Actions.TAKE) + ); + + 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, receiver, 0); + + return abi.encode(actions, params); + } +} From 591d73ba717deb1773f5c10f9085cc1175df2536 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Wed, 12 Feb 2025 19:30:21 +0000 Subject: [PATCH 26/35] feat: Support uniswap v4 callback in TychoRouter Make TychoRouter inherit from SafeCallback and then delegatecall to the UniswapV4 executor Add a test for this. I had to update the block of our forked network in the tests. Because of this I had to update all the asserts in previous tests Had to change the optimizer_runs in foundry.toml because of weird Yul errors when compiling --- don't change below this line --- ENG-4223 Took 1 hour 21 minutes Took 7 seconds Took 35 seconds --- foundry/foundry.toml | 2 +- foundry/src/TychoRouter.sol | 30 ++++++++- foundry/test/TychoRouter.t.sol | 62 ++++++++++++++----- foundry/test/TychoRouterTestSetup.sol | 28 ++++++--- .../test/executors/BalancerV2Executor.t.sol | 2 +- .../test/executors/UniswapV2Executor.t.sol | 2 +- foundry/test/executors/UniswapV4Utils.sol | 5 +- 7 files changed, 101 insertions(+), 30 deletions(-) diff --git a/foundry/foundry.toml b/foundry/foundry.toml index e573137..548f699 100644 --- a/foundry/foundry.toml +++ b/foundry/foundry.toml @@ -5,7 +5,7 @@ libs = ['lib'] auto_detect_sol = true evm_version = 'cancun' optimizer = true -optimizer_runs = 1000 +optimizer_runs = 200 via_ir = true [rpc_endpoints] diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 311c916..36e3499 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -15,6 +15,8 @@ import "@uniswap/v3-updated/CallbackValidationV2.sol"; import "./ExecutionDispatcher.sol"; import "./CallbackVerificationDispatcher.sol"; import {LibSwap} from "../lib/LibSwap.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol"; error TychoRouter__WithdrawalFailed(); error TychoRouter__AddressZero(); @@ -28,7 +30,8 @@ contract TychoRouter is ExecutionDispatcher, CallbackVerificationDispatcher, Pausable, - ReentrancyGuard + ReentrancyGuard, + SafeCallback { IAllowanceTransfer public immutable permit2; IWETH private immutable _weth; @@ -65,7 +68,12 @@ contract TychoRouter is address private immutable _usv3Factory; - constructor(address _permit2, address weth, address usv3Factory) { + constructor( + IPoolManager _poolManager, + address _permit2, + address weth, + address usv3Factory + ) SafeCallback(_poolManager) { if ( _permit2 == address(0) || weth == address(0) || usv3Factory == address(0) @@ -434,4 +442,22 @@ contract TychoRouter is return (amountIn, tokenIn); } + + function _unlockCallback(bytes calldata data) + internal + override + 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]; + + // slither-disable-next-line controlled-delegatecall,low-level-calls + (bool success,) = executor.delegatecall( + abi.encodeWithSelector(selector, protocolData) + ); + require(success, "delegatecall to uniswap v4 callback failed"); + return ""; + } } diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index f7df52a..960e7b9 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; +import "@src/executors/UniswapV4Executor.sol"; import {TychoRouter} from "@src/TychoRouter.sol"; import "./TychoRouterTestSetup.sol"; +import "./executors/UniswapV4Utils.sol"; contract TychoRouterTest is TychoRouterTestSetup { bytes32 public constant EXECUTOR_SETTER_ROLE = @@ -254,7 +256,7 @@ contract TychoRouterTest is TychoRouterTestSetup { tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr); - assertEq(daiBalance, 2630432278145144658455); + assertEq(daiBalance, 2659881924818443699787); assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } @@ -291,7 +293,7 @@ contract TychoRouterTest is TychoRouterTestSetup { tychoRouter.exposedSwap(amountIn, 3, pleEncode(swaps)); uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); - assertEq(usdcBalance, 2610580090); + assertEq(usdcBalance, 2644659787); assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } @@ -352,7 +354,7 @@ contract TychoRouterTest is TychoRouterTestSetup { tychoRouter.exposedSwap(amountIn, 4, pleEncode(swaps)); uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); - assertEq(usdcBalance, 2581503157); + assertEq(usdcBalance, 2615491639); assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } @@ -400,7 +402,7 @@ contract TychoRouterTest is TychoRouterTestSetup { pleEncode(swaps) ); - uint256 expectedAmount = 2630432278145144658455; + uint256 expectedAmount = 2659881924818443699787; assertEq(amountOut, expectedAmount); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); assertEq(daiBalance, expectedAmount); @@ -442,7 +444,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.expectRevert( abi.encodeWithSelector( TychoRouter__NegativeSlippage.selector, - 2630432278145144658455, // actual amountOut + 2659881924818443699787, // actual amountOut minAmountOut ) ); @@ -511,11 +513,11 @@ contract TychoRouterTest is TychoRouterTestSetup { pleEncode(swaps) ); - uint256 expectedAmount = 2604127955363693211871; + uint256 expectedAmount = 2633283105570259262790; assertEq(amountOut, expectedAmount); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); assertEq(daiBalance, expectedAmount); - assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26304322781451446584); + assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26598819248184436997); vm.stopPrank(); } @@ -567,7 +569,7 @@ contract TychoRouterTest is TychoRouterTestSetup { "", pleEncode(swaps) ); - uint256 expectedAmount = 2630432278145144658455; + uint256 expectedAmount = 2659881924818443699787; assertEq(amountOut, expectedAmount); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); assertEq(daiBalance, expectedAmount); @@ -617,7 +619,7 @@ contract TychoRouterTest is TychoRouterTestSetup { pleEncode(swaps) ); - uint256 expectedAmount = 1132829934891544187; // 1.13 ETH + uint256 expectedAmount = 1120007305574805922; // 1.12 ETH assertEq(amountOut, expectedAmount); assertEq(ALICE.balance, expectedAmount); @@ -695,7 +697,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call( - hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067c43ba900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000679cb5b10000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000415bfd02ffd61c11192d1b54d76e0af125afbb32568aad37ec35f918bd5fb304cd314954213ed77c0d071301ddc45243ad57e86fe18f2905b682acc4f1a43ad8dc1c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000000000" + hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067d481bb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfbc3000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041f2740fde9662d8bc1f8fe8e8fc29447c1832d625f06f4a56ee5103ad555c12323af5d50eb840f73d17873383ae3b7573956d5df7b2bf76bddba768c2837894a51b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000000000" ); vm.stopPrank(); @@ -717,14 +719,14 @@ contract TychoRouterTest is TychoRouterTestSetup { // Approve permit2 vm.startPrank(ALICE); - IERC20(WETH_ADDR).approve(address(permit2Address), type(uint256).max); + // IERC20(WETH_ADDR).approve(address(permit2Address), type(uint256).max); // Encoded solution generated using // `test_split_swap_strategy_encoder_simple_route_wrap` // but manually replacing the executor address // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call{value: 1 ether}( - hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067c9179300000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067a1919b000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041cea77a63613f6a02aaee522c91f9569b8377a7f0200d141fafa3e1c42011e1c668555b49a1e7dd960091d0e33764ad24db6550bc761e228864495b478f1a23721b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00020000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000000000" + hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067d4806b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfa73000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041c36406a750c499ac7f79f7666650f0d4f20fc27bb49ab68121c0be6554cb5cab6caf90dc3aab2e21083a8fa46976521a1e9df41ce74be59abf03e0d3691541e91c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00020000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000000000" ); vm.stopPrank(); @@ -753,7 +755,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call( - hex"4860f9ed0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000000000000000000000000000000000000067c9185300000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067a1925b000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041fd1c3dfce5afcb47988cc68165d5de64186cedbeb7eee6fc9cd087bceeaacdfe1ab799d60e0c628f24edfd9819b94ed60846dd23240c481f1d6e5470a7815a891c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950100000000" + hex"4860f9ed0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000000000000000000000000000000000000067d4809800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfaa000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004146411c70ec7fee0d5d260803cb220f5365792426c5d94f7a0a4d37abb05205752c5418b1fadd059570a71f0911814e546728e1f21876f2a1c6d38d34bd235fd61c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950100000000" ); vm.stopPrank(); @@ -784,7 +786,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call( - hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067c48ea700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000679d08af00000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004197c2ff7801fa573e4e8e4af1df41499045485c2b48d090833dc85be38e002c1a1e7ef354285d79c2dcb40c4837e5156069de9aaf42365aef54fdc4cca2c76ccb1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000170005a00028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d0139500005a02030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d53ede3eca2a72b3aecc820e955b36f38437d0139501005a01030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d013950100000000000000000000000000000000" + hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067d4810d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfb15000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041ecaab75f0791c9683b001ea2f0e01a0a6aaf03e6e49c83e9c8a8e588a38e3be9230d962926628ffbf6a5370cda559ff0e7876a63ed38eebe33dbef5b5e2e46ef1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000170005a00028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d0139500005a02030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d53ede3eca2a72b3aecc820e955b36f38437d0139501005a01030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d013950100000000000000000000000000000000" ); vm.stopPrank(); @@ -850,4 +852,36 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } + + function testSwapSingleUSV4Callback() public { + uint256 amountIn = 100 ether; + deal(USDE_ADDR, tychoRouterAddr, amountIn); + + bytes memory protocolData = UniswapV4Utils.encodeExactInputSingle( + USDE_ADDR, USDT_ADDR, 100, true, 1, uint128(amountIn) + ); + + // add executor and selector for callback + bytes memory protocolDataWithCallBack = abi.encodePacked( + protocolData, + SafeCallback.unlockCallback.selector, + address(usv4Executor) + ); + + bytes memory swap = encodeSwap( + uint8(0), + uint8(1), + uint24(0), + address(usv4Executor), + bytes4(0), + protocolDataWithCallBack + ); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + + assertTrue(IERC20(USDT_ADDR).balanceOf(tychoRouterAddr) == 99943852); + } } diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 2d63fe2..b0bbf2e 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -2,16 +2,22 @@ pragma solidity ^0.8.26; import "../src/executors/UniswapV2Executor.sol"; +import "../src/executors/UniswapV3Executor.sol"; +import "../src/executors/UniswapV4Executor.sol"; import "./Constants.sol"; import "./mock/MockERC20.sol"; import "@src/TychoRouter.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol"; -import "../src/executors/UniswapV3Executor.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; contract TychoRouterExposed is TychoRouter { - constructor(address _permit2, address weth, address usv3Factory) - TychoRouter(_permit2, weth, usv3Factory) - {} + constructor( + IPoolManager _poolManager, + address _permit2, + address weth, + address usv3Factory + ) TychoRouter(_poolManager, _permit2, weth, usv3Factory) {} function wrapETH(uint256 amount) external payable { return _wrapETH(amount); @@ -36,16 +42,20 @@ contract TychoRouterTestSetup is Test, Constants { address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3); UniswapV2Executor public usv2Executor; UniswapV3Executor public usv3Executor; + UniswapV4Executor public usv4Executor; MockERC20[] tokens; function setUp() public { - uint256 forkBlock = 21000000; + uint256 forkBlock = 21817316; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.startPrank(ADMIN); address factoryV3 = address(0x1F98431c8aD98523631AE4a59f267346ea31F984); - tychoRouter = - new TychoRouterExposed(permit2Address, WETH_ADDR, factoryV3); + address poolManagerAddress = 0x000000000004444c5dc75cB358380D2e3dE08A90; + IPoolManager poolManager = IPoolManager(poolManagerAddress); + tychoRouter = new TychoRouterExposed( + poolManager, permit2Address, WETH_ADDR, factoryV3 + ); tychoRouterAddr = address(tychoRouter); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER); @@ -59,10 +69,12 @@ contract TychoRouterTestSetup is Test, Constants { usv2Executor = new UniswapV2Executor(); usv3Executor = new UniswapV3Executor(); + usv4Executor = new UniswapV4Executor(poolManager); vm.startPrank(EXECUTOR_SETTER); - address[] memory executors = new address[](2); + address[] memory executors = new address[](3); executors[0] = address(usv2Executor); executors[1] = address(usv3Executor); + executors[2] = address(usv4Executor); tychoRouter.setExecutors(executors); vm.stopPrank(); diff --git a/foundry/test/executors/BalancerV2Executor.t.sol b/foundry/test/executors/BalancerV2Executor.t.sol index 117b153..7581146 100644 --- a/foundry/test/executors/BalancerV2Executor.t.sol +++ b/foundry/test/executors/BalancerV2Executor.t.sol @@ -83,7 +83,7 @@ contract BalancerV2ExecutorTest is assertEq(balanceAfter - balanceBefore, amountOut); } - function testDecodeIntegration() public { + function testDecodeIntegration() public view { // Generated by the SwapEncoder - test_encode_balancer_v2 bytes memory protocolData = hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01"; diff --git a/foundry/test/executors/UniswapV2Executor.t.sol b/foundry/test/executors/UniswapV2Executor.t.sol index dbcffbc..f377f5a 100644 --- a/foundry/test/executors/UniswapV2Executor.t.sol +++ b/foundry/test/executors/UniswapV2Executor.t.sol @@ -94,7 +94,7 @@ contract UniswapV2ExecutorTest is UniswapV2ExecutorExposed, Test, Constants { assertGe(finalBalance, amountOut); } - function testDecodeIntegration() public { + function testDecodeIntegration() public view { // Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode bytes memory protocolData = hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f5640000000000000000000000000000000000000000100"; diff --git a/foundry/test/executors/UniswapV4Utils.sol b/foundry/test/executors/UniswapV4Utils.sol index 093a944..f4d021c 100644 --- a/foundry/test/executors/UniswapV4Utils.sol +++ b/foundry/test/executors/UniswapV4Utils.sol @@ -8,7 +8,6 @@ library UniswapV4Utils { address tokenIn, address tokenOut, uint24 fee, - address receiver, bool zeroForOne, uint24 tickSpacing, uint128 amountIn @@ -24,7 +23,7 @@ library UniswapV4Utils { bytes memory actions = abi.encodePacked( uint8(Actions.SWAP_EXACT_IN_SINGLE), uint8(Actions.SETTLE_ALL), - uint8(Actions.TAKE) + uint8(Actions.TAKE_ALL) ); bytes[] memory params = new bytes[](3); @@ -40,7 +39,7 @@ library UniswapV4Utils { ); params[1] = abi.encode(key.currency0, amountIn); - params[2] = abi.encode(key.currency1, receiver, 0); + params[2] = abi.encode(key.currency1, 0); return abi.encode(actions, params); } From 4c5e3bf6a9070878c684ae8d029451178201d428 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Thu, 13 Feb 2025 09:46:14 +0000 Subject: [PATCH 27/35] fix: Verify that the executor exists in the uni v4 callback --- don't change below this line --- ENG-4223 Took 10 minutes --- foundry/src/TychoRouter.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 36e3499..9d77dec 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -453,6 +453,10 @@ contract TychoRouter is address executor = address(uint160(bytes20(data[data.length - 20:]))); bytes memory protocolData = data[:data.length - 24]; + if (!executors[executor]) { + revert ExecutionDispatcher__UnapprovedExecutor(); + } + // slither-disable-next-line controlled-delegatecall,low-level-calls (bool success,) = executor.delegatecall( abi.encodeWithSelector(selector, protocolData) From dae38ceaf9b407d3ee93535ea0032804cbca9d59 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Thu, 13 Feb 2025 10:08:34 +0000 Subject: [PATCH 28/35] feat: Add a production foundry profile It can be used with FOUNDRY_PROFILE=production forge build --- don't change below this line --- ENG-4223 Took 15 minutes --- foundry/foundry.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/foundry/foundry.toml b/foundry/foundry.toml index 548f699..7cc18b3 100644 --- a/foundry/foundry.toml +++ b/foundry/foundry.toml @@ -8,6 +8,16 @@ optimizer = true optimizer_runs = 200 via_ir = true +[profile.production] +src = 'src' +out = 'out' +libs = ['lib'] +auto_detect_sol = true +evm_version = 'cancun' +optimizer = true +optimizer_runs = 44444444 +via_ir = true + [rpc_endpoints] mainnet = "${RPC_URL}" From bb7c6c25a5f747ef439c04f089bce05d9d5c57ba Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 13 Feb 2025 12:12:02 +0000 Subject: [PATCH 29/35] chore(release): 0.38.0 [skip ci] ## [0.38.0](https://github.com/propeller-heads/tycho-execution/compare/0.37.0...0.38.0) (2025-02-13) ### Features * Add a production foundry profile ([dae38ce](https://github.com/propeller-heads/tycho-execution/commit/dae38ceaf9b407d3ee93535ea0032804cbca9d59)) * Support uniswap v4 callback in TychoRouter ([591d73b](https://github.com/propeller-heads/tycho-execution/commit/591d73ba717deb1773f5c10f9085cc1175df2536)) ### Bug Fixes * Verify that the executor exists in the uni v4 callback ([4c5e3bf](https://github.com/propeller-heads/tycho-execution/commit/4c5e3bf6a9070878c684ae8d029451178201d428)) --- CHANGELOG.md | 13 +++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efcc4cf..07aa426 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## [0.38.0](https://github.com/propeller-heads/tycho-execution/compare/0.37.0...0.38.0) (2025-02-13) + + +### Features + +* Add a production foundry profile ([dae38ce](https://github.com/propeller-heads/tycho-execution/commit/dae38ceaf9b407d3ee93535ea0032804cbca9d59)) +* Support uniswap v4 callback in TychoRouter ([591d73b](https://github.com/propeller-heads/tycho-execution/commit/591d73ba717deb1773f5c10f9085cc1175df2536)) + + +### Bug Fixes + +* Verify that the executor exists in the uni v4 callback ([4c5e3bf](https://github.com/propeller-heads/tycho-execution/commit/4c5e3bf6a9070878c684ae8d029451178201d428)) + ## [0.37.0](https://github.com/propeller-heads/tycho-execution/compare/0.36.2...0.37.0) (2025-02-12) diff --git a/Cargo.lock b/Cargo.lock index c8276f8..f7e5375 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4340,7 +4340,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.37.0" +version = "0.38.0" dependencies = [ "alloy", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 5dc05e9..9e655fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.37.0" +version = "0.38.0" edition = "2021" [[bin]] From be7883affc2e481fce76dcd762215efb83905478 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Thu, 13 Feb 2025 18:40:10 +0000 Subject: [PATCH 30/35] 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 31/35] 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 32/35] 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 33/35] 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 34/35] 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); } From 002d0495018d6eef1f0f81470b3d30ea95403dc7 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 14 Feb 2025 17:11:31 +0000 Subject: [PATCH 35/35] chore(release): 0.39.0 [skip ci] ## [0.39.0](https://github.com/propeller-heads/tycho-execution/compare/0.38.0...0.39.0) (2025-02-14) ### Features * **univ4:** Implement swapping with multiple hops ([21a8c1a](https://github.com/propeller-heads/tycho-execution/commit/21a8c1a27a8370bf7471b206e78b6a2fcf38ce00)) * **univ4:** Refactor input and handle single swap case ([be7883a](https://github.com/propeller-heads/tycho-execution/commit/be7883affc2e481fce76dcd762215efb83905478)) ### Bug Fixes * Fix PLE tests that break after foundry update ([69d03f0](https://github.com/propeller-heads/tycho-execution/commit/69d03f060872bf9cce313f2420dc95c6d7554dec)) * **univ4:** Append callback data instead of prepending ([4d0f5ce](https://github.com/propeller-heads/tycho-execution/commit/4d0f5cec64af9c65f5a03685d4c89bb0dd0a897c)) * **univ4:** Make slither happy ([8a8bc69](https://github.com/propeller-heads/tycho-execution/commit/8a8bc697eb68308aedf74bd605d2f555328df99c)) --- CHANGELOG.md | 15 +++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07aa426..011c20b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## [0.39.0](https://github.com/propeller-heads/tycho-execution/compare/0.38.0...0.39.0) (2025-02-14) + + +### Features + +* **univ4:** Implement swapping with multiple hops ([21a8c1a](https://github.com/propeller-heads/tycho-execution/commit/21a8c1a27a8370bf7471b206e78b6a2fcf38ce00)) +* **univ4:** Refactor input and handle single swap case ([be7883a](https://github.com/propeller-heads/tycho-execution/commit/be7883affc2e481fce76dcd762215efb83905478)) + + +### Bug Fixes + +* Fix PLE tests that break after foundry update ([69d03f0](https://github.com/propeller-heads/tycho-execution/commit/69d03f060872bf9cce313f2420dc95c6d7554dec)) +* **univ4:** Append callback data instead of prepending ([4d0f5ce](https://github.com/propeller-heads/tycho-execution/commit/4d0f5cec64af9c65f5a03685d4c89bb0dd0a897c)) +* **univ4:** Make slither happy ([8a8bc69](https://github.com/propeller-heads/tycho-execution/commit/8a8bc697eb68308aedf74bd605d2f555328df99c)) + ## [0.38.0](https://github.com/propeller-heads/tycho-execution/compare/0.37.0...0.38.0) (2025-02-13) diff --git a/Cargo.lock b/Cargo.lock index f7e5375..56600d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4340,7 +4340,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.38.0" +version = "0.39.0" dependencies = [ "alloy", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 9e655fd..c07ebf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.38.0" +version = "0.39.0" edition = "2021" [[bin]]