From cb4c8f4e51d4f1900149288339c6a6fc75a515b1 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 6 Feb 2025 20:21:24 +0530 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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 09/21] 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 10/21] 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 11/21] 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 12/21] 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 13/21] 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 14/21] 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 15/21] 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 16/21] 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 17/21] 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 18/21] 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 19/21] 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 20/21] 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 21/21] 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); }