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