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); + } +}