From d998c88cfef300e41714c6c3c6164e761d14e2de Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 11 Feb 2025 20:01:09 +0530 Subject: [PATCH] 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);