From e62c332451d7cf0d2fc471faa7af7b26fd1a000d Mon Sep 17 00:00:00 2001 From: royvardhan Date: Mon, 10 Feb 2025 21:24:35 +0530 Subject: [PATCH] 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;