From 155460472703c441a52d80b5e449f9f701b954c0 Mon Sep 17 00:00:00 2001 From: gretzke Date: Wed, 18 Jun 2025 03:35:11 +0200 Subject: [PATCH 01/33] Initial commit: Permissioned Pools --- src/PositionManager.sol | 4 +- .../BaseAllowListChecker.sol | 13 +++ .../PermissionedPositionManager.sol | 74 +++++++++++++ .../PermissionedV4Router.sol | 53 +++++++++ .../WrappedPermissionedToken.sol | 104 ++++++++++++++++++ .../WrappedPermissionedTokenFactory.sol | 42 +++++++ .../interfaces/IAllowlistChecker.sol | 8 ++ .../interfaces/IWrappedPermissionedToken.sol | 60 ++++++++++ .../IWrappedPermissionedTokenFactory.sol | 48 ++++++++ 9 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 src/hooks/permissionedPools/BaseAllowListChecker.sol create mode 100644 src/hooks/permissionedPools/PermissionedPositionManager.sol create mode 100644 src/hooks/permissionedPools/PermissionedV4Router.sol create mode 100644 src/hooks/permissionedPools/WrappedPermissionedToken.sol create mode 100644 src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol create mode 100644 src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol create mode 100644 src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol create mode 100644 src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol diff --git a/src/PositionManager.sol b/src/PositionManager.sol index 71feac7b7..533638b1b 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -354,7 +354,7 @@ contract PositionManager is uint128 amount1Max, address owner, bytes calldata hookData - ) internal { + ) internal virtual { // mint receipt token uint256 tokenId; // tokenId is assigned to current nextTokenId before incrementing it @@ -512,7 +512,7 @@ contract PositionManager is } // implementation of abstract function DeltaResolver._pay - function _pay(Currency currency, address payer, uint256 amount) internal override { + function _pay(Currency currency, address payer, uint256 amount) internal virtual override { if (payer == address(this)) { currency.transfer(address(poolManager), amount); } else { diff --git a/src/hooks/permissionedPools/BaseAllowListChecker.sol b/src/hooks/permissionedPools/BaseAllowListChecker.sol new file mode 100644 index 000000000..fd3f0115d --- /dev/null +++ b/src/hooks/permissionedPools/BaseAllowListChecker.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IAllowlistChecker, IERC165} from "./interfaces/IAllowlistChecker.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +abstract contract BaseAllowlistChecker is IAllowlistChecker, ERC165 { + function checkAllowList(address account) public view virtual returns (bool); + + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IAllowlistChecker).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol new file mode 100644 index 000000000..f26bd07cd --- /dev/null +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { + PositionManager, + PoolKey, + IPoolManager, + IAllowanceTransfer, + IPositionDescriptor, + IWETH9, + Currency +} from "../../PositionManager.sol"; +import { + IWrappedPermissionedTokenFactory, + IWrappedPermissionedToken +} from "./interfaces/IWrappedPermissionedTokenFactory.sol"; + +contract PermissionedPositionManager is PositionManager { + IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; + + constructor( + IPoolManager _poolManager, + IAllowanceTransfer _permit2, + uint256 _unsubscribeGasLimit, + IPositionDescriptor _tokenDescriptor, + IWETH9 _weth9, + IWrappedPermissionedTokenFactory _wrappedTokenFactory + ) PositionManager(_poolManager, _permit2, _unsubscribeGasLimit, _tokenDescriptor, _weth9) { + WRAPPED_TOKEN_FACTORY = _wrappedTokenFactory; + } + + /// @dev Disables transfers of the ERC721 liquidity position tokens + function transferFrom(address, address, uint256) public pure override { + revert("Transfer disabled"); + } + + /// @dev When minting a position, verify that the sender is allowed to mint the position. This prevents a disallowed user from minting one sided liquidity. + function _mint( + PoolKey calldata poolKey, + int24 tickLower, + int24 tickUpper, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) internal override { + address permissionedToken0 = + WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(poolKey.currency0)); + address permissionedToken1 = + WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(poolKey.currency1)); + if (permissionedToken0 != address(0)) { + IWrappedPermissionedToken(permissionedToken0).isAllowed(msgSender()); + } + if (permissionedToken1 != address(0)) { + IWrappedPermissionedToken(permissionedToken1).isAllowed(msgSender()); + } + super._mint(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData); + } + + /// @dev When paying to settle, if the currency is a permissioned token, wrap the token and transfer it to the pool manager. + function _pay(Currency currency, address payer, uint256 amount) internal virtual override { + address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(currency)); + if (permissionedToken == address(0)) { + // token is not a permissioned token, use the default implementation + super._pay(currency, payer, amount); + return; + } + // token is a permissioned token, wrap the token + IWrappedPermissionedToken wrappedPermissionedToken = IWrappedPermissionedToken(Currency.unwrap(currency)); + permit2.transferFrom(payer, address(wrappedPermissionedToken), uint160(amount), permissionedToken); + wrappedPermissionedToken.wrapToPoolManager(amount); + } +} diff --git a/src/hooks/permissionedPools/PermissionedV4Router.sol b/src/hooks/permissionedPools/PermissionedV4Router.sol new file mode 100644 index 000000000..01d7ec4c6 --- /dev/null +++ b/src/hooks/permissionedPools/PermissionedV4Router.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {V4Router, IPoolManager, Currency} from "../../V4Router.sol"; +import {ReentrancyLock} from "../../base/ReentrancyLock.sol"; +import { + IWrappedPermissionedTokenFactory, + IWrappedPermissionedToken +} from "./interfaces/IWrappedPermissionedTokenFactory.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; + +contract PermissionedV4Router is V4Router, ReentrancyLock { + IAllowanceTransfer public immutable PERMIT2; + IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; + + constructor( + IPoolManager poolManager_, + IAllowanceTransfer _permit2, + IWrappedPermissionedTokenFactory wrappedTokenFactory + ) V4Router(poolManager_) { + PERMIT2 = _permit2; + WRAPPED_TOKEN_FACTORY = wrappedTokenFactory; + } + + function execute(bytes calldata input) public payable isNotLocked { + _executeActions(input); + } + + /// @notice Public view function to be used instead of msg.sender, as the contract performs self-reentrancy and at + /// times msg.sender == address(this). Instead msgSender() returns the initiator of the lock + /// @dev overrides BaseActionsRouter.msgSender in V4Router + function msgSender() public view override returns (address) { + return _getLocker(); + } + + function _pay(Currency currency, address payer, uint256 amount) internal override { + address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(currency)); + if (permissionedToken == address(0)) { + // token is not a permissioned token, use the default implementation + if (payer == address(this)) { + currency.transfer(address(poolManager), amount); + } else { + // Casting from uint256 to uint160 is safe due to limits on the total supply of a pool + PERMIT2.transferFrom(payer, address(poolManager), uint160(amount), Currency.unwrap(currency)); + } + return; + } + // token is a permissioned token, wrap the token + IWrappedPermissionedToken wrappedPermissionedToken = IWrappedPermissionedToken(Currency.unwrap(currency)); + PERMIT2.transferFrom(payer, address(wrappedPermissionedToken), uint160(amount), permissionedToken); + wrappedPermissionedToken.wrapToPoolManager(amount); + } +} diff --git a/src/hooks/permissionedPools/WrappedPermissionedToken.sol b/src/hooks/permissionedPools/WrappedPermissionedToken.sol new file mode 100644 index 000000000..7f7c827b1 --- /dev/null +++ b/src/hooks/permissionedPools/WrappedPermissionedToken.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {IWrappedPermissionedToken} from "./interfaces/IWrappedPermissionedToken.sol"; +import {IAllowlistChecker} from "./interfaces/IAllowlistChecker.sol"; + +contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedToken { + /// @inheritdoc IWrappedPermissionedToken + address public immutable POOL_MANAGER; + + /// @inheritdoc IWrappedPermissionedToken + IERC20 public immutable PERMISSIONED_TOKEN; + + /// @inheritdoc IWrappedPermissionedToken + IAllowlistChecker public allowListChecker; + + /// @inheritdoc IWrappedPermissionedToken + mapping(address wrapper => bool) public allowedWrappers; + + constructor( + IERC20 permissionedToken, + address poolManager, + address initialOwner, + IAllowlistChecker allowListChecker_ + ) ERC20(_getName(permissionedToken), _getSymbol(permissionedToken)) Ownable(initialOwner) { + PERMISSIONED_TOKEN = permissionedToken; + POOL_MANAGER = poolManager; + _updateAllowListChecker(allowListChecker_); + } + + /// @inheritdoc IWrappedPermissionedToken + function wrapToPoolManager(uint256 amount) external { + if (!allowedWrappers[msg.sender]) revert UnauthorizedWrapper(msg.sender); + uint256 availableBalance = PERMISSIONED_TOKEN.balanceOf(address(this)) - totalSupply(); + if (amount > availableBalance) revert InsufficientBalance(amount, availableBalance); + _mint(POOL_MANAGER, amount); + } + + /// @inheritdoc IWrappedPermissionedToken + function updateAllowListChecker(IAllowlistChecker newAllowListChecker) external onlyOwner { + _updateAllowListChecker(newAllowListChecker); + } + + /// @inheritdoc IWrappedPermissionedToken + function updateAllowedWrapper(address wrapper, bool allowed) external onlyOwner { + _updateAllowedWrapper(wrapper, allowed); + } + + /// @inheritdoc IWrappedPermissionedToken + function isAllowed(address account) public view returns (bool) { + return allowListChecker.checkAllowList(account); + } + + function _updateAllowListChecker(IAllowlistChecker newAllowListChecker) internal { + if (!newAllowListChecker.supportsInterface(type(IAllowlistChecker).interfaceId)) { + revert InvalidAllowListChecker(newAllowListChecker); + } + allowListChecker = newAllowListChecker; + emit AllowListCheckerUpdated(newAllowListChecker); + } + + function _updateAllowedWrapper(address wrapper, bool allowed) internal { + allowedWrappers[wrapper] = allowed; + emit AllowedWrapperUpdated(wrapper, allowed); + } + + /// @dev Overrides the ERC20._update function to add the following checks and logic: + /// - Before `settle` is called on the pool manager, the token is wrapped and minted to the pool manager + /// - When `take` is called on the pool manager, the token is automatically unwrapped when the pool manager transfers the token to the recipient + /// - Enforces that the pool manager is the only holder of the wrapped token + function _update(address from, address to, uint256 amount) internal override { + if (from == address(0)) { + assert(to == POOL_MANAGER); + // token is being wrapped + super._update(from, to, amount); + return; + } else if (from != POOL_MANAGER) { + // if the pool manager is the sender, the token is automatically unwrapped, skip the checks + revert InvalidTransfer(from, to); + } + super._update(from, to, amount); + if (from == POOL_MANAGER) { + _unwrap(to, amount); + } + // the pool manager must always be the only holder of the wrapped token + assert(balanceOf(POOL_MANAGER) == totalSupply()); + } + + function _unwrap(address account, uint256 amount) internal { + _burn(POOL_MANAGER, amount); + PERMISSIONED_TOKEN.transfer(account, amount); + } + + function _getName(IERC20 permissionedToken) private view returns (string memory) { + return string.concat("Uniswap v4 Wrapped ", ERC20(address(permissionedToken)).name()); + } + + function _getSymbol(IERC20 permissionedToken) private view returns (string memory) { + return string.concat("uw", ERC20(address(permissionedToken)).symbol()); + } +} diff --git a/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol b/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol new file mode 100644 index 000000000..09c939c09 --- /dev/null +++ b/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IWrappedPermissionedToken, IERC20} from "./interfaces/IWrappedPermissionedToken.sol"; +import {IAllowlistChecker} from "./interfaces/IAllowlistChecker.sol"; +import {WrappedPermissionedToken} from "./WrappedPermissionedToken.sol"; +import {IWrappedPermissionedTokenFactory} from "./interfaces/IWrappedPermissionedTokenFactory.sol"; + +contract WrappedPermissionedTokenFactory is IWrappedPermissionedTokenFactory { + address public immutable POOL_MANAGER; + + /// @inheritdoc IWrappedPermissionedTokenFactory + mapping(address wrappedToken => address permissionedToken) public permissionedTokenOf; + /// @inheritdoc IWrappedPermissionedTokenFactory + mapping(address wrappedToken => address permissionedToken) public verifiedPermissionedTokenOf; + + constructor(address poolManager) { + POOL_MANAGER = poolManager; + } + + /// @inheritdoc IWrappedPermissionedTokenFactory + function createWrappedPermissionedToken( + IERC20 permissionedToken, + address initialOwner, + IAllowlistChecker allowListChecker + ) external returns (address wrappedPermissionedToken) { + wrappedPermissionedToken = + address(new WrappedPermissionedToken(permissionedToken, POOL_MANAGER, initialOwner, allowListChecker)); + permissionedTokenOf[wrappedPermissionedToken] = address(permissionedToken); + emit WrappedPermissionedTokenCreated(wrappedPermissionedToken, address(permissionedToken)); + } + + /// @inheritdoc IWrappedPermissionedTokenFactory + function verifyWrappedToken(address wrappedToken) external { + IERC20 permissionedToken = IERC20(permissionedTokenOf[wrappedToken]); + if (address(permissionedToken) == address(0)) revert WrappedTokenNotFound(wrappedToken); + if (verifiedPermissionedTokenOf[wrappedToken] != address(0)) revert WrappedTokenAlreadyVerified(wrappedToken); + if (permissionedToken.balanceOf(wrappedToken) != 0) revert WrappedTokenNotVerified(wrappedToken); + verifiedPermissionedTokenOf[wrappedToken] = address(permissionedToken); + emit WrappedTokenVerified(wrappedToken, address(permissionedToken)); + } +} diff --git a/src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol b/src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol new file mode 100644 index 000000000..a714cd6f9 --- /dev/null +++ b/src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +interface IAllowlistChecker is IERC165 { + function checkAllowList(address account) external view returns (bool); +} diff --git a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol new file mode 100644 index 000000000..e8ea5befa --- /dev/null +++ b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IAllowlistChecker} from "./IAllowlistChecker.sol"; + +interface IWrappedPermissionedToken is IERC20 { + /// @notice Emitted when the allow list checker is updated + event AllowListCheckerUpdated(IAllowlistChecker indexed newAllowListChecker); + + /// @notice Emitted when an allowed wrapper is updated + event AllowedWrapperUpdated(address indexed wrapper, bool allowed); + + /// @notice Thrown when the allow list checker does not implement the IAllowListChecker interface + error InvalidAllowListChecker(IAllowlistChecker newAllowListChecker); + + /// @notice Thrown when the transfer is not interacting with the pool manager + error InvalidTransfer(address from, address to); + + /// @notice Thrown when the wrapper is not allowed to trigger transfers on the wrapped token + error UnauthorizedWrapper(address wrapper); + + /// @notice Thrown when there is an insufficient amount of permissioned tokens available to wrap + error InsufficientBalance(uint256 amount, uint256 availableBalance); + + /// @notice Updates the allow list checker + /// @param newAllowListChecker The new allow list checker + /// @dev Only callable by the owner + function updateAllowListChecker(IAllowlistChecker newAllowListChecker) external; + + /// @notice Wraps the permissioned token to the pool manager + /// @param amount The amount of permissioned tokens to wrap + /// @dev Only callable by allowed wrappers + /// @dev The `amount` must be sent to this contract before calling this function + function wrapToPoolManager(uint256 amount) external; + + /// @notice Updates the allowed wrapper that can wrap the permissioned token + /// @param wrapper The wrapper to update + /// @param allowed Whether the wrapper is allowed + /// @dev Only callable by the owner + /// @dev To ensure the wrapped token cannot be wrapped in an ERC6909 token on the PoolManager, the wrapper must only implement `swap` or `modifyLiquidity` functions + function updateAllowedWrapper(address wrapper, bool allowed) external; + + /// @notice Returns whether a transfer is allowed + /// @param account The account to check + function isAllowed(address account) external view returns (bool); + + /// @notice Returns the allow list checker + function allowListChecker() external view returns (IAllowlistChecker); + + /// @notice Returns the allowed wrappers that can wrap the permissioned token + /// @dev e.g., the permissioned pool manager, quoters or the universal router + function allowedWrappers(address wrapper) external view returns (bool); + + /// @notice Returns the Uniswap v4 pool manager + function POOL_MANAGER() external view returns (address); + + /// @notice Returns the permissioned token that is wrapped by this contract + function PERMISSIONED_TOKEN() external view returns (IERC20); +} diff --git a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol new file mode 100644 index 000000000..d5b31ac1b --- /dev/null +++ b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IWrappedPermissionedToken, IERC20} from "./IWrappedPermissionedToken.sol"; +import {IAllowlistChecker} from "./IAllowlistChecker.sol"; + +interface IWrappedPermissionedTokenFactory { + /// @notice Emitted when a wrapped permissioned token is created + event WrappedPermissionedTokenCreated(address indexed wrappedToken, address indexed permissionedToken); + + /// @notice Emitted when a wrapped permissioned token is verified + event WrappedTokenVerified(address indexed wrappedToken, address indexed permissionedToken); + + /// @notice Thrown when the wrapped token does not exist + error WrappedTokenNotFound(address wrappedToken); + + /// @notice Thrown when the wrapped token is already verified + error WrappedTokenAlreadyVerified(address wrappedToken); + + /// @notice Thrown when the wrapped token is not verified + error WrappedTokenNotVerified(address wrappedToken); + + /// @notice Creates a new wrapped permissioned token + /// @param permissionedToken The permissioned token to wrap + /// @param initialOwner The initial owner of the wrapped permissioned token + /// @param allowListChecker The allow list checker that will be used to check if transfers are allowed + function createWrappedPermissionedToken( + IERC20 permissionedToken, + address initialOwner, + IAllowlistChecker allowListChecker + ) external returns (address); + + /// @notice Verifies a wrapped permissioned token + /// @param wrappedToken The wrapped permissioned token + /// @dev This function verifies that the wrapped token has a balance of the permissioned token. This means that the wrapped token is on the allow list of the permissioned token and can be used to wrap and unwrap the permissioned token. + function verifyWrappedToken(address wrappedToken) external; + + /// @notice Returns the permissioned token of a wrapped permissioned token + /// @param wrappedToken The wrapped permissioned token + /// @return permissionedToken The permissioned token + function permissionedTokenOf(address wrappedToken) external view returns (address permissionedToken); + + /// @notice Returns the verified permissioned token of a wrapped permissioned token + /// @param wrappedToken The wrapped permissioned token + /// @return permissionedToken The verified permissioned token + /// @dev A reverse lookup of the permissioned token is required, otherwise anyone could create a wrapped token for a non-permissioned token + function verifiedPermissionedTokenOf(address wrappedToken) external view returns (address permissionedToken); +} From 92fd04a68404685d1b56ad79837414fd34c1c2b0 Mon Sep 17 00:00:00 2001 From: gretzke Date: Sat, 21 Jun 2025 04:41:39 +0200 Subject: [PATCH 02/33] Hook implementation --- .../PermissionedPositionManager.sol | 45 ++++-- .../PermissionedV4Router.sol | 138 +++++++++++++++++- .../interfaces/IWrappedPermissionedToken.sol | 2 +- 3 files changed, 165 insertions(+), 20 deletions(-) diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index f26bd07cd..ceff248eb 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -17,16 +17,23 @@ import { contract PermissionedPositionManager is PositionManager { IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; + address public immutable PERMISSIONED_SWAP_ROUTER; + error InvalidHook(); + + /// @dev as this contract and the swap router rely on each others addresses in the constructor, both contracts need + /// to be deployed using create3 to create deterministic addresses that do not depend on the constructor arguments constructor( IPoolManager _poolManager, IAllowanceTransfer _permit2, uint256 _unsubscribeGasLimit, IPositionDescriptor _tokenDescriptor, IWETH9 _weth9, - IWrappedPermissionedTokenFactory _wrappedTokenFactory + IWrappedPermissionedTokenFactory _wrappedTokenFactory, + address _permissionedSwapRouter // address needs to be calculated in advance using create3 ) PositionManager(_poolManager, _permit2, _unsubscribeGasLimit, _tokenDescriptor, _weth9) { WRAPPED_TOKEN_FACTORY = _wrappedTokenFactory; + PERMISSIONED_SWAP_ROUTER = _permissionedSwapRouter; } /// @dev Disables transfers of the ERC721 liquidity position tokens @@ -34,6 +41,14 @@ contract PermissionedPositionManager is PositionManager { revert("Transfer disabled"); } + function safeTransferFrom(address, address, uint256) public pure override { + revert("Transfer disabled"); + } + + function safeTransferFrom(address, address, uint256, bytes calldata) public pure override { + revert("Transfer disabled"); + } + /// @dev When minting a position, verify that the sender is allowed to mint the position. This prevents a disallowed user from minting one sided liquidity. function _mint( PoolKey calldata poolKey, @@ -45,16 +60,8 @@ contract PermissionedPositionManager is PositionManager { address owner, bytes calldata hookData ) internal override { - address permissionedToken0 = - WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(poolKey.currency0)); - address permissionedToken1 = - WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(poolKey.currency1)); - if (permissionedToken0 != address(0)) { - IWrappedPermissionedToken(permissionedToken0).isAllowed(msgSender()); - } - if (permissionedToken1 != address(0)) { - IWrappedPermissionedToken(permissionedToken1).isAllowed(msgSender()); - } + // allowlist is verified in the hook call + if (address(poolKey.hooks) != PERMISSIONED_SWAP_ROUTER) revert InvalidHook(); super._mint(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData); } @@ -66,9 +73,19 @@ contract PermissionedPositionManager is PositionManager { super._pay(currency, payer, amount); return; } - // token is a permissioned token, wrap the token + // token is permissioned, wrap the token and transfer it to the pool manager IWrappedPermissionedToken wrappedPermissionedToken = IWrappedPermissionedToken(Currency.unwrap(currency)); - permit2.transferFrom(payer, address(wrappedPermissionedToken), uint160(amount), permissionedToken); - wrappedPermissionedToken.wrapToPoolManager(amount); + if (payer == address(this)) { + // @audit is it necessary to check the allowlist here? + if (!wrappedPermissionedToken.isAllowed(msgSender())) { + revert Unauthorized(); + } + currency.transfer(address(wrappedPermissionedToken), amount); + wrappedPermissionedToken.wrapToPoolManager(amount); + } else { + // token is a permissioned token, wrap the token + permit2.transferFrom(payer, address(wrappedPermissionedToken), uint160(amount), permissionedToken); + wrappedPermissionedToken.wrapToPoolManager(amount); + } } } diff --git a/src/hooks/permissionedPools/PermissionedV4Router.sol b/src/hooks/permissionedPools/PermissionedV4Router.sol index 01d7ec4c6..bb8c9c8c0 100644 --- a/src/hooks/permissionedPools/PermissionedV4Router.sol +++ b/src/hooks/permissionedPools/PermissionedV4Router.sol @@ -8,18 +8,33 @@ import { IWrappedPermissionedToken } from "./interfaces/IWrappedPermissionedTokenFactory.sol"; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {Hooks, IHooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {IMsgSender} from "../../interfaces/IMsgSender.sol"; -contract PermissionedV4Router is V4Router, ReentrancyLock { +contract PermissionedV4Router is V4Router, ReentrancyLock, IHooks { IAllowanceTransfer public immutable PERMIT2; IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; + address public immutable PERMISSIONED_POSITION_MANAGER; + error Unauthorized(); + error HookNotImplemented(); + + /// @dev as this contract and the swap router rely on each others addresses in the constructor, both contracts need + /// to be deployed using create3 to create deterministic addresses that do not depend on the constructor arguments constructor( IPoolManager poolManager_, IAllowanceTransfer _permit2, - IWrappedPermissionedTokenFactory wrappedTokenFactory + IWrappedPermissionedTokenFactory wrappedTokenFactory, + address permissionedPositionManager // address needs to be calculated in advance using create3 ) V4Router(poolManager_) { PERMIT2 = _permit2; WRAPPED_TOKEN_FACTORY = wrappedTokenFactory; + PERMISSIONED_POSITION_MANAGER = permissionedPositionManager; + Hooks.validateHookPermissions(this, getHookPermissions()); } function execute(bytes calldata input) public payable isNotLocked { @@ -33,6 +48,47 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { return _getLocker(); } + function getHookPermissions() public pure returns (Hooks.Permissions memory permissions) { + permissions.beforeSwap = true; + permissions.beforeAddLiquidity = true; + } + + /// @inheritdoc IHooks + function beforeSwap(address sender, PoolKey calldata key, SwapParams calldata, bytes calldata) + external + view + returns (bytes4, BeforeSwapDelta, uint24) + { + if (sender != address(this)) revert Unauthorized(); + _verifyAllowlist(IMsgSender(sender), key); + return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + /// @inheritdoc IHooks + function beforeAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata) + external + view + returns (bytes4) + { + if (sender != PERMISSIONED_POSITION_MANAGER) revert Unauthorized(); + _verifyAllowlist(IMsgSender(sender), key); + return IHooks.beforeAddLiquidity.selector; + } + + function _verifyAllowlist(IMsgSender sender, PoolKey calldata poolKey) internal view { + _isAllowed(Currency.unwrap(poolKey.currency0), sender.msgSender()); + _isAllowed(Currency.unwrap(poolKey.currency1), sender.msgSender()); + } + + /// @dev checks if the provided token is a wrapped token by checking if it has a verified permissioned token, if yes, check the allowlist + function _isAllowed(address wrappedToken, address sender) internal view { + address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(wrappedToken); + if (permissionedToken == address(0)) return; + if (!IWrappedPermissionedToken(wrappedToken).isAllowed(sender)) { + revert Unauthorized(); + } + } + function _pay(Currency currency, address payer, uint256 amount) internal override { address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(currency)); if (permissionedToken == address(0)) { @@ -45,9 +101,81 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { } return; } - // token is a permissioned token, wrap the token + // token is permissioned, wrap the token and transfer it to the pool manager IWrappedPermissionedToken wrappedPermissionedToken = IWrappedPermissionedToken(Currency.unwrap(currency)); - PERMIT2.transferFrom(payer, address(wrappedPermissionedToken), uint160(amount), permissionedToken); - wrappedPermissionedToken.wrapToPoolManager(amount); + if (payer == address(this)) { + // allowlist check necessary to ensure a disallowed user cannot sell a permissioned token + if (!wrappedPermissionedToken.isAllowed(msgSender())) { + revert Unauthorized(); + } + currency.transfer(address(wrappedPermissionedToken), amount); + wrappedPermissionedToken.wrapToPoolManager(amount); + } else { + // token is a permissioned token, wrap the token + PERMIT2.transferFrom(payer, address(wrappedPermissionedToken), uint160(amount), permissionedToken); + wrappedPermissionedToken.wrapToPoolManager(amount); + } + } + + /// @inheritdoc IHooks + function beforeInitialize(address, PoolKey calldata, uint160) external pure returns (bytes4) { + revert HookNotImplemented(); + } + + /// @inheritdoc IHooks + function afterInitialize(address, PoolKey calldata, uint160, int24) external pure returns (bytes4) { + revert HookNotImplemented(); + } + + /// @inheritdoc IHooks + function afterAddLiquidity( + address, + PoolKey calldata, + ModifyLiquidityParams calldata, + BalanceDelta, + BalanceDelta, + bytes calldata + ) external pure returns (bytes4, BalanceDelta) { + revert HookNotImplemented(); + } + + /// @inheritdoc IHooks + function beforeRemoveLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata) + external + pure + returns (bytes4) + { + revert HookNotImplemented(); + } + + /// @inheritdoc IHooks + function afterRemoveLiquidity( + address, + PoolKey calldata, + ModifyLiquidityParams calldata, + BalanceDelta, + BalanceDelta, + bytes calldata + ) external pure returns (bytes4, BalanceDelta) { + revert HookNotImplemented(); + } + + /// @inheritdoc IHooks + function afterSwap(address, PoolKey calldata, SwapParams calldata, BalanceDelta, bytes calldata) + external + pure + returns (bytes4, int128) + { + revert HookNotImplemented(); + } + + /// @inheritdoc IHooks + function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) external pure returns (bytes4) { + revert HookNotImplemented(); + } + + /// @inheritdoc IHooks + function afterDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) external pure returns (bytes4) { + revert HookNotImplemented(); } } diff --git a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol index e8ea5befa..7549c0460 100644 --- a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol +++ b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol @@ -49,7 +49,7 @@ interface IWrappedPermissionedToken is IERC20 { function allowListChecker() external view returns (IAllowlistChecker); /// @notice Returns the allowed wrappers that can wrap the permissioned token - /// @dev e.g., the permissioned pool manager, quoters or the universal router + /// @dev e.g., the permissioned pool manager, quoters or the swap router function allowedWrappers(address wrapper) external view returns (bool); /// @notice Returns the Uniswap v4 pool manager From 8249788dfea3a627810b3f9596f492c875c131f3 Mon Sep 17 00:00:00 2001 From: gretzke Date: Wed, 25 Jun 2025 00:48:09 +0200 Subject: [PATCH 03/33] fix infinite loop --- src/hooks/permissionedPools/WrappedPermissionedToken.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/hooks/permissionedPools/WrappedPermissionedToken.sol b/src/hooks/permissionedPools/WrappedPermissionedToken.sol index 7f7c827b1..7b57c81fa 100644 --- a/src/hooks/permissionedPools/WrappedPermissionedToken.sol +++ b/src/hooks/permissionedPools/WrappedPermissionedToken.sol @@ -72,6 +72,11 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo /// - When `take` is called on the pool manager, the token is automatically unwrapped when the pool manager transfers the token to the recipient /// - Enforces that the pool manager is the only holder of the wrapped token function _update(address from, address to, uint256 amount) internal override { + if (to == address(0)) { + // prevents infinite loop when burning + super._update(from, to, amount); + return; + } if (from == address(0)) { assert(to == POOL_MANAGER); // token is being wrapped @@ -90,7 +95,7 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo } function _unwrap(address account, uint256 amount) internal { - _burn(POOL_MANAGER, amount); + _burn(account, amount); PERMISSIONED_TOKEN.transfer(account, amount); } From af9ff57072e21113d8573f5152773a2b1958f05b Mon Sep 17 00:00:00 2001 From: Daniel Gretzke Date: Thu, 26 Jun 2025 00:02:30 +0200 Subject: [PATCH 04/33] Wrapped token tests (#477) * Initial commit: wrapped token tests * fix imports --- .../BaseAllowListChecker.sol | 2 +- .../WrappedPermissionedToken.sol | 6 +- .../WrappedPermissionedTokenFactory.sol | 2 +- .../interfaces/IAllowlistChecker.sol | 2 +- .../IWrappedPermissionedTokenFactory.sol | 4 + .../PermissionedPoolsBase.sol | 59 +++++++ .../WrappedPermissionedToken.t.sol | 154 ++++++++++++++++++ .../WrappedPermissionedTokenFactory.t.sol | 98 +++++++++++ 8 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 test/hooks/permissionedPools/PermissionedPoolsBase.sol create mode 100644 test/hooks/permissionedPools/WrappedPermissionedToken.t.sol create mode 100644 test/hooks/permissionedPools/WrappedPermissionedTokenFactory.t.sol diff --git a/src/hooks/permissionedPools/BaseAllowListChecker.sol b/src/hooks/permissionedPools/BaseAllowListChecker.sol index fd3f0115d..9e19364e7 100644 --- a/src/hooks/permissionedPools/BaseAllowListChecker.sol +++ b/src/hooks/permissionedPools/BaseAllowListChecker.sol @@ -5,7 +5,7 @@ import {IAllowlistChecker, IERC165} from "./interfaces/IAllowlistChecker.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; abstract contract BaseAllowlistChecker is IAllowlistChecker, ERC165 { - function checkAllowList(address account) public view virtual returns (bool); + function checkAllowlist(address account) public view virtual returns (bool); function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { return interfaceId == type(IAllowlistChecker).interfaceId || super.supportsInterface(interfaceId); diff --git a/src/hooks/permissionedPools/WrappedPermissionedToken.sol b/src/hooks/permissionedPools/WrappedPermissionedToken.sol index 7b57c81fa..4bc13bb41 100644 --- a/src/hooks/permissionedPools/WrappedPermissionedToken.sol +++ b/src/hooks/permissionedPools/WrappedPermissionedToken.sol @@ -51,7 +51,7 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo /// @inheritdoc IWrappedPermissionedToken function isAllowed(address account) public view returns (bool) { - return allowListChecker.checkAllowList(account); + return allowListChecker.checkAllowlist(account); } function _updateAllowListChecker(IAllowlistChecker newAllowListChecker) internal { @@ -106,4 +106,8 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo function _getSymbol(IERC20 permissionedToken) private view returns (string memory) { return string.concat("uw", ERC20(address(permissionedToken)).symbol()); } + + function decimals() public view override returns (uint8) { + return ERC20(address(PERMISSIONED_TOKEN)).decimals(); + } } diff --git a/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol b/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol index 09c939c09..6b942830c 100644 --- a/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol +++ b/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol @@ -35,7 +35,7 @@ contract WrappedPermissionedTokenFactory is IWrappedPermissionedTokenFactory { IERC20 permissionedToken = IERC20(permissionedTokenOf[wrappedToken]); if (address(permissionedToken) == address(0)) revert WrappedTokenNotFound(wrappedToken); if (verifiedPermissionedTokenOf[wrappedToken] != address(0)) revert WrappedTokenAlreadyVerified(wrappedToken); - if (permissionedToken.balanceOf(wrappedToken) != 0) revert WrappedTokenNotVerified(wrappedToken); + if (permissionedToken.balanceOf(wrappedToken) == 0) revert WrappedTokenNotVerified(wrappedToken); verifiedPermissionedTokenOf[wrappedToken] = address(permissionedToken); emit WrappedTokenVerified(wrappedToken, address(permissionedToken)); } diff --git a/src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol b/src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol index a714cd6f9..faed3b725 100644 --- a/src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol +++ b/src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol @@ -4,5 +4,5 @@ pragma solidity ^0.8.0; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; interface IAllowlistChecker is IERC165 { - function checkAllowList(address account) external view returns (bool); + function checkAllowlist(address account) external view returns (bool); } diff --git a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol index d5b31ac1b..b0ad31e17 100644 --- a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol +++ b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol @@ -45,4 +45,8 @@ interface IWrappedPermissionedTokenFactory { /// @return permissionedToken The verified permissioned token /// @dev A reverse lookup of the permissioned token is required, otherwise anyone could create a wrapped token for a non-permissioned token function verifiedPermissionedTokenOf(address wrappedToken) external view returns (address permissionedToken); + + /// @notice Returns the v4 pool manager + /// @return poolManager The v4 pool manager + function POOL_MANAGER() external view returns (address poolManager); } diff --git a/test/hooks/permissionedPools/PermissionedPoolsBase.sol b/test/hooks/permissionedPools/PermissionedPoolsBase.sol new file mode 100644 index 000000000..d3cc14e31 --- /dev/null +++ b/test/hooks/permissionedPools/PermissionedPoolsBase.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {BaseAllowlistChecker} from "../../../src/hooks/permissionedPools/BaseAllowListChecker.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockToken is ERC20 { + constructor() ERC20("MockToken", "MT") {} + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } + + function burn(address from, uint256 amount) public { + _burn(from, amount); + } + + function decimals() public pure override returns (uint8) { + return 6; + } +} + +contract MockPermissionedToken is MockToken { + mapping(address account => bool allowed) public isAllowed; + + error Unauthorized(); + + function setAllowlist(address account, bool allowed) public { + isAllowed[account] = allowed; + } + + function _update(address from, address to, uint256 amount) internal override { + if (!isAllowed[to]) revert Unauthorized(); + super._update(from, to, amount); + } +} + +contract MockAllowlistChecker is BaseAllowlistChecker { + MockPermissionedToken public token; + + constructor(MockPermissionedToken token_) { + token = token_; + } + + function checkAllowlist(address account) public view override returns (bool) { + return token.isAllowed(account); + } +} + +contract PermissionedPoolsBase is Test { + MockPermissionedToken public permissionedToken; + MockAllowlistChecker public allowlistChecker; + + function setUp() public virtual { + permissionedToken = new MockPermissionedToken(); + allowlistChecker = new MockAllowlistChecker(permissionedToken); + } +} diff --git a/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol b/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol new file mode 100644 index 000000000..ded897dc1 --- /dev/null +++ b/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import { + IWrappedPermissionedToken, + IAllowlistChecker +} from "../../../src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol"; +import {IERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {PermissionedPoolsBase, MockAllowlistChecker} from "./PermissionedPoolsBase.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract WrappedPermissionedTokenTest is PermissionedPoolsBase { + IWrappedPermissionedToken public wrappedPermissionedToken; + address public mockPoolManager; + address public owner; + + function setUp() public override { + super.setUp(); + mockPoolManager = makeAddr("mockPoolManager"); + owner = makeAddr("owner"); + bytes memory args = abi.encode(permissionedToken, mockPoolManager, owner, allowlistChecker); + bytes memory initcode = + abi.encodePacked(vm.getCode("WrappedPermissionedToken.sol:WrappedPermissionedToken"), args); + assembly { + sstore(wrappedPermissionedToken.slot, create(0, add(initcode, 0x20), mload(initcode))) + } + permissionedToken.setAllowlist(address(wrappedPermissionedToken), true); + vm.prank(owner); + wrappedPermissionedToken.updateAllowedWrapper(address(this), true); + } + + function test_InitialState() public view { + assertEq(IERC20Metadata(address(wrappedPermissionedToken)).name(), "Uniswap v4 Wrapped MockToken"); + assertEq(IERC20Metadata(address(wrappedPermissionedToken)).symbol(), "uwMT"); + assertEq(IERC20Metadata(address(wrappedPermissionedToken)).decimals(), permissionedToken.decimals()); + assertEq(wrappedPermissionedToken.totalSupply(), 0); + assertEq(wrappedPermissionedToken.balanceOf(mockPoolManager), 0); + assertEq(address(wrappedPermissionedToken.allowListChecker()), address(allowlistChecker)); + assertEq(wrappedPermissionedToken.POOL_MANAGER(), mockPoolManager); + assertEq(address(wrappedPermissionedToken.PERMISSIONED_TOKEN()), address(permissionedToken)); + } + + function testRevert_WhenNotOwner(address account) public { + vm.assume(account != owner); + vm.startPrank(account); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, account)); + wrappedPermissionedToken.updateAllowedWrapper(account, true); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, account)); + wrappedPermissionedToken.updateAllowListChecker(allowlistChecker); + vm.stopPrank(); + } + + function testRevert_WhenNotAllowedWrapper(address wrapper) public { + vm.assume(wrapper != address(this)); + assertFalse(wrappedPermissionedToken.allowedWrappers(wrapper)); + vm.prank(wrapper); + vm.expectRevert(abi.encodeWithSelector(IWrappedPermissionedToken.UnauthorizedWrapper.selector, wrapper)); + wrappedPermissionedToken.wrapToPoolManager(100); + } + + function testRevert_WhenInsufficientBalance(uint256 amount, uint256 transferAmount) public { + vm.assume(amount != 0); + transferAmount = bound(amount, 0, amount - 1); + permissionedToken.mint(address(wrappedPermissionedToken), transferAmount); + vm.expectRevert( + abi.encodeWithSelector(IWrappedPermissionedToken.InsufficientBalance.selector, amount, transferAmount) + ); + wrappedPermissionedToken.wrapToPoolManager(amount); + } + + function test_WrapToPoolManager(uint256 amount, uint256 actualAmount) public { + actualAmount = bound(amount, amount, type(uint256).max); + permissionedToken.mint(address(wrappedPermissionedToken), actualAmount); + vm.expectEmit(true, true, true, true); + emit IERC20.Transfer(address(0), mockPoolManager, amount); + wrappedPermissionedToken.wrapToPoolManager(amount); + assertEq(wrappedPermissionedToken.balanceOf(mockPoolManager), amount); + assertEq(permissionedToken.balanceOf(mockPoolManager), actualAmount - amount); + } + + function test_UpdateAllowedWrapper(address wrapper, bool allowed) public { + vm.assume(wrapper != address(this)); + assertFalse(wrappedPermissionedToken.allowedWrappers(wrapper)); + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit IWrappedPermissionedToken.AllowedWrapperUpdated(wrapper, allowed); + wrappedPermissionedToken.updateAllowedWrapper(wrapper, allowed); + assertEq(wrappedPermissionedToken.allowedWrappers(wrapper), allowed); + } + + function testRevert_WhenNotSupportedInterfaceEOA(IAllowlistChecker newAllowListCheckerEOA) public { + vm.assume(newAllowListCheckerEOA != allowlistChecker); + vm.prank(owner); + vm.expectRevert(); // expect revert without data + wrappedPermissionedToken.updateAllowListChecker(newAllowListCheckerEOA); + } + + function testRevert_WhenNotSupportedInterfaceContract() public { + IAllowlistChecker newAllowListCheckerContract = new ImproperAllowlistChecker(); + vm.prank(owner); + vm.expectRevert( + abi.encodeWithSelector( + IWrappedPermissionedToken.InvalidAllowListChecker.selector, newAllowListCheckerContract + ) + ); + wrappedPermissionedToken.updateAllowListChecker(newAllowListCheckerContract); + } + + function test_UpdateAllowListChecker() public { + IAllowlistChecker newAllowListChecker = new MockAllowlistChecker(permissionedToken); + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit IWrappedPermissionedToken.AllowListCheckerUpdated(newAllowListChecker); + wrappedPermissionedToken.updateAllowListChecker(newAllowListChecker); + assertEq(address(wrappedPermissionedToken.allowListChecker()), address(newAllowListChecker)); + } + + function testRevert_WhenInvalidTransfer(address from, address to) public { + vm.assume(from != address(0) && from != mockPoolManager); + vm.assume(to != address(0) && to != mockPoolManager); + vm.prank(from); + vm.expectRevert(abi.encodeWithSelector(IWrappedPermissionedToken.InvalidTransfer.selector, from, to)); + wrappedPermissionedToken.transfer(to, 0); + } + + function test_UnwrapOnPoolManagerTransfer(uint256 mintAmount, uint256 transferAmount, address recipient) public { + vm.assume( + recipient != address(0) && recipient != mockPoolManager && recipient != address(wrappedPermissionedToken) + ); + assertEq(permissionedToken.balanceOf(recipient), 0); + permissionedToken.setAllowlist(recipient, true); + transferAmount = bound(transferAmount, 0, mintAmount); + permissionedToken.mint(address(wrappedPermissionedToken), mintAmount); + wrappedPermissionedToken.wrapToPoolManager(mintAmount); + vm.prank(mockPoolManager); + vm.expectEmit(true, true, true, true); + emit IERC20.Transfer(mockPoolManager, recipient, transferAmount); + vm.expectEmit(true, true, true, true); + emit IERC20.Transfer(recipient, address(0), transferAmount); + wrappedPermissionedToken.transfer(recipient, transferAmount); + assertEq(wrappedPermissionedToken.balanceOf(mockPoolManager), mintAmount - transferAmount); + assertEq(permissionedToken.balanceOf(recipient), transferAmount); + } +} + +contract ImproperAllowlistChecker is IAllowlistChecker { + function checkAllowlist(address) public pure returns (bool) { + return true; + } + + function supportsInterface(bytes4) external pure returns (bool) { + return false; + } +} diff --git a/test/hooks/permissionedPools/WrappedPermissionedTokenFactory.t.sol b/test/hooks/permissionedPools/WrappedPermissionedTokenFactory.t.sol new file mode 100644 index 000000000..a36faeb9a --- /dev/null +++ b/test/hooks/permissionedPools/WrappedPermissionedTokenFactory.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import { + IWrappedPermissionedToken, + IAllowlistChecker +} from "../../../src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol"; +import {IWrappedPermissionedTokenFactory} from + "../../../src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol"; +import {IERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {PermissionedPoolsBase, MockAllowlistChecker} from "./PermissionedPoolsBase.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract WrappedPermissionedTokenFactoryTest is PermissionedPoolsBase { + IWrappedPermissionedTokenFactory public wrappedPermissionedTokenFactory; + address public mockPoolManager; + + function setUp() public override { + super.setUp(); + mockPoolManager = makeAddr("mockPoolManager"); + bytes memory args = abi.encode(mockPoolManager); + bytes memory initcode = + abi.encodePacked(vm.getCode("WrappedPermissionedTokenFactory.sol:WrappedPermissionedTokenFactory"), args); + assembly { + sstore(wrappedPermissionedTokenFactory.slot, create(0, add(initcode, 0x20), mload(initcode))) + } + } + + function test_InitialState() public view { + assertEq(wrappedPermissionedTokenFactory.POOL_MANAGER(), mockPoolManager); + } + + function test_CreateWrappedPermissionedToken(address initialOwner) public { + vm.assume(initialOwner != address(0)); + address expectedWrappedPermissionedToken = vm.computeCreateAddress(address(wrappedPermissionedTokenFactory), 1); + vm.expectEmit(true, true, true, true); + emit IWrappedPermissionedTokenFactory.WrappedPermissionedTokenCreated( + expectedWrappedPermissionedToken, address(permissionedToken) + ); + address wrappedPermissionedToken = wrappedPermissionedTokenFactory.createWrappedPermissionedToken( + permissionedToken, initialOwner, allowlistChecker + ); + assertEq(wrappedPermissionedToken, expectedWrappedPermissionedToken); + assertEq( + wrappedPermissionedTokenFactory.permissionedTokenOf(wrappedPermissionedToken), address(permissionedToken) + ); + assertEq(wrappedPermissionedTokenFactory.verifiedPermissionedTokenOf(wrappedPermissionedToken), address(0)); + } + + function testRevert_WhenWrappedTokenNotDeployed(address wrappedToken) public { + vm.expectRevert( + abi.encodeWithSelector(IWrappedPermissionedTokenFactory.WrappedTokenNotFound.selector, wrappedToken) + ); + wrappedPermissionedTokenFactory.verifyWrappedToken(wrappedToken); + } + + function testRevert_WhenWrappedTokenNotVerified() public { + address wrappedPermissionedToken = wrappedPermissionedTokenFactory.createWrappedPermissionedToken( + permissionedToken, makeAddr("initialOwner"), allowlistChecker + ); + vm.expectRevert( + abi.encodeWithSelector( + IWrappedPermissionedTokenFactory.WrappedTokenNotVerified.selector, wrappedPermissionedToken + ) + ); + wrappedPermissionedTokenFactory.verifyWrappedToken(wrappedPermissionedToken); + } + + function test_VerifyWrappedToken() public { + address wrappedPermissionedToken = wrappedPermissionedTokenFactory.createWrappedPermissionedToken( + permissionedToken, makeAddr("initialOwner"), allowlistChecker + ); + permissionedToken.setAllowlist(wrappedPermissionedToken, true); + permissionedToken.mint(wrappedPermissionedToken, 1); + vm.expectEmit(true, true, true, true); + emit IWrappedPermissionedTokenFactory.WrappedTokenVerified(wrappedPermissionedToken, address(permissionedToken)); + wrappedPermissionedTokenFactory.verifyWrappedToken(wrappedPermissionedToken); + assertEq( + wrappedPermissionedTokenFactory.verifiedPermissionedTokenOf(wrappedPermissionedToken), + address(permissionedToken) + ); + } + + function testRevert_WhenWrappedTokenAlreadyVerified() public { + address wrappedPermissionedToken = wrappedPermissionedTokenFactory.createWrappedPermissionedToken( + permissionedToken, makeAddr("initialOwner"), allowlistChecker + ); + permissionedToken.setAllowlist(wrappedPermissionedToken, true); + permissionedToken.mint(wrappedPermissionedToken, 1); + wrappedPermissionedTokenFactory.verifyWrappedToken(wrappedPermissionedToken); + vm.expectRevert( + abi.encodeWithSelector( + IWrappedPermissionedTokenFactory.WrappedTokenAlreadyVerified.selector, wrappedPermissionedToken + ) + ); + wrappedPermissionedTokenFactory.verifyWrappedToken(wrappedPermissionedToken); + } +} From c6b593193a194d051beb1efc6b78dd00e4c7049e Mon Sep 17 00:00:00 2001 From: ens_408 Date: Wed, 9 Jul 2025 16:14:38 -0400 Subject: [PATCH 05/33] Permissioned router tests, permissioned posm tests (#479) * initial commit * touch ups, rm shadowed vars * bring in wrapped change * format * nit change to pure * external, pure nit * reset getCode paths * relative imports * fix isolated mint_clear_revert test * use relative paths for mock allowlist * use BaseAllowList not BaseAllowlist * change allowList to allowlist * move to permissioned tests folder * all tests working * merging new changes from permissioned-pools * touch ups, rm shadowed vars * bring in wrapped change * format * nit change to pure * external, pure nit * reset getCode paths * relative imports * fix isolated mint_clear_revert test * use relative paths for mock allowlist * use BaseAllowList not BaseAllowlist * change allowList to allowlist * move to permissioned tests folder * all tests working * replace mockAllowlist with permissionedPoolsBase * rm mock allowlist * run linter * rm unused * change casing on const * reorganize vars * add most of the tests mentioned in comments * all paths covered * seperate hooks contract instead of delegate router * minor cleanup * use all combinations of permissioned and normal in liq tests * spacing nit * nit, edit note * add clarifying comment to wrapped factory balance check * remove circular dependency from permissioned pools implementation (#482) * initial commit * rm fallback from permissioned v4 router, leave receive * fix github ci * use currency instead of ierc20 in some tests * add comment to hooks on why verifyAllowlist is sufficient * inherit from BaseHook contract * remove unused import, generate gas snapshot --------- Co-authored-by: gretzke --- snapshots/PermissionedV4RouterTest.json | 3 + src/base/DeltaResolver.sol | 2 +- .../permissionedPools/PermissionedHooks.sol | 75 + .../PermissionedPositionManager.sol | 14 +- .../PermissionedV4Router.sol | 138 +- .../WrappedPermissionedTokenFactory.sol | 1 + src/utils/HookMinerCreate3.sol | 82 + .../PermissionedPositionManager.t.sol | 1080 ++++++++++++ .../PermissionedV4Router.t.sol | 1453 +++++++++++++++++ .../permissionedPools/mocks/MockHooks.sol | 37 + .../shared/PermissionedDeployers.sol | 316 ++++ .../shared/PermissionedPosmTestSetup.sol | 260 +++ .../shared/PermissionedRoutingTestHelpers.sol | 419 +++++ test/shared/Deploy.sol | 39 + 14 files changed, 3799 insertions(+), 120 deletions(-) create mode 100644 snapshots/PermissionedV4RouterTest.json create mode 100644 src/hooks/permissionedPools/PermissionedHooks.sol create mode 100644 src/utils/HookMinerCreate3.sol create mode 100644 test/hooks/permissionedPools/PermissionedPositionManager.t.sol create mode 100644 test/hooks/permissionedPools/PermissionedV4Router.t.sol create mode 100644 test/hooks/permissionedPools/mocks/MockHooks.sol create mode 100644 test/hooks/permissionedPools/shared/PermissionedDeployers.sol create mode 100644 test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol create mode 100644 test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol diff --git a/snapshots/PermissionedV4RouterTest.json b/snapshots/PermissionedV4RouterTest.json new file mode 100644 index 000000000..ecfe63058 --- /dev/null +++ b/snapshots/PermissionedV4RouterTest.json @@ -0,0 +1,3 @@ +{ + "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "223119" +} \ No newline at end of file diff --git a/src/base/DeltaResolver.sol b/src/base/DeltaResolver.sol index bd0d39bf0..7646e6006 100644 --- a/src/base/DeltaResolver.sol +++ b/src/base/DeltaResolver.sol @@ -76,7 +76,7 @@ abstract contract DeltaResolver is ImmutableState { } /// @notice Calculates the amount for a settle action - function _mapSettleAmount(uint256 amount, Currency currency) internal view returns (uint256) { + function _mapSettleAmount(uint256 amount, Currency currency) internal view virtual returns (uint256) { if (amount == ActionConstants.CONTRACT_BALANCE) { return currency.balanceOfSelf(); } else if (amount == ActionConstants.OPEN_DELTA) { diff --git a/src/hooks/permissionedPools/PermissionedHooks.sol b/src/hooks/permissionedPools/PermissionedHooks.sol new file mode 100644 index 000000000..1736737b8 --- /dev/null +++ b/src/hooks/permissionedPools/PermissionedHooks.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IPoolManager, Currency} from "../../V4Router.sol"; +import { + IWrappedPermissionedTokenFactory, + IWrappedPermissionedToken +} from "./interfaces/IWrappedPermissionedTokenFactory.sol"; +import {IMsgSender} from "../../interfaces/IMsgSender.sol"; +import {ActionConstants} from "../../libraries/ActionConstants.sol"; +import {ReentrancyLock} from "../../base/ReentrancyLock.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {Hooks, IHooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BaseHook} from "../../utils/BaseHook.sol"; + +contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { + IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; + + error Unauthorized(); + + constructor(IPoolManager manager, IWrappedPermissionedTokenFactory wrappedTokenFactory) BaseHook(manager) { + WRAPPED_TOKEN_FACTORY = wrappedTokenFactory; + Hooks.validateHookPermissions(this, getHookPermissions()); + } + + /// @dev Returns the hook permissions configuration for this contract + function getHookPermissions() public pure override returns (Hooks.Permissions memory permissions) { + permissions.beforeSwap = true; + permissions.beforeAddLiquidity = true; + } + + /// @dev Does not need to verify msg.sender address directly, as verifying the allowlist is sufficient due to the fact that any valid senders are allowed wrappers + function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata, bytes calldata) + internal + view + override + returns (bytes4, BeforeSwapDelta, uint24) + { + _verifyAllowlist(IMsgSender(sender), key); + return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + /// @dev Does not need to verify msg.sender address directly, as verifying the allowlist is sufficient due to the fact that any valid senders are allowed wrappers + function _beforeAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata) + internal + view + override + returns (bytes4) + { + _verifyAllowlist(IMsgSender(sender), key); + return IHooks.beforeAddLiquidity.selector; + } + + /// @dev checks if the sender is allowed to access both tokens in the pool + function _verifyAllowlist(IMsgSender sender, PoolKey calldata poolKey) internal view { + _isAllowed(Currency.unwrap(poolKey.currency0), sender.msgSender(), address(sender)); + _isAllowed(Currency.unwrap(poolKey.currency1), sender.msgSender(), address(sender)); + } + + /// @dev checks if the provided token is a wrapped token by checking if it has a verified permissioned token, if yes, check the allowlist + function _isAllowed(address wrappedToken, address sender, address router) internal view { + address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(wrappedToken); + if (permissionedToken == address(0)) return; + if ( + !IWrappedPermissionedToken(wrappedToken).isAllowed(sender) + || !IWrappedPermissionedToken(wrappedToken).allowedWrappers(router) + ) { + revert Unauthorized(); + } + } +} diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index ceff248eb..e7dae84cc 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -14,15 +14,15 @@ import { IWrappedPermissionedTokenFactory, IWrappedPermissionedToken } from "./interfaces/IWrappedPermissionedTokenFactory.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; contract PermissionedPositionManager is PositionManager { IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; - address public immutable PERMISSIONED_SWAP_ROUTER; + IHooks public immutable PERMISSIONED_HOOKS; error InvalidHook(); - /// @dev as this contract and the swap router rely on each others addresses in the constructor, both contracts need - /// to be deployed using create3 to create deterministic addresses that do not depend on the constructor arguments + /// @dev as this contract must know the hooks address in advance, it must be passed in as a constructor argument constructor( IPoolManager _poolManager, IAllowanceTransfer _permit2, @@ -30,10 +30,10 @@ contract PermissionedPositionManager is PositionManager { IPositionDescriptor _tokenDescriptor, IWETH9 _weth9, IWrappedPermissionedTokenFactory _wrappedTokenFactory, - address _permissionedSwapRouter // address needs to be calculated in advance using create3 + IHooks _permissionedHooks ) PositionManager(_poolManager, _permit2, _unsubscribeGasLimit, _tokenDescriptor, _weth9) { WRAPPED_TOKEN_FACTORY = _wrappedTokenFactory; - PERMISSIONED_SWAP_ROUTER = _permissionedSwapRouter; + PERMISSIONED_HOOKS = _permissionedHooks; } /// @dev Disables transfers of the ERC721 liquidity position tokens @@ -61,7 +61,7 @@ contract PermissionedPositionManager is PositionManager { bytes calldata hookData ) internal override { // allowlist is verified in the hook call - if (address(poolKey.hooks) != PERMISSIONED_SWAP_ROUTER) revert InvalidHook(); + if (poolKey.hooks != PERMISSIONED_HOOKS) revert InvalidHook(); super._mint(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData); } @@ -80,7 +80,7 @@ contract PermissionedPositionManager is PositionManager { if (!wrappedPermissionedToken.isAllowed(msgSender())) { revert Unauthorized(); } - currency.transfer(address(wrappedPermissionedToken), amount); + Currency.wrap(permissionedToken).transfer(address(wrappedPermissionedToken), amount); wrappedPermissionedToken.wrapToPoolManager(amount); } else { // token is a permissioned token, wrap the token diff --git a/src/hooks/permissionedPools/PermissionedV4Router.sol b/src/hooks/permissionedPools/PermissionedV4Router.sol index bb8c9c8c0..8212d9666 100644 --- a/src/hooks/permissionedPools/PermissionedV4Router.sol +++ b/src/hooks/permissionedPools/PermissionedV4Router.sol @@ -1,40 +1,42 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {V4Router, IPoolManager, Currency} from "../../V4Router.sol"; +import {IMsgSender} from "../../interfaces/IMsgSender.sol"; +import {ActionConstants} from "../../libraries/ActionConstants.sol"; import {ReentrancyLock} from "../../base/ReentrancyLock.sol"; +import {V4Router, IPoolManager, Currency} from "../../V4Router.sol"; import { IWrappedPermissionedTokenFactory, IWrappedPermissionedToken } from "./interfaces/IWrappedPermissionedTokenFactory.sol"; +import {PermissionedHooks} from "./PermissionedHooks.sol"; +import {IWETH9} from "../../interfaces/external/IWETH9.sol"; + import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; -import {Hooks, IHooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {IMsgSender} from "../../interfaces/IMsgSender.sol"; +import {Hooks, IHooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -contract PermissionedV4Router is V4Router, ReentrancyLock, IHooks { +contract PermissionedV4Router is V4Router, ReentrancyLock { IAllowanceTransfer public immutable PERMIT2; IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; - address public immutable PERMISSIONED_POSITION_MANAGER; + IWETH9 public immutable WETH9; error Unauthorized(); error HookNotImplemented(); + error InvalidEthSender(); - /// @dev as this contract and the swap router rely on each others addresses in the constructor, both contracts need - /// to be deployed using create3 to create deterministic addresses that do not depend on the constructor arguments constructor( IPoolManager poolManager_, - IAllowanceTransfer _permit2, + IAllowanceTransfer permit2, IWrappedPermissionedTokenFactory wrappedTokenFactory, - address permissionedPositionManager // address needs to be calculated in advance using create3 + IWETH9 weth9 ) V4Router(poolManager_) { - PERMIT2 = _permit2; + PERMIT2 = permit2; WRAPPED_TOKEN_FACTORY = wrappedTokenFactory; - PERMISSIONED_POSITION_MANAGER = permissionedPositionManager; - Hooks.validateHookPermissions(this, getHookPermissions()); + WETH9 = weth9; } function execute(bytes calldata input) public payable isNotLocked { @@ -48,47 +50,6 @@ contract PermissionedV4Router is V4Router, ReentrancyLock, IHooks { return _getLocker(); } - function getHookPermissions() public pure returns (Hooks.Permissions memory permissions) { - permissions.beforeSwap = true; - permissions.beforeAddLiquidity = true; - } - - /// @inheritdoc IHooks - function beforeSwap(address sender, PoolKey calldata key, SwapParams calldata, bytes calldata) - external - view - returns (bytes4, BeforeSwapDelta, uint24) - { - if (sender != address(this)) revert Unauthorized(); - _verifyAllowlist(IMsgSender(sender), key); - return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); - } - - /// @inheritdoc IHooks - function beforeAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata) - external - view - returns (bytes4) - { - if (sender != PERMISSIONED_POSITION_MANAGER) revert Unauthorized(); - _verifyAllowlist(IMsgSender(sender), key); - return IHooks.beforeAddLiquidity.selector; - } - - function _verifyAllowlist(IMsgSender sender, PoolKey calldata poolKey) internal view { - _isAllowed(Currency.unwrap(poolKey.currency0), sender.msgSender()); - _isAllowed(Currency.unwrap(poolKey.currency1), sender.msgSender()); - } - - /// @dev checks if the provided token is a wrapped token by checking if it has a verified permissioned token, if yes, check the allowlist - function _isAllowed(address wrappedToken, address sender) internal view { - address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(wrappedToken); - if (permissionedToken == address(0)) return; - if (!IWrappedPermissionedToken(wrappedToken).isAllowed(sender)) { - revert Unauthorized(); - } - } - function _pay(Currency currency, address payer, uint256 amount) internal override { address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(currency)); if (permissionedToken == address(0)) { @@ -108,7 +69,7 @@ contract PermissionedV4Router is V4Router, ReentrancyLock, IHooks { if (!wrappedPermissionedToken.isAllowed(msgSender())) { revert Unauthorized(); } - currency.transfer(address(wrappedPermissionedToken), amount); + Currency.wrap(permissionedToken).transfer(address(wrappedPermissionedToken), amount); wrappedPermissionedToken.wrapToPoolManager(amount); } else { // token is a permissioned token, wrap the token @@ -117,65 +78,18 @@ contract PermissionedV4Router is V4Router, ReentrancyLock, IHooks { } } - /// @inheritdoc IHooks - function beforeInitialize(address, PoolKey calldata, uint160) external pure returns (bytes4) { - revert HookNotImplemented(); - } - - /// @inheritdoc IHooks - function afterInitialize(address, PoolKey calldata, uint160, int24) external pure returns (bytes4) { - revert HookNotImplemented(); - } - - /// @inheritdoc IHooks - function afterAddLiquidity( - address, - PoolKey calldata, - ModifyLiquidityParams calldata, - BalanceDelta, - BalanceDelta, - bytes calldata - ) external pure returns (bytes4, BalanceDelta) { - revert HookNotImplemented(); - } - - /// @inheritdoc IHooks - function beforeRemoveLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata) - external - pure - returns (bytes4) - { - revert HookNotImplemented(); - } - - /// @inheritdoc IHooks - function afterRemoveLiquidity( - address, - PoolKey calldata, - ModifyLiquidityParams calldata, - BalanceDelta, - BalanceDelta, - bytes calldata - ) external pure returns (bytes4, BalanceDelta) { - revert HookNotImplemented(); - } - - /// @inheritdoc IHooks - function afterSwap(address, PoolKey calldata, SwapParams calldata, BalanceDelta, bytes calldata) - external - pure - returns (bytes4, int128) - { - revert HookNotImplemented(); - } - - /// @inheritdoc IHooks - function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) external pure returns (bytes4) { - revert HookNotImplemented(); + /// @notice Calculates the amount for a settle action + function _mapSettleAmount(uint256 amount, Currency currency) internal view override returns (uint256) { + address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(currency)); + // use the default implementation unless the currency is a permissioned token with a balance on the router + if (permissionedToken == address(0) || amount != ActionConstants.CONTRACT_BALANCE) { + return super._mapSettleAmount(amount, currency); + } + return Currency.wrap(permissionedToken).balanceOfSelf(); } - /// @inheritdoc IHooks - function afterDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) external pure returns (bytes4) { - revert HookNotImplemented(); + /// @notice To receive ETH from WETH + receive() external payable { + if (msg.sender != address(WETH9) && msg.sender != address(poolManager)) revert InvalidEthSender(); } } diff --git a/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol b/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol index 6b942830c..8a5568adb 100644 --- a/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol +++ b/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol @@ -35,6 +35,7 @@ contract WrappedPermissionedTokenFactory is IWrappedPermissionedTokenFactory { IERC20 permissionedToken = IERC20(permissionedTokenOf[wrappedToken]); if (address(permissionedToken) == address(0)) revert WrappedTokenNotFound(wrappedToken); if (verifiedPermissionedTokenOf[wrappedToken] != address(0)) revert WrappedTokenAlreadyVerified(wrappedToken); + // this requires that the verifier has some comntrol or ownership of the permissioned token if (permissionedToken.balanceOf(wrappedToken) == 0) revert WrappedTokenNotVerified(wrappedToken); verifiedPermissionedTokenOf[wrappedToken] = address(permissionedToken); emit WrappedTokenVerified(wrappedToken, address(permissionedToken)); diff --git a/src/utils/HookMinerCreate3.sol b/src/utils/HookMinerCreate3.sol new file mode 100644 index 000000000..4ae914688 --- /dev/null +++ b/src/utils/HookMinerCreate3.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {CREATE3} from "solmate/src/utils/CREATE3.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; + +/// @title HookMinerCreate3 +/// @notice A minimal library for mining hook addresses using CREATE3 +library HookMinerCreate3 { + // mask to slice out the bottom 14 bit of the address + uint160 constant FLAG_MASK = Hooks.ALL_HOOK_MASK; // 0000 ... 0000 0011 1111 1111 1111 + + // Maximum number of iterations to find a salt, avoid infinite loops or MemoryOOG + // (arbitrarily set) + uint256 constant MAX_LOOP = 160_444; + + /// @notice Find a salt that produces a hook address with the desired `flags` using CREATE3 + /// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy) + /// @param flags The desired flags for the hook address. Example `uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | ...)` + /// @param creationCode The creation code of a hook contract. Example: `type(Counter).creationCode` + /// @param constructorArgs The encoded constructor arguments of a hook contract. Example: `abi.encode(address(manager))` + /// @return (hookAddress, salt) The hook deploys to `hookAddress` when using `salt` with CREATE3 + function find(address deployer, uint160 flags, bytes memory creationCode, bytes memory constructorArgs) + internal + view + returns (address, bytes32) + { + flags = flags & FLAG_MASK; // mask for only the bottom 14 bits + bytes memory creationCodeWithArgs = abi.encodePacked(creationCode, constructorArgs); + + address hookAddress; + for (uint256 salt; salt < MAX_LOOP; salt++) { + hookAddress = computeAddress(deployer, salt, creationCodeWithArgs); + + // if the hook's bottom 14 bits match the desired flags AND the address does not have bytecode, we found a match + if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) { + return (hookAddress, bytes32(salt)); + } + } + revert("HookMinerCreate3: could not find salt"); + } + + /// @notice Precompute a contract address deployed via CREATE3 + /// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy) + /// @param salt The salt used to deploy the hook + function computeAddress(address deployer, uint256 salt, bytes memory) internal pure returns (address hookAddress) { + bytes32 saltBytes = bytes32(salt); + return CREATE3.getDeployed(saltBytes, deployer); + } + + /// @notice Find a salt that produces a hook address with the desired `flags` using CREATE3 with a custom salt prefix + /// @param deployer The address that will deploy the hook + /// @param flags The desired flags for the hook address + /// @param creationCode The creation code of a hook contract + /// @param constructorArgs The encoded constructor arguments of a hook contract + /// @param saltPrefix A prefix to use for the salt (e.g., "permissioned-router-") + /// @return (hookAddress, salt) The hook deploys to `hookAddress` when using `salt` with CREATE3 + function findWithPrefix( + address deployer, + uint160 flags, + bytes memory creationCode, + bytes memory constructorArgs, + string memory saltPrefix + ) internal view returns (address, bytes32) { + flags = flags & FLAG_MASK; // mask for only the bottom 14 bits + bytes memory creationCodeWithArgs = abi.encodePacked(creationCode, constructorArgs); + + address hookAddress; + for (uint256 i; i < MAX_LOOP; i++) { + bytes32 salt = keccak256(abi.encodePacked(saltPrefix, i)); + hookAddress = computeAddress(deployer, uint256(salt), creationCodeWithArgs); + + // if the hook's bottom 14 bits match the desired flags AND the address does not have bytecode, we found a match + if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) { + return (hookAddress, salt); + } + } + revert("HookMinerCreate3: could not find salt with prefix"); + } +} diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol new file mode 100644 index 000000000..70516be99 --- /dev/null +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -0,0 +1,1080 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; +import {IERC721} from "forge-std/interfaces/IERC721.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; + +import {IPositionManager} from "../../../src/interfaces/IPositionManager.sol"; +import {Actions} from "../../../src/libraries/Actions.sol"; +import {DeltaResolver} from "../../../src/base/DeltaResolver.sol"; +import {PositionConfig} from "test/shared/PositionConfig.sol"; +import {SlippageCheck} from "../../../src/libraries/SlippageCheck.sol"; +import {BaseActionsRouter} from "../../../src/base/BaseActionsRouter.sol"; +import {ActionConstants} from "../../../src/libraries/ActionConstants.sol"; +import {LiquidityFuzzers} from "../../shared/fuzz/LiquidityFuzzers.sol"; +import {Planner, Plan} from "../../shared/Planner.sol"; +import {PermissionedPosmTestSetup, BalanceInfo} from "./shared/PermissionedPosmTestSetup.sol"; +import {ReentrantToken} from "../../mocks/ReentrantToken.sol"; +import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +import {MockAllowlistChecker, MockPermissionedToken} from "./PermissionedPoolsBase.sol"; +import {WrappedPermissionedToken, IERC20} from "../../../src/hooks/permissionedPools/WrappedPermissionedToken.sol"; + +contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, LiquidityFuzzers { + using FixedPointMathLib for uint256; + using StateLibrary for IPoolManager; + + PoolId public poolId; + PoolId public poolIdFake; + + // Permissioned components + MockAllowlistChecker public mockAllowListChecker; + WrappedPermissionedToken public wrappedToken0; + WrappedPermissionedToken public wrappedToken2; + MockERC20 public originalToken0; + MockERC20 public originalToken2; + IPositionManager public secondaryPosm; + IPositionManager public tertiaryPosm; + + PoolKey public key0; + PoolKey public key1; + PoolKey public key2; + PoolKey public keyFake0; + PoolKey public keyFake1; + PoolKey public keyFake2; + + // Test Users + address public alice = makeAddr("ALICE"); + address public unauthorizedUser = makeAddr("UNAUTHORIZED"); + + function setUp() public { + permit2 = IAllowanceTransfer(deployPermit2()); + + deployFreshManagerAndRoutersPermissioned(address(permit2), address(_WETH9)); + (currency0, currency1) = deployMintAndApprove2Currencies(true, false); + currency2 = deployMintAndApproveCurrency(true); + + setUpPosms(); + setupPermissionedComponents(); + setupPool(); + + // set up approvals for alice + seedBalance(alice); + approvePosmFor(alice); + } + + function setUpPosms() internal { + deployAndApprovePosm( + manager, address(wrappedTokenFactory), address(permissionedHooks), keccak256("permissionedPosm") + ); + + // alternate position manager using different hooks + secondaryPosm = deployAndApprovePosmOnly( + manager, address(wrappedTokenFactory), address(secondaryPermissionedHooks), keccak256("secondaryPosm") + ); + + // alternate position manager using the same hooks + tertiaryPosm = deployAndApprovePosmOnly( + manager, address(wrappedTokenFactory), address(permissionedHooks), keccak256("tertiaryPosm") + ); + } + + function setupPool() internal { + (key0, poolId) = + initPool(Currency.wrap(address(wrappedToken0)), currency1, permissionedHooks, 3000, SQRT_PRICE_1_1); + (key1, poolId) = + initPool(currency1, Currency.wrap(address(wrappedToken2)), permissionedHooks, 3000, SQRT_PRICE_1_1); + (key2, poolId) = initPool( + Currency.wrap(address(wrappedToken0)), + Currency.wrap(address(wrappedToken2)), + permissionedHooks, + 3000, + SQRT_PRICE_1_1 + ); + (keyFake0, poolId) = + initPool(Currency.wrap(address(wrappedToken0)), currency1, secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1); + (keyFake1, poolId) = + initPool(currency1, Currency.wrap(address(wrappedToken2)), secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1); + (keyFake2, poolId) = initPool( + Currency.wrap(address(wrappedToken0)), + Currency.wrap(address(wrappedToken2)), + secondaryPermissionedHooks, + 3000, + SQRT_PRICE_1_1 + ); + } + + function setupPermissionedComponents() internal { + mockAllowListChecker = new MockAllowlistChecker(MockPermissionedToken(Currency.unwrap(currency0))); + setUpCurrencyZero(); + setUpCurrencyTwo(); + } + + function setUpCurrencyZero() internal { + setUpAllowlist(currency0); + originalToken0 = MockERC20(Currency.unwrap(currency0)); + // ensure expected ordering + while (true) { + wrappedToken0 = WrappedPermissionedToken( + wrappedTokenFactory.createWrappedPermissionedToken( + IERC20(address(originalToken0)), address(this), mockAllowListChecker + ) + ); + if (address(wrappedToken0) < Currency.unwrap(currency1)) { + break; + } + } + setUpWrappedToken(wrappedToken0, currency0); + } + + function setUpCurrencyTwo() internal { + setUpAllowlist(currency2); + originalToken2 = MockERC20(Currency.unwrap(currency2)); + + // ensure expected ordering + while (true) { + wrappedToken2 = WrappedPermissionedToken( + wrappedTokenFactory.createWrappedPermissionedToken( + IERC20(address(originalToken2)), address(this), mockAllowListChecker + ) + ); + if (Currency.unwrap(currency1) < address(wrappedToken2)) { + break; + } + } + setUpWrappedToken(wrappedToken2, currency2); + } + + function setUpAllowlist(Currency currency) internal { + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(this), true); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(alice, true); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), true); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(secondaryPosm), true); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(wrappedTokenFactory), true); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), true); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(manager), true); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(permissionedSwapRouter), true); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(address(permissionedHooks), true); + } + + function setUpWrappedToken(WrappedPermissionedToken wrappedToken, Currency currency) internal { + wrappedToPermissioned[Currency.wrap(address(wrappedToken))] = currency; + + MockPermissionedToken(Currency.unwrap(currency)).mint(address(this), 1000 ether); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(wrappedToken), true); + + // wrapped token contract must have a non-zero balance of the permissioned token + currency.transfer(address(wrappedToken), 1); + + wrappedToken.updateAllowedWrapper(address(manager), true); + wrappedToken.updateAllowedWrapper(address(secondaryPosm), true); + wrappedToken.updateAllowedWrapper(address(lpm), true); + wrappedToken.updateAllowedWrapper(address(permissionedSwapRouter), true); + + wrappedTokenFactory.verifyWrappedToken(address(wrappedToken)); + } + + function test_modifyLiquidities_reverts_deadlinePassed() public { + _test_modifyLiquidities_reverts_deadlinePassed(key0); + _test_modifyLiquidities_reverts_deadlinePassed(key1); + _test_modifyLiquidities_reverts_deadlinePassed(key2); + } + + function _test_modifyLiquidities_reverts_deadlinePassed(PoolKey memory key) internal { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: 0, tickUpper: 60}); + bytes memory calls = getMintEncoded(config, 1e18, ActionConstants.MSG_SENDER, ""); + + uint256 deadline = vm.getBlockTimestamp() - 1; + + vm.expectRevert(abi.encodeWithSelector(IPositionManager.DeadlinePassed.selector, deadline)); + lpm.modifyLiquidities(calls, deadline); + } + + function test_modifyLiquidities_reverts_mismatchedLengths() public { + Plan memory planner = Planner.init(); + planner.add(Actions.MINT_POSITION, abi.encode("test")); + planner.add(Actions.BURN_POSITION, abi.encode("test")); + + bytes[] memory badParams = new bytes[](1); + + vm.expectRevert(BaseActionsRouter.InputLengthMismatch.selector); + lpm.modifyLiquidities(abi.encode(planner.actions, badParams), block.timestamp + 1); + } + + function test_modifyLiquidities_reverts_reentrancy() public { + PoolKey memory key; + + // Create a reentrant token and initialize the pool + Currency reentrantToken = Currency.wrap(address(new ReentrantToken(lpm))); + (currency0, currency1) = (Currency.unwrap(reentrantToken) < Currency.unwrap(currency1)) + ? (reentrantToken, currency1) + : (currency1, reentrantToken); + + // Set up approvals for the reentrant token + approvePosmCurrency(reentrantToken); + (key, poolId) = initPool(currency0, currency1, permissionedHooks, 3000, SQRT_PRICE_1_1); + + // Try to add liquidity at that range, but the token reenters posm + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: -int24(key.tickSpacing), tickUpper: int24(key.tickSpacing)}); + + bytes memory calls = getMintEncoded(config, 1e18, ActionConstants.MSG_SENDER, ""); + + // Permit2.transferFrom does not bubble the ContractLocked error and instead reverts with its own error + vm.expectRevert("TRANSFER_FROM_FAILED"); + lpm.modifyLiquidities(calls, block.timestamp + 1); + } + + function test_fuzz_mint_withLiquidityDelta(ModifyLiquidityParams memory params, uint160 sqrtPriceX96) public { + bound(sqrtPriceX96, MIN_PRICE_LIMIT, MAX_PRICE_LIMIT); + + _test_fuzz_mint_withLiquidityDelta(key0, params, sqrtPriceX96); + _test_fuzz_mint_withLiquidityDelta(key1, params, sqrtPriceX96); + _test_fuzz_mint_withLiquidityDelta(key2, params, sqrtPriceX96); + } + + function _test_fuzz_mint_withLiquidityDelta( + PoolKey memory key, + ModifyLiquidityParams memory params, + uint160 sqrtPriceX96 + ) internal { + params = createFuzzyLiquidityParams(key, params, sqrtPriceX96); + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // liquidity is a uint + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + + uint256 balance0Before = getPermissionedCurrency(key.currency0).balanceOfSelf(); + uint256 balance1Before = getPermissionedCurrency(key.currency1).balanceOfSelf(); + uint256 balance0ManagerBefore = key.currency0.balanceOf(address(manager)); + uint256 balance1ManagerBefore = key.currency1.balanceOf(address(manager)); + uint256 tokenId = lpm.nextTokenId(); + + mint(config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES); + + uint256 balance0ManagerAfter = key.currency0.balanceOf(address(manager)); + uint256 balance1ManagerAfter = key.currency1.balanceOf(address(manager)); + uint256 liquidity = lpm.getPositionLiquidity(tokenId); + + assertEq(tokenId, lpm.nextTokenId() - 1); + assertEq(IERC721(address(lpm)).ownerOf(tokenId), address(this)); + assertEq(liquidity, uint256(params.liquidityDelta)); + assertEq( + balance0Before - getPermissionedCurrency(key.currency0).balanceOfSelf(), + balance0ManagerAfter - balance0ManagerBefore, + "incorrect amount0" + ); + assertEq( + balance1Before - getPermissionedCurrency(key.currency1).balanceOfSelf(), + balance1ManagerAfter - balance1ManagerBefore, + "incorrect amount1" + ); + } + + function test_mint_exactTokenRatios() public { + _test_mint_exactTokenRatios(key0); + _test_mint_exactTokenRatios(key1); + _test_mint_exactTokenRatios(key2); + } + + function _test_mint_exactTokenRatios(PoolKey memory key) internal { + int24 tickLower = -int24(key.tickSpacing); + int24 tickUpper = int24(key.tickSpacing); + uint256 amount0Desired = 100e18; + uint256 amount1Desired = 100e18; + uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + amount0Desired, + amount1Desired + ); + + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + + uint256 balance0Before = getPermissionedCurrency(key.currency0).balanceOfSelf(); + uint256 balance1Before = getPermissionedCurrency(key.currency1).balanceOfSelf(); + uint256 balance0ManagerBefore = key.currency0.balanceOf(address(manager)); + uint256 tokenId = lpm.nextTokenId(); + + mint(config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES); + + uint256 balance0After = getPermissionedCurrency(key.currency0).balanceOfSelf(); + uint256 balance1After = getPermissionedCurrency(key.currency1).balanceOfSelf(); + uint256 balance0ManagerAfter = key.currency0.balanceOf(address(manager)); + assertEq(tokenId, lpm.nextTokenId() - 1); + assertEq(IERC721(address(lpm)).ownerOf(tokenId), address(this)); + assertEq(balance0Before - balance0After, balance0ManagerAfter - balance0ManagerBefore); + assertEq(balance1Before - balance1After, amount1Desired); + } + + function test_mint_toRecipient() public { + _test_mint_toRecipient(key0); + _test_mint_toRecipient(key1); + _test_mint_toRecipient(key2); + } + + function _test_mint_toRecipient(PoolKey memory key) internal { + int24 tickLower = -int24(key.tickSpacing); + int24 tickUpper = int24(key.tickSpacing); + uint256 amount0Desired = 100e18; + uint256 amount1Desired = 100e18; + uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + amount0Desired, + amount1Desired + ); + + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + + uint256 tokenId = lpm.nextTokenId(); + + BalanceInfo memory balanceInfoBefore = getBalanceInfoSelfAndManager(key); + + // mint to specific recipient, alice, not using the recipient constants + mint(config, liquidityToAdd, alice, ZERO_BYTES); + + BalanceInfo memory balanceInfoAfter = getBalanceInfoSelfAndManager(key); + + assertEq(tokenId, lpm.nextTokenId() - 1); + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + assertEq( + balanceInfoBefore.balance0 - balanceInfoAfter.balance0, + balanceInfoAfter.balance0Manager - balanceInfoBefore.balance0Manager + ); + assertEq( + balanceInfoBefore.balance1 - balanceInfoAfter.balance1, + balanceInfoAfter.balance1Manager - balanceInfoBefore.balance1Manager + ); + assertEq(balanceInfoBefore.balance0 - balanceInfoAfter.balance0, amount0Desired); + assertEq(balanceInfoBefore.balance1 - balanceInfoAfter.balance1, amount1Desired); + } + + function test_fuzz_mint_recipient(ModifyLiquidityParams memory seedParams) public { + _test_fuzz_mint_recipient(key0, seedParams); + _test_fuzz_mint_recipient(key1, seedParams); + _test_fuzz_mint_recipient(key2, seedParams); + } + + function _test_fuzz_mint_recipient(PoolKey memory key, ModifyLiquidityParams memory seedParams) internal { + ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1); + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + + Currency currency0_ = getPermissionedCurrency(key.currency0); + Currency currency1_ = getPermissionedCurrency(key.currency1); + Currency currency0wrapped = key.currency0; + Currency currency1wrapped = key.currency1; + uint256 tokenId = lpm.nextTokenId(); + uint256 balance0Before = currency0_.balanceOfSelf(); + uint256 balance1Before = currency1_.balanceOfSelf(); + uint256 balance0BeforeAlice = currency0_.balanceOf(alice); + uint256 balance1BeforeAlice = currency1_.balanceOf(alice); + uint256 balance0ManagerBefore = currency0wrapped.balanceOf(address(manager)); + uint256 balance1ManagerBefore = currency1wrapped.balanceOf(address(manager)); + + mint(config, liquidityToAdd, alice, ZERO_BYTES); + + uint256 balance0ManagerAfter = currency0wrapped.balanceOf(address(manager)); + uint256 balance1ManagerAfter = currency1wrapped.balanceOf(address(manager)); + + // alice was not the payer + assertEq(tokenId, lpm.nextTokenId() - 1); + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + assertEq(balance0Before - currency0_.balanceOfSelf(), balance0ManagerAfter - balance0ManagerBefore); + assertEq(balance1Before - currency1_.balanceOfSelf(), balance1ManagerAfter - balance1ManagerBefore); + assertEq(currency0_.balanceOf(alice), balance0BeforeAlice); + assertEq(currency1_.balanceOf(alice), balance1BeforeAlice); + } + + /// @dev clear cannot be used on mint (negative delta) + function test_fuzz_mint_clear_revert(ModifyLiquidityParams memory seedParams) public { + _test_fuzz_mint_clear_revert(key0, seedParams); + _test_fuzz_mint_clear_revert(key1, seedParams); + _test_fuzz_mint_clear_revert(key2, seedParams); + } + + function _test_fuzz_mint_clear_revert(PoolKey memory key, ModifyLiquidityParams memory seedParams) internal { + ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1); + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + + Plan memory planner = Planner.init(); + planner.add( + Actions.MINT_POSITION, + abi.encode( + config.poolKey, + config.tickLower, + config.tickUpper, + liquidityToAdd, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + address(this), + ZERO_BYTES + ) + ); + planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency0, type(uint256).max)); + planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency1, type(uint256).max)); + bytes memory calls = planner.encode(); + + Currency negativeDeltaCurrency = key.currency0; + // because we're fuzzing the range, single-sided mint with currency1 means currency0Delta = 0 and currency1Delta < 0 + if (config.tickUpper <= 0) { + negativeDeltaCurrency = key.currency1; + } + + vm.expectRevert(abi.encodeWithSelector(DeltaResolver.DeltaNotPositive.selector, (negativeDeltaCurrency))); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_mint_slippage_revertAmount0() public { + _test_mint_slippage_revertAmount0(key0); + _test_mint_slippage_revertAmount0(key1); + _test_mint_slippage_revertAmount0(key2); + } + + function _test_mint_slippage_revertAmount0(PoolKey memory key) internal { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 1e18; + (uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint128(liquidity) + ); + + bytes memory calls = + getMintEncoded(config, liquidity, 1 wei, MAX_SLIPPAGE_INCREASE, ActionConstants.MSG_SENDER, ZERO_BYTES); + + vm.expectRevert(abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, 1 wei, amount0 + 1)); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_mint_slippage_revertAmount1() public { + _test_mint_slippage_revertAmount1(key0); + _test_mint_slippage_revertAmount1(key1); + _test_mint_slippage_revertAmount1(key2); + } + + function _test_mint_slippage_revertAmount1(PoolKey memory key) internal { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 1e18; + (, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint128(liquidity) + ); + + bytes memory calls = + getMintEncoded(config, liquidity, MAX_SLIPPAGE_INCREASE, 1 wei, ActionConstants.MSG_SENDER, ZERO_BYTES); + + vm.expectRevert(abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, 1 wei, amount1 + 1)); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_mint_slippage_exactDoesNotRevert() public { + _test_mint_slippage_exactDoesNotRevert(key0); + _test_mint_slippage_exactDoesNotRevert(key1); + _test_mint_slippage_exactDoesNotRevert(key2); + } + + function _test_mint_slippage_exactDoesNotRevert(PoolKey memory key) internal { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 1e18; + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint128(liquidity) + ); + uint128 slippage = uint128(amount0) + 1; + + uint256 balance0ManagerBefore = key.currency0.balanceOf(address(manager)); + uint256 balance1ManagerBefore = key.currency1.balanceOf(address(manager)); + + assertEq(amount0, amount1); // symmetric liquidity + + bytes memory calls = + getMintEncoded(config, liquidity, slippage, slippage, ActionConstants.MSG_SENDER, ZERO_BYTES); + + lpm.modifyLiquidities(calls, _deadline); + + uint256 balance0ManagerAfter = key.currency0.balanceOf(address(manager)); + uint256 balance1ManagerAfter = key.currency1.balanceOf(address(manager)); + + assertEq(balance0ManagerAfter - balance0ManagerBefore, slippage); + assertEq(balance1ManagerAfter - balance1ManagerBefore, slippage); + } + + function test_mint_slippage_revert_swap() public { + _test_mint_slippage_revert_swap(key0); + _test_mint_slippage_revert_swap(key1); + _test_mint_slippage_revert_swap(key2); + } + + function _test_mint_slippage_revert_swap(PoolKey memory key) internal { + // swapping will cause a slippage revert + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 100e18; + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint128(liquidity) + ); + uint128 slippage = uint128(amount0) + 1; + + assertEq(amount0, amount1); // symmetric liquidity + + bytes memory calls = + getMintEncoded(config, liquidity, slippage, slippage, ActionConstants.MSG_SENDER, ZERO_BYTES); + + // swap to move the price and cause a slippage revert + swap(key, true, -1e18); + + vm.expectRevert( + abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, slippage, 1199947202932782783) + ); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_permissioned_mint_allowed_user() public { + _test_permissioned_mint_allowed_user(key0); + _test_permissioned_mint_allowed_user(key1); + _test_permissioned_mint_allowed_user(key2); + } + + function _test_permissioned_mint_allowed_user(PoolKey memory key) internal { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 1e18; + uint256 tokenId = lpm.nextTokenId(); + + // Alice is in the allowlist, so she should be able to mint + vm.prank(alice); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + } + + function test_permissioned_mint_alternate_posm_reverts() public { + // secondary posm uses different hooks contract + _test_permissioned_mint_alternate_posm_reverts(key0, secondaryPosm); + _test_permissioned_mint_alternate_posm_reverts(key1, secondaryPosm); + _test_permissioned_mint_alternate_posm_reverts(key2, secondaryPosm); + + // tertiary posm uses the same hooks contract + _test_permissioned_mint_alternate_posm_reverts(key0, tertiaryPosm); + _test_permissioned_mint_alternate_posm_reverts(key1, tertiaryPosm); + _test_permissioned_mint_alternate_posm_reverts(key2, tertiaryPosm); + } + + function _test_permissioned_mint_alternate_posm_reverts(PoolKey memory key, IPositionManager posm) internal { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 1e18; + + // we don't use the helper, so we can choose which position manager to use + bytes memory calls = getMintEncoded(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + + vm.prank(alice); + vm.expectRevert(); + posm.modifyLiquidities(calls, block.timestamp + 1); + } + + function test_permissioned_mint_disallowed_user_reverts() public { + _test_permissioned_mint_disallowed_user_reverts(key0); + _test_permissioned_mint_disallowed_user_reverts(key1); + _test_permissioned_mint_disallowed_user_reverts(key2); + } + + function _test_permissioned_mint_disallowed_user_reverts(PoolKey memory key) internal { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 1e18; + + // Add some tokens to unauthorized user + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, true); + MockPermissionedToken(Currency.unwrap(currency0)).mint(unauthorizedUser, 1000e18); + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, false); + MockERC20(Currency.unwrap(currency1)).mint(unauthorizedUser, 1000e18); + + vm.startPrank(unauthorizedUser); + + // Approve tokens for the position manager + originalToken0.approve(address(permit2), type(uint256).max); + MockERC20(Currency.unwrap(currency1)).approve(address(permit2), type(uint256).max); + permit2.approve(address(originalToken0), address(lpm), type(uint160).max, type(uint48).max); + permit2.approve(Currency.unwrap(currency1), address(lpm), type(uint160).max, type(uint48).max); + + // This should revert because the user is not in the allowlist + vm.expectRevert(); + mint(config, liquidity, unauthorizedUser, ZERO_BYTES); + vm.stopPrank(); + } + + function test_permissioned_mint_increase_allowed_user() public { + _test_permissioned_mint_increase_allowed_user(key0); + _test_permissioned_mint_increase_allowed_user(key1); + _test_permissioned_mint_increase_allowed_user(key2); + } + + function _test_permissioned_mint_increase_allowed_user(PoolKey memory key) internal { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 1e18; + uint256 tokenId = lpm.nextTokenId(); + + vm.startPrank(alice); + + // add initial liquidity + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + + tokenId = lpm.nextTokenId(); + + // Increase liquidity + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + vm.stopPrank(); + + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + } + + function test_permissioned_mint_increase_disallowed_user_reverts() public { + _test_permissioned_mint_increase_disallowed_user_reverts(key0); + _test_permissioned_mint_increase_disallowed_user_reverts(key1); + _test_permissioned_mint_increase_disallowed_user_reverts(key2); + } + + function _test_permissioned_mint_increase_disallowed_user_reverts(PoolKey memory key) internal { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 1e17; + uint256 tokenId = lpm.nextTokenId(); + + vm.startPrank(alice); + + // add initial liquidity + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, false); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, false); + + tokenId = lpm.nextTokenId(); + + // Increasing liquidity should revert because the user is no longer in the allowlist + vm.expectRevert(); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + vm.stopPrank(); + + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, true); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, true); + } + + function test_unpermissioned_sided_mint_disallowed_user_reverts() public { + _test_unpermissioned_sided_mint_disallowed_user_reverts(key0, false); + _test_unpermissioned_sided_mint_disallowed_user_reverts(key1, true); + // key2 has no unpermissioned side + } + + function _test_unpermissioned_sided_mint_disallowed_user_reverts(PoolKey memory key, bool useZero) internal { + PositionConfig memory config; + Currency currencyUnpermissioned; + + uint256 liquidity = 1e18; + + if (useZero) { + currencyUnpermissioned = getPermissionedCurrency(key.currency0); + config = PositionConfig({poolKey: key, tickLower: -180, tickUpper: -60}); + } else { + currencyUnpermissioned = getPermissionedCurrency(key.currency1); + config = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 180}); + } + + // Add some tokens to unauthorized user + MockERC20(Currency.unwrap(currencyUnpermissioned)).mint(unauthorizedUser, 1000e18); + + vm.startPrank(unauthorizedUser); + + // Approve tokens for the position manager and permit2 + MockERC20(Currency.unwrap(currencyUnpermissioned)).approve(address(permit2), type(uint256).max); + permit2.approve(Currency.unwrap(currencyUnpermissioned), address(lpm), type(uint160).max, type(uint48).max); + + // This should revert because the user is not in the allowlist + vm.expectRevert(); + mint(config, liquidity, unauthorizedUser, ZERO_BYTES); + vm.stopPrank(); + } + + function test_unpermissioned_sided_mint_allowed_user() public { + _test_unpermissioned_sided_mint_allowed_user(key0, false); + _test_unpermissioned_sided_mint_allowed_user(key1, true); + // key2 has no unpermissioned side + } + + function _test_unpermissioned_sided_mint_allowed_user(PoolKey memory key, bool useZero) internal { + PositionConfig memory config; + Currency currencyPermissioned; + + uint256 liquidity = 1e18; + uint256 tokenId = lpm.nextTokenId(); + + if (useZero) { + currencyPermissioned = getPermissionedCurrency(key.currency0); + config = PositionConfig({poolKey: key, tickLower: -180, tickUpper: -60}); + } else { + currencyPermissioned = getPermissionedCurrency(key.currency1); + config = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 180}); + } + + vm.startPrank(alice); + + // Approve tokens for the position manager and permit2 + MockERC20(Currency.unwrap(currencyPermissioned)).approve(address(permit2), type(uint256).max); + permit2.approve(Currency.unwrap(currencyPermissioned), address(lpm), type(uint160).max, type(uint48).max); + + mint(config, liquidity, alice, ZERO_BYTES); + vm.stopPrank(); + + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + } + + function test_permissioned_sided_mint_disallowed_user_reverts() public { + _test_permissioned_sided_mint_disallowed_user_reverts(key0, true); + _test_permissioned_sided_mint_disallowed_user_reverts(key1, false); + _test_permissioned_sided_mint_disallowed_user_reverts(key2, true); + } + + function _test_permissioned_sided_mint_disallowed_user_reverts(PoolKey memory key, bool useZero) internal { + PositionConfig memory config; + Currency currencyPermissioned; + + uint256 liquidity = 1e18; + + if (useZero) { + currencyPermissioned = getPermissionedCurrency(key.currency0); + config = PositionConfig({poolKey: key, tickLower: -180, tickUpper: -60}); + } else { + currencyPermissioned = getPermissionedCurrency(key.currency1); + config = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 180}); + } + + // Add some tokens to unauthorized user + MockPermissionedToken(Currency.unwrap(currencyPermissioned)).setAllowlist(unauthorizedUser, true); + MockPermissionedToken(Currency.unwrap(currencyPermissioned)).mint(unauthorizedUser, 1000e18); + MockPermissionedToken(Currency.unwrap(currencyPermissioned)).setAllowlist(unauthorizedUser, false); + + vm.startPrank(unauthorizedUser); + + // Approve tokens for the position manager and permit2 + MockERC20(Currency.unwrap(currencyPermissioned)).approve(address(permit2), type(uint256).max); + permit2.approve(Currency.unwrap(currencyPermissioned), address(lpm), type(uint160).max, type(uint48).max); + + // This should revert because the user is not in the allowlist + vm.expectRevert(); + mint(config, liquidity, unauthorizedUser, ZERO_BYTES); + vm.stopPrank(); + } + + function test_permissioned_single_sided_mint_allowed_user() public { + _test_permissioned_sided_mint_allowed_user(key0, true); + _test_permissioned_sided_mint_allowed_user(key1, false); + _test_permissioned_sided_mint_allowed_user(key2, true); + } + + function _test_permissioned_sided_mint_allowed_user(PoolKey memory key, bool useZero) internal { + PositionConfig memory config; + + uint256 liquidity = 1e18; + uint256 tokenId = lpm.nextTokenId(); + + if (useZero) { + config = PositionConfig({poolKey: key, tickLower: -180, tickUpper: -60}); + } else { + config = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 180}); + } + + vm.prank(alice); + mint(config, liquidity, alice, ZERO_BYTES); + + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + } + + function test_fuzz_burn_emptyPosition(ModifyLiquidityParams memory params) public { + _test_fuzz_burn_emptyPosition(key0, params); + _test_fuzz_burn_emptyPosition(key1, params); + _test_fuzz_burn_emptyPosition(key2, params); + } + + function _test_fuzz_burn_emptyPosition(PoolKey memory key, ModifyLiquidityParams memory params) internal { + uint256 balance0Start = key.currency0.balanceOfSelf(); + uint256 balance1Start = key.currency1.balanceOfSelf(); + uint256 tokenId; + + // create liquidity we can burn + (tokenId, params) = addFuzzyLiquidity(lpm, ActionConstants.MSG_SENDER, key, params, SQRT_PRICE_1_1, ZERO_BYTES); + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + assertEq(tokenId, lpm.nextTokenId() - 1); + assertEq(IERC721(address(lpm)).ownerOf(1), address(this)); + + uint256 liquidity = lpm.getPositionLiquidity(tokenId); + + assertEq(liquidity, uint256(params.liquidityDelta)); + + uint256 balance0BeforeBurn = getPermissionedCurrency(key.currency0).balanceOfSelf(); + uint256 balance1BeforeBurn = getPermissionedCurrency(key.currency1).balanceOfSelf(); + uint256 balance0ManagerBefore = (key.currency0).balanceOf(address(manager)); + uint256 balance1ManagerBefore = (key.currency1).balanceOf(address(manager)); + + // burn liquidity + decreaseLiquidity(tokenId, config, liquidity, ZERO_BYTES); + + uint256 balance0ManagerAfter = (key.currency0).balanceOf(address(manager)); + uint256 balance1ManagerAfter = (key.currency1).balanceOf(address(manager)); + + liquidity = lpm.getPositionLiquidity(tokenId); + + assertEq(liquidity, 0); + assertEq( + getPermissionedCurrency(key.currency0).balanceOfSelf(), + balance0BeforeBurn + balance0ManagerBefore - balance0ManagerAfter + ); + assertEq( + getPermissionedCurrency(key.currency1).balanceOfSelf(), + balance1BeforeBurn + balance1ManagerBefore - balance1ManagerAfter + ); + + IERC721(address(lpm)).ownerOf(lpm.nextTokenId() - 1); + + // see note from position manager test: "no tokens were lost ... fuzzer showing off by 1 sometimes" + assertApproxEqAbs(key.currency0.balanceOfSelf(), balance0Start, 1 wei); + assertApproxEqAbs(key.currency1.balanceOfSelf(), balance1Start, 1 wei); + } + + function test_initialize() public { + _test_initialize(currency0, currency1); + _test_initialize(Currency.wrap(address(wrappedToken0)), currency1); + _test_initialize(currency1, Currency.wrap(address(wrappedToken2))); + _test_initialize(Currency.wrap(address(wrappedToken0)), Currency.wrap(address(wrappedToken2))); + } + + function _test_initialize(Currency currency0_, Currency currency1_) internal { + // initialize a new pool + PoolKey memory key = + PoolKey({currency0: currency0_, currency1: currency1_, fee: 0, tickSpacing: 100, hooks: IHooks(address(0))}); + + lpm.initializePool(key, SQRT_PRICE_1_1); + + (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId()); + + assertEq(sqrtPriceX96, SQRT_PRICE_1_1); + assertEq(tick, 0); + assertEq(protocolFee, 0); + assertEq(lpFee, key.fee); + } + + function test_fuzz_initialize(uint160 sqrtPrice, uint24 fee) public { + _test_fuzz_initialize(currency0, currency1, sqrtPrice, fee); + _test_fuzz_initialize(Currency.wrap(address(wrappedToken0)), currency1, sqrtPrice, fee); + _test_fuzz_initialize(currency1, Currency.wrap(address(wrappedToken2)), sqrtPrice, fee); + _test_fuzz_initialize( + Currency.wrap(address(wrappedToken0)), Currency.wrap(address(wrappedToken2)), sqrtPrice, fee + ); + } + + function _test_fuzz_initialize(Currency currency0_, Currency currency1_, uint160 sqrtPrice, uint24 fee) internal { + sqrtPrice = + uint160(bound(sqrtPrice, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE_MINUS_MIN_SQRT_PRICE_MINUS_ONE)); + fee = uint24(bound(fee, 0, LPFeeLibrary.MAX_LP_FEE)); + PoolKey memory key = PoolKey({ + currency0: currency0_, + currency1: currency1_, + fee: fee, + tickSpacing: 10, + hooks: IHooks(address(0)) + }); + + lpm.initializePool(key, sqrtPrice); + + (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId()); + + assertEq(sqrtPriceX96, sqrtPrice); + assertEq(tick, TickMath.getTickAtSqrtPrice(sqrtPrice)); + assertEq(protocolFee, 0); + assertEq(lpFee, fee); + } + + function test_liquidity_token_transfer_reverts() public { + _test_liquidity_token_transfer_reverts(key0); + _test_liquidity_token_transfer_reverts(key1); + _test_liquidity_token_transfer_reverts(key2); + } + + function _test_liquidity_token_transfer_reverts(PoolKey memory key) internal { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 1e18; + uint256 tokenId = lpm.nextTokenId(); + + vm.startPrank(alice); + + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + + vm.expectRevert(); + IERC721(address(lpm)).transferFrom(alice, address(this), tokenId); + vm.stopPrank(); + } + + function test_safe_transfer_from_reverts() public { + bytes memory encodedCall = + abi.encodeWithSignature("safeTransferFrom(address,address,uint256)", alice, address(this), 1); + + address target = address(lpm); + + vm.startPrank(alice); + vm.expectRevert(abi.encode("Transfer disabled")); + + bool success; + assembly ("memory-safe") { + success := call(gas(), target, 0, add(encodedCall, 0x20), mload(encodedCall), 0, 0) + } + vm.stopPrank(); + } + + function test_safe_transfer_from_with_bytes_reverts() public { + bytes memory encodedCall = + abi.encodeWithSignature("safeTransferFrom(address,address,uint256,bytes)", alice, address(this), 1, "abcd"); + address target = address(lpm); + + vm.startPrank(alice); + vm.expectRevert(abi.encode("Transfer disabled")); + + bool success; + assembly ("memory-safe") { + success := call(gas(), target, 0, add(encodedCall, 0x20), mload(encodedCall), 0, 0) + } + vm.stopPrank(); + } + + function test_mint_from_contract_balance() public { + _test_mint_from_contract_balance(key0); + _test_mint_from_contract_balance(key1); + _test_mint_from_contract_balance(key2); + } + + /// @dev This function had to be split up and refactored to avoid stack-too-deep errors + function _test_mint_from_contract_balance(PoolKey memory key) internal { + uint256 amount0ToTransfer = 100e18; + uint256 amount1ToTransfer = 100e18; + + // Setup and transfer tokens + setupContractBalance(key, amount0ToTransfer, amount1ToTransfer); + + // Get balances before minting + BalanceInfo memory balanceInfo = getBalanceInfo(key); + + // Create and execute mint plan + bytes memory calls = createMintPlan(key, amount0ToTransfer, amount1ToTransfer); + uint256 tokenId = lpm.nextTokenId(); + lpm.modifyLiquidities(calls, _deadline); + + // Verify results + verifyMintResults(key, balanceInfo, tokenId); + } + + function test_mint_from_contract_balance_disallowed_revert() public { + _test_mint_from_contract_balance_disallowed_revert(keyFake0); + _test_mint_from_contract_balance_disallowed_revert(keyFake1); + _test_mint_from_contract_balance_disallowed_revert(keyFake2); + } + + function _test_mint_from_contract_balance_disallowed_revert(PoolKey memory key) internal { + uint256 amount0ToTransfer = 1e18; + uint256 amount1ToTransfer = 1e18; + + Currency currency0_ = getPermissionedCurrency(key.currency0); + Currency currency1_ = getPermissionedCurrency(key.currency1); + + uint256 balance0Before = currency0_.balanceOf(address(secondaryPosm)); + uint256 balance1Before = currency1_.balanceOf(address(secondaryPosm)); + + // Transfer tokens to the contract + currency0_.transfer(address(secondaryPosm), amount0ToTransfer); + currency1_.transfer(address(secondaryPosm), amount1ToTransfer); + + // Calculate liquidity for the desired amounts + int24 tickLower = -int24(key.tickSpacing); + int24 tickUpper = int24(key.tickSpacing); + uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + amount0ToTransfer, + amount1ToTransfer + ); + + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + + // Verify the contract has the tokens + assertEq(currency0_.balanceOf(address(secondaryPosm)), amount0ToTransfer + balance0Before); + assertEq(currency1_.balanceOf(address(secondaryPosm)), amount1ToTransfer + balance1Before); + + // Create a plan that uses the contract's balance instead of the caller's + Plan memory planner = Planner.init(); + planner.add( + Actions.MINT_POSITION, + abi.encode( + config.poolKey, + config.tickLower, + config.tickUpper, + liquidityToAdd, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + address(this), // recipient + ZERO_BYTES // hookData + ) + ); + + // Add actions to settle from the contract's balance + planner.add(Actions.SETTLE, abi.encode(key.currency0, ActionConstants.OPEN_DELTA, false)); // false = payer is contract + planner.add(Actions.SETTLE, abi.encode(key.currency1, ActionConstants.OPEN_DELTA, false)); // false = payer is contract + + bytes memory calls = planner.finalizeModifyLiquidityWithClose(config.poolKey); + + vm.prank(unauthorizedUser); + vm.expectRevert(); + secondaryPosm.modifyLiquidities(calls, _deadline); + } +} diff --git a/test/hooks/permissionedPools/PermissionedV4Router.t.sol b/test/hooks/permissionedPools/PermissionedV4Router.t.sol new file mode 100644 index 000000000..bda977c45 --- /dev/null +++ b/test/hooks/permissionedPools/PermissionedV4Router.t.sol @@ -0,0 +1,1453 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {IV4Router} from "../../../src/interfaces/IV4Router.sol"; +import {PermissionedRoutingTestHelpers} from "./shared/PermissionedRoutingTestHelpers.sol"; +import {Planner} from "../../shared/Planner.sol"; +import {Actions} from "../../../src/libraries/Actions.sol"; +import {ActionConstants} from "../../../src/libraries/ActionConstants.sol"; +import {MockPermissionedToken} from "./PermissionedPoolsBase.sol"; + +contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { + // To allow testing without importing PermissionedV4Router + error HookNotImplemented(); + + Currency public wrappedCurrency0; + Currency public wrappedCurrency1; + + // Test Users + address public alice = makeAddr("ALICE"); + address public unauthorizedUser = makeAddr("UNAUTHORIZED"); + + function setUp() public { + setupPermissionedRouterCurrenciesAndPoolsWithLiquidity(alice); + + wrappedCurrency0 = Currency.wrap(address(wrappedToken0)); + wrappedCurrency1 = Currency.wrap(address(wrappedToken1)); + + plan = Planner.init(); + + setupPermissionsAndApprovals(); + } + + function setupPermissionsAndApprovals() internal { + // Setup approvals for test address + _setupApprovals(); + + // Setup approvals for authorized user + vm.startPrank(alice); + _setupApprovals(); + vm.stopPrank(); + } + + function _setupApprovals() internal { + IERC20(Currency.unwrap(currency0)).approve(address(permit2), type(uint160).max); + IERC20(Currency.unwrap(currency1)).approve(address(permit2), type(uint160).max); + IERC20(Currency.unwrap(currency0)).approve(address(permissionedRouter), type(uint160).max); + IERC20(Currency.unwrap(currency1)).approve(address(permissionedRouter), type(uint160).max); + IERC20(Currency.unwrap(currency0)).approve(address(permissionedHooks), type(uint160).max); + IERC20(Currency.unwrap(currency1)).approve(address(permissionedHooks), type(uint160).max); + IERC20(Currency.unwrap(currency0)).approve(address(positionManager), type(uint160).max); + IERC20(Currency.unwrap(currency1)).approve(address(positionManager), type(uint160).max); + + permit2.approve(Currency.unwrap(currency0), address(permissionedRouter), type(uint160).max, 2 ** 47); + permit2.approve(Currency.unwrap(currency1), address(permissionedRouter), type(uint160).max, 2 ** 47); + permit2.approve(Currency.unwrap(currency0), address(permissionedHooks), type(uint160).max, 2 ** 47); + permit2.approve(Currency.unwrap(currency1), address(permissionedHooks), type(uint160).max, 2 ** 47); + permit2.approve(Currency.unwrap(currency0), address(positionManager), type(uint160).max, 2 ** 47); + permit2.approve(Currency.unwrap(currency1), address(positionManager), type(uint160).max, 2 ** 47); + } + + function getInputAndOutputBalances(PoolKey memory poolKey, bool zeroForOne, address manager_) + public + view + returns (uint256 inputBalance, uint256 outputBalance, uint256 ethBalance) + { + if (zeroForOne) { + inputBalance = poolKey.currency0.balanceOf(manager_); + outputBalance = poolKey.currency1.balanceOf(manager_); + } else { + inputBalance = poolKey.currency1.balanceOf(manager_); + outputBalance = poolKey.currency0.balanceOf(manager_); + } + ethBalance = address(this).balance; + } + + function getInputAndOutputBalancesPath(Currency[] memory path, address manager_) + public + view + returns (uint256 inputBalance, uint256 outputBalance, uint256 ethBalance) + { + inputBalance = path[0].balanceOf(manager_); + outputBalance = path[path.length - 1].balanceOf(manager_); + ethBalance = address(this).balance; + } + + function test_gas_swapExactInputSingle_permissionedTokens() public { + uint256 amountIn = 1000; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + permissionedRouter.execute(data); + vm.snapshotGasLastCall("PermissionedV4Router_ExactInputSingle_PermissionedTokens"); + } + + /*////////////////////////////////////////////////////////////// + PERMISSION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_swap_reverts_unauthorized_user() public { + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, true); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, true); + IERC20(Currency.unwrap(currency0)).transfer(unauthorizedUser, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(unauthorizedUser, 2 ether); + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, false); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, false); + + uint256 amountIn = 1 ether; + PoolKey memory wrappedKey = PoolKey(currency0, currency1, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency1, ActionConstants.MSG_SENDER); + + vm.prank(unauthorizedUser); + vm.expectRevert(); + permissionedRouter.execute(data); + } + + function test_swap_succeeds_authorized_user() public { + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + vm.prank(alice); + permissionedRouter.execute(data); + } + + function test_swap_authorized_router() public { + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + uint256 expectedAmountOut = 98; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalances(wrappedKey, true, address(manager)); + vm.prank(alice); + permissionedRouter.execute(data); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalances(wrappedKey, true, address(manager)); + + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + function test_swap_unauthorized_router_reverts() public { + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + wrappedToken0.updateAllowedWrapper(address(permissionedRouter), false); + wrappedToken1.updateAllowedWrapper(address(permissionedRouter), false); + + vm.prank(alice); + vm.expectRevert(); + permissionedRouter.execute(data); + } + + function test_swap_succeeds_authorized_user_mixed_entry() public { + uint256 amountIn = 100; + uint256 expectedAmountOut = 98; + bool zeroToOne = wrappedCurrency1 == key1.currency0; + Currency inputCurrency; + Currency outputCurrency; + + if (zeroToOne) { + inputCurrency = key1.currency0; + outputCurrency = key1.currency1; + getPermissionedCurrency(key1.currency0).transfer(alice, 2 ether); + } else { + inputCurrency = key1.currency1; + outputCurrency = key1.currency0; + getPermissionedCurrency(key1.currency1).transfer(alice, 2 ether); + } + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key1, zeroToOne, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, ActionConstants.MSG_SENDER); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalances(key1, zeroToOne, address(manager)); + vm.prank(alice); + permissionedRouter.execute(data); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalances(key1, zeroToOne, address(manager)); + + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + function test_swap_succeeds_authorized_user_mixed_exit() public { + IERC20(Currency.unwrap(currency2)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + uint256 expectedAmountOut = 98; + bool zeroToOne = !(wrappedCurrency1 == key1.currency0); + Currency inputCurrency; + Currency outputCurrency; + + if (zeroToOne) { + inputCurrency = key1.currency0; + outputCurrency = key1.currency1; + getPermissionedCurrency(key1.currency0).transfer(alice, 2 ether); + } else { + inputCurrency = key1.currency1; + outputCurrency = key1.currency0; + getPermissionedCurrency(key1.currency1).transfer(alice, 2 ether); + } + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key1, zeroToOne, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, ActionConstants.MSG_SENDER); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalances(key1, zeroToOne, address(manager)); + vm.prank(alice); + permissionedRouter.execute(data); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalances(key1, zeroToOne, address(manager)); + + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + function test_swap_succeeds_unauthorized_user_mixed_entry() public { + bool zeroToOne = wrappedCurrency1 == key1.currency0; + Currency inputCurrency; + Currency outputCurrency; + + if (zeroToOne) { + inputCurrency = key1.currency0; + outputCurrency = key1.currency1; + getPermissionedCurrency(key1.currency0).transfer(alice, 2 ether); + } else { + inputCurrency = key1.currency1; + outputCurrency = key1.currency0; + getPermissionedCurrency(key1.currency1).transfer(alice, 2 ether); + } + + // Give unauthorized user the permissioned currency + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, true); + IERC20(Currency.unwrap(currency1)).transfer(unauthorizedUser, 2 ether); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, false); + + uint256 amountIn = 100; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key1, zeroToOne, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, ActionConstants.MSG_SENDER); + + vm.prank(unauthorizedUser); + vm.expectRevert(); + permissionedRouter.execute(data); + } + + function test_swap_succeeds_unauthorized_user_mixed_exit() public { + IERC20(Currency.unwrap(currency2)).transfer(unauthorizedUser, 2 ether); + + uint256 amountIn = 100; + bool zeroToOne = !(wrappedCurrency1 == key1.currency0); + Currency inputCurrency; + Currency outputCurrency; + + if (zeroToOne) { + inputCurrency = key1.currency0; + outputCurrency = key1.currency1; + getPermissionedCurrency(key1.currency0).transfer(alice, 2 ether); + } else { + inputCurrency = key1.currency1; + outputCurrency = key1.currency0; + getPermissionedCurrency(key1.currency1).transfer(alice, 2 ether); + } + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key1, zeroToOne, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, ActionConstants.MSG_SENDER); + + vm.prank(unauthorizedUser); + vm.expectRevert(); + permissionedRouter.execute(data); + } + + /*////////////////////////////////////////////////////////////// + ERC20 -> ERC20 EXACT INPUT + //////////////////////////////////////////////////////////////*/ + + function test_swapExactInputSingle_revertsForAmountOut() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 19992; + // min amount out of 1 higher than the actual amount out + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), uint128(expectedAmountOut + 1), bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, ActionConstants.MSG_SENDER); + + vm.expectRevert( + abi.encodeWithSelector(IV4Router.V4TooLittleReceived.selector, expectedAmountOut + 1, expectedAmountOut) + ); + permissionedRouter.execute(data); + } + + function test_swapExactInputSingle_zeroForOne_takeToMsgSender() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 19992; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalances(key0, true, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(key0.currency0, key0.currency1, amountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalances(key0, true, address(manager)); + + assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + function test_swapExactInputSingle_zeroForOne_takeToRecipient() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 19992; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + + uint256 aliceOutputBalanceBefore = getPermissionedCurrency(key0.currency1).balanceOf(alice); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalances(key0, true, address(manager)); + // swap with alice as the take recipient + _finalizeAndExecuteSwap(key0.currency0, key0.currency1, amountIn, alice); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalances(key0, true, address(manager)); + + uint256 aliceOutputBalanceAfter = getPermissionedCurrency(key0.currency1).balanceOf(alice); + + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + assertEq(aliceOutputBalanceAfter - aliceOutputBalanceBefore, expectedAmountOut); + } + + // This is not a real use-case in isolation, but will be used in the UniversalRouter if a v4 + // swap is before another swap on v2/v3 + function test_swapExactInputSingle_zeroForOne_takeAllToRouter() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 19992; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + + // the router holds no funds before + assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalances(key0, true, address(manager)); + // swap with the router as the take recipient + _finalizeAndExecuteSwap(key0.currency0, key0.currency1, amountIn, ActionConstants.ADDRESS_THIS); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalances(key0, true, address(manager)); + + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + // This is not a real use-case in isolation, but will be used in the UniversalRouter if a v4 + // swap is before another swap on v2/v3 + function test_swapExactInputSingle_zeroForOne_takeToRouter() public { + uint256 amountIn = 1000; + uint256 expectedAmountOut = 949; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); + + Currency inputCurrency = getPermissionedCurrency(key0.currency0); + Currency outputCurrency = getPermissionedCurrency(key0.currency1); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, expectedAmountOut * 12 / 10)); + // take the entire open delta to the router's address + plan = + plan.add(Actions.TAKE, abi.encode(key0.currency1, ActionConstants.ADDRESS_THIS, ActionConstants.OPEN_DELTA)); + bytes memory data = plan.encode(); + // the router holds no funds before + assertEq(inputCurrency.balanceOf(address(permissionedRouter)), 0); + assertEq(outputCurrency.balanceOf(address(permissionedRouter)), 0); + + permissionedRouter.execute(data); + // the output tokens have been left in the router + assertEq(outputCurrency.balanceOf(address(permissionedRouter)), expectedAmountOut); + assertEq(inputCurrency.balanceOf(address(permissionedRouter)), 0); + } + + function test_swapExactInputSingle_oneForZero() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 19992; + bool zeroForOne = false; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, zeroForOne, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalances(key0, zeroForOne, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(key0.currency1, key0.currency0, amountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalances(key0, zeroForOne, address(manager)); + + assertEq(key0.currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(key0.currency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + function test_swapExactInput_revertsForAmountOut() public { + uint256 amountIn = 1000; + uint256 expectedAmountOut = 949; + + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + + IV4Router.ExactInputParams memory params = + _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks), expectedAmountOut); + params.amountOutMinimum = uint128(expectedAmountOut + 1); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, ActionConstants.MSG_SENDER); + + vm.expectRevert( + abi.encodeWithSelector(IV4Router.V4TooLittleReceived.selector, expectedAmountOut + 1, expectedAmountOut) + ); + permissionedRouter.execute(data); + } + + function test_swapExactIn_1Hop_zeroForOne() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 19992; + + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + + IV4Router.ExactInputParams memory params = + _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks), expectedAmountOut); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(wrappedCurrency0, wrappedCurrency1, amountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + function test_swapExactIn_1Hop_oneForZero() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 19992; + + tokenPath.push(wrappedCurrency1); + tokenPath.push(wrappedCurrency0); + + IV4Router.ExactInputParams memory params = + _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks), expectedAmountOut); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(wrappedCurrency1, wrappedCurrency0, amountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + function test_swapExactIn_2Hops() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 9982; + + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + tokenPath.push(currency2); + + IV4Router.ExactInputParams memory params = + _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks), expectedAmountOut); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + uint256 intermediateBalanceBefore = currency1.balanceOfSelf(); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(wrappedCurrency0, currency2, amountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + // check intermediate token balances + assertEq(intermediateBalanceBefore, currency1.balanceOfSelf()); + assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(currency2.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + function test_swapExactIn_3Hops() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 6645; + + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + tokenPath.push(currency2); + tokenPath.push(currency3); + + IV4Router.ExactInputParams memory params = + _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks), expectedAmountOut); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(wrappedCurrency0, currency3, amountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + // check intermediate tokens werent left in the router + assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(currency2.balanceOf(address(permissionedRouter)), 0); + assertEq(currency3.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + /*////////////////////////////////////////////////////////////// + ETH -> ERC20 and ERC20 -> ETH EXACT INPUT + //////////////////////////////////////////////////////////////*/ + + function test_nativeIn_swapExactInputSingle() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 19992; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(nativeKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + + (, uint256 outputBalanceBefore, uint256 ethBalanceBefore) = + getInputAndOutputBalances(nativeKey, true, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(nativeKey.currency0, nativeKey.currency1, amountIn); + (, uint256 outputBalanceAfter, uint256 ethBalanceAfter) = + getInputAndOutputBalances(nativeKey, true, address(manager)); + + assertEq(nativeKey.currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(nativeKey.currency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); + assertEq(ethBalanceBefore - ethBalanceAfter, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + function test_nativeOut_swapExactInputSingle() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 19992; + bool zeroForOne = false; + + // native output means we need !zeroForOne + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(nativeKey, zeroForOne, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + + (uint256 inputBalanceBefore,, uint256 ethBalanceBefore) = + getInputAndOutputBalances(nativeKey, zeroForOne, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(nativeKey.currency1, nativeKey.currency0, amountIn); + (uint256 inputBalanceAfter,, uint256 ethBalanceAfter) = + getInputAndOutputBalances(nativeKey, zeroForOne, address(manager)); + + assertEq(nativeKey.currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(nativeKey.currency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); + assertEq(ethBalanceAfter - ethBalanceBefore, expectedAmountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + } + + function test_nativeIn_swapExactIn_1Hop() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 19992; + + tokenPath.push(CurrencyLibrary.ADDRESS_ZERO); + tokenPath.push(nativeKey.currency1); + + IV4Router.ExactInputParams memory params = + _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks)); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + (, uint256 outputBalanceBefore, uint256 ethBalanceBefore) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(CurrencyLibrary.ADDRESS_ZERO, nativeKey.currency1, amountIn); + (, uint256 outputBalanceAfter, uint256 ethBalanceAfter) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + assertEq(nativeKey.currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(nativeKey.currency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); + assertEq(ethBalanceBefore - ethBalanceAfter, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + function test_nativeOut_swapExactIn_1Hop() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 19992; + + tokenPath.push(nativeKey.currency1); + tokenPath.push(CurrencyLibrary.ADDRESS_ZERO); + + IV4Router.ExactInputParams memory params = + _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks)); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + (uint256 inputBalanceBefore,, uint256 ethBalanceBefore) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(nativeKey.currency1, CurrencyLibrary.ADDRESS_ZERO, amountIn); + (uint256 inputBalanceAfter,, uint256 ethBalanceAfter) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + assertEq(nativeKey.currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(nativeKey.currency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(ethBalanceAfter - ethBalanceBefore, expectedAmountOut); + } + + function test_nativeIn_swapExactIn_2Hops() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 9982; + + // the initialized nativeKey is (native, currency0) + tokenPath.push(CurrencyLibrary.ADDRESS_ZERO); + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + + IV4Router.ExactInputParams memory params = + _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks)); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + uint256 intermediateBalanceBefore = currency0.balanceOfSelf(); + (, uint256 outputBalanceBefore, uint256 ethBalanceBefore) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(CurrencyLibrary.ADDRESS_ZERO, wrappedCurrency1, amountIn); + (, uint256 outputBalanceAfter, uint256 ethBalanceAfter) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + // check intermediate token balances + assertEq(intermediateBalanceBefore, currency0.balanceOfSelf()); + assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); + assertEq(nativeKey.currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(nativeKey.currency1.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(ethBalanceBefore - ethBalanceAfter, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + function test_nativeOut_swapExactIn_2Hops() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 9982; + + // the initialized nativeKey is (native, currency0) + tokenPath.push(wrappedCurrency1); + tokenPath.push(wrappedCurrency0); + tokenPath.push(CurrencyLibrary.ADDRESS_ZERO); + + IV4Router.ExactInputParams memory params = + _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks)); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + uint256 intermediateBalanceBefore = currency0.balanceOfSelf(); + + (uint256 inputBalanceBefore,, uint256 ethBalanceBefore) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(wrappedCurrency1, CurrencyLibrary.ADDRESS_ZERO, amountIn); + (uint256 inputBalanceAfter,, uint256 ethBalanceAfter) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + // check intermediate token balances + assertEq(intermediateBalanceBefore, currency0.balanceOfSelf()); + assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); + assertEq(nativeKey.currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(nativeKey.currency1.balanceOf(address(permissionedRouter)), 0); + assertEq(currency1.balanceOf(address(permissionedRouter)), 0); + assertEq(ethBalanceAfter - ethBalanceBefore, expectedAmountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + } + + function test_swap_nativeIn_settleRouterBalance_swapOpenDelta() public { + uint256 amountIn = 1000; + uint256 expectedAmountOut = 949; + bool zeroForOne = true; + + vm.deal(address(permissionedRouter), amountIn); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(nativeKey, zeroForOne, ActionConstants.OPEN_DELTA, 0, bytes("")); + + plan = plan.add(Actions.SETTLE, abi.encode(nativeKey.currency0, ActionConstants.CONTRACT_BALANCE, false)); + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(nativeKey.currency1, MIN_TAKE_AMOUNT)); + + bytes memory data = plan.encode(); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 ethBalanceBefore) = + getInputAndOutputBalances(nativeKey, zeroForOne, address(manager)); + permissionedRouter.execute(data); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter, uint256 ethBalanceAfter) = + getInputAndOutputBalances(nativeKey, zeroForOne, address(manager)); + + // caller didnt pay, router paid, caller received the output + assertEq(ethBalanceBefore, ethBalanceAfter); + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); + } + + /*////////////////////////////////////////////////////////////// + ERC20 -> ERC20 EXACT OUTPUT + //////////////////////////////////////////////////////////////*/ + + function test_swapExactOutputSingle_revertsForAmountIn() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 369070324193623892281288; + + IV4Router.ExactOutputSingleParams memory params = + IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn - 1), bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, ActionConstants.MSG_SENDER); + + vm.expectRevert( + abi.encodeWithSelector( + IV4Router.V4TooMuchRequested.selector, expectedAmountIn - 1, 369070324193623892281288 + ) + ); + permissionedRouter.execute(data); + } + + function test_swapExactOutputSingle_zeroForOne() public { + uint256 amountOut = 19992; + uint256 expectedAmountIn = 434604409; + bool zeroForOne = true; + + IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( + key0, zeroForOne, uint128(amountOut), uint128(expectedAmountIn + 1), bytes("") + ); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalances(key0, zeroForOne, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(key0.currency0, key0.currency1, expectedAmountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalances(key0, zeroForOne, address(manager)); + + assertEq(key0.currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(key0.currency1.balanceOf(address(permissionedRouter)), 0); + assertEq(currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(currency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); + assertEq(outputBalanceBefore - outputBalanceAfter, amountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, expectedAmountIn); + } + + function test_swapExactOutputSingle_oneForZero() public { + uint256 amountOut = 19992; + uint256 expectedAmountIn = 433302557; + bool zeroForOne = false; + + IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( + key0, zeroForOne, uint128(amountOut), uint128(expectedAmountIn + 1), bytes("") + ); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalances(key0, zeroForOne, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(key0.currency1, key0.currency0, expectedAmountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalances(key0, zeroForOne, address(manager)); + + assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); + assertEq(outputBalanceBefore - outputBalanceAfter, amountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, expectedAmountIn); + } + + function test_swapExactOut_revertsForAmountIn() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 369070324262815812743748; + + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + + IV4Router.ExactOutputParams memory params = + _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); + params.amountInMaximum = uint128(expectedAmountIn - 1); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, ActionConstants.MSG_SENDER); + + vm.expectRevert( + abi.encodeWithSelector(IV4Router.V4TooMuchRequested.selector, expectedAmountIn - 1, expectedAmountIn) + ); + permissionedRouter.execute(data); + } + + function test_swapExactOut_1Hop_zeroForOne() public { + uint256 amountOut = 19992; + uint256 expectedAmountIn = 434604409; + + tokenPath.push(wrappedCurrency1); + tokenPath.push(wrappedCurrency0); + + IV4Router.ExactOutputParams memory params = + _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(key0.currency0, key0.currency1, expectedAmountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); + assertEq(outputBalanceBefore - outputBalanceAfter, amountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, expectedAmountIn); + } + + function test_swapExactOut_1Hop_oneForZero() public { + uint256 amountOut = 19992; + uint256 expectedAmountIn = 433302557; + + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + + IV4Router.ExactOutputParams memory params = + _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(wrappedCurrency0, key0.currency0, expectedAmountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); + assertEq(outputBalanceBefore - outputBalanceAfter, amountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, expectedAmountIn); + } + + function test_swapExactOut_2Hops() public { + uint256 amountOut = 100; + uint256 expectedAmountIn = 104; + + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + tokenPath.push(currency2); + + IV4Router.ExactOutputParams memory params = + _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); + + uint256 intermediateBalanceBefore = currency1.balanceOfSelf(); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(wrappedCurrency0, currency2, expectedAmountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + assertEq(intermediateBalanceBefore, currency1.balanceOfSelf()); + assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(currency2.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); + assertEq(outputBalanceBefore - outputBalanceAfter, amountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, expectedAmountIn); + } + + function test_swapExactOut_3Hops() public { + uint256 amountOut = 100; + uint256 expectedAmountIn = 106; + + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + tokenPath.push(currency2); + tokenPath.push(currency3); + + IV4Router.ExactOutputParams memory params = + _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(key0.currency1, currency3, expectedAmountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(currency2.balanceOf(address(permissionedRouter)), 0); + assertEq(currency3.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); + assertEq(outputBalanceBefore - outputBalanceAfter, amountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, expectedAmountIn); + } + + function test_swapExactOut_3Hops_permissioned_middle() public { + uint256 amountOut = 100; + uint256 expectedAmountIn = 106; + + tokenPath.push(currency4); + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + tokenPath.push(currency2); + + IV4Router.ExactOutputParams memory params = + _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(currency4, currency2, expectedAmountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + assertEq(currency4.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(currency2.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); + assertEq(outputBalanceBefore - outputBalanceAfter, amountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, expectedAmountIn); + } + + function test_swapExactOut_3Hops_mmiddle_unauthorized_reverts() public { + uint256 amountOut = 100; + uint256 expectedAmountIn = 106; + + currency4.transfer(unauthorizedUser, 1000); + + tokenPath.push(currency4); + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + tokenPath.push(currency2); + + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, false); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, false); + + IV4Router.ExactOutputParams memory params = + _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + + bytes memory data = plan.finalizeSwap(currency4, currency2, ActionConstants.MSG_SENDER); + + vm.prank(unauthorizedUser); + vm.expectRevert(); + permissionedRouter.execute{value: 0}(data); + } + + function test_swapExactOut_native_3Hops_permissioned_middle() public { + uint256 amountOut = 100; + uint256 expectedAmountIn = 106; + + tokenPath.push(nativeKey.currency0); + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + tokenPath.push(currency2); + + IV4Router.ExactOutputParams memory params = + _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(nativeKey.currency0, currency2, expectedAmountIn); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + assertEq(nativeKey.currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); + assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(currency2.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); + assertEq(outputBalanceBefore - outputBalanceAfter, amountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, expectedAmountIn); + } + + function test_swapExactOut_native_3Hops_mmiddle_unauthorized_reverts() public { + nativeKey.currency0.transfer(unauthorizedUser, 1000); + + uint256 amountOut = 100; + uint256 expectedAmountIn = 106; + + tokenPath.push(nativeKey.currency0); + tokenPath.push(wrappedCurrency0); + tokenPath.push(wrappedCurrency1); + tokenPath.push(currency2); + + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, false); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, false); + + IV4Router.ExactOutputParams memory params = + _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + + bytes memory data = plan.finalizeSwap(nativeKey.currency0, currency2, ActionConstants.MSG_SENDER); + + vm.prank(unauthorizedUser); + vm.expectRevert(); + permissionedRouter.execute{value: (nativeKey.currency0.isAddressZero()) ? expectedAmountIn : 0}(data); + } + + function test_swapExactOutputSingle_swapOpenDelta() public { + uint256 expectedAmountIn = 946; + uint256 expectedOutput = 900; + + IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( + key0, true, ActionConstants.OPEN_DELTA, uint128(expectedAmountIn), bytes("") + ); + + plan = plan.add(Actions.TAKE, abi.encode(key0.currency1, ActionConstants.ADDRESS_THIS, expectedOutput)); + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + plan = plan.add(Actions.SETTLE, abi.encode(key0.currency0, ActionConstants.OPEN_DELTA, true)); + + bytes memory data = plan.encode(); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalances(key0, true, address(manager)); + permissionedRouter.execute(data); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalances(key0, true, address(manager)); + + assertEq(inputBalanceAfter - inputBalanceBefore, expectedAmountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedOutput); + } + + function test_swapExactOut_swapOpenDelta() public { + uint256 expectedAmountIn = 1057; + uint256 expectedOutput = 1000; + + tokenPath.push(wrappedCurrency1); + tokenPath.push(wrappedCurrency0); + + IV4Router.ExactOutputParams memory params = _getExactOutputParamsWithHook( + tokenPath, ActionConstants.OPEN_DELTA, address(permissionedHooks), expectedAmountIn + ); + + plan = plan.add(Actions.TAKE, abi.encode(key0.currency1, ActionConstants.ADDRESS_THIS, expectedOutput)); + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + plan = plan.add(Actions.SETTLE, abi.encode(key0.currency0, ActionConstants.OPEN_DELTA, true)); + + bytes memory data = plan.encode(); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + permissionedRouter.execute(data); + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + assertEq(inputBalanceAfter - inputBalanceBefore, expectedAmountIn); + assertEq(outputBalanceBefore - outputBalanceAfter, expectedOutput); + } + + /*////////////////////////////////////////////////////////////// + ETH -> ERC20 and ERC20 -> ETH EXACT OUTPUT + //////////////////////////////////////////////////////////////*/ + + function test_nativeOut_swapExactOutputSingle() public { + uint256 amountOut = 19992; + uint256 expectedAmountIn = 433302557; + bool zeroForOne = false; + + IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( + nativeKey, zeroForOne, uint128(amountOut), uint128(expectedAmountIn + 1), bytes("") + ); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + + (uint256 inputBalanceBefore,, uint256 ethBalanceBefore) = + getInputAndOutputBalances(nativeKey, zeroForOne, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(nativeKey.currency1, nativeKey.currency0, expectedAmountIn); + (uint256 inputBalanceAfter,, uint256 ethBalanceAfter) = + getInputAndOutputBalances(nativeKey, zeroForOne, address(manager)); + + assertEq(nativeKey.currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(nativeKey.currency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); + assertEq(ethBalanceAfter - ethBalanceBefore, amountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, expectedAmountIn); + } + + function test_nativeOut_swapExactOut_1Hop() public { + uint256 amountOut = 19992; + uint256 expectedAmountIn = 433302557; + + tokenPath.push(nativeKey.currency1); + tokenPath.push(CurrencyLibrary.ADDRESS_ZERO); + + IV4Router.ExactOutputParams memory params = + _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + (uint256 inputBalanceBefore,, uint256 ethBalanceBefore) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + ( + uint256 userInputBalanceBefore, + uint256 userOutputBalanceBefore, + uint256 userInputBalanceAfter, + uint256 userOutputBalanceAfter + ) = _finalizeAndExecuteSwap(nativeKey.currency1, CurrencyLibrary.ADDRESS_ZERO, expectedAmountIn); + (uint256 inputBalanceAfter,, uint256 ethBalanceAfter) = + getInputAndOutputBalancesPath(tokenPath, address(manager)); + + assertEq(nativeKey.currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(nativeKey.currency1.balanceOf(address(permissionedRouter)), 0); + assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); + assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); + assertEq(ethBalanceAfter - ethBalanceBefore, amountOut); + assertEq(inputBalanceAfter - inputBalanceBefore, expectedAmountIn); + } + + /*////////////////////////////////////////////////////////////// + ROUTER SELF-PAYMENT COVERAGE TEST + //////////////////////////////////////////////////////////////*/ + + function test_router_self_payment() public { + uint256 amountIn = 100000; + + IERC20(Currency.unwrap(key2.currency0)).transfer(address(permissionedRouter), amountIn); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key2, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SETTLE, abi.encode(key2.currency0, ActionConstants.CONTRACT_BALANCE, false)); + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(key2.currency1, 0)); + + bytes memory data = plan.encode(); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalances(key2, true, address(manager)); + + // Execute the swap - this should trigger the _pay function with payer == address(this) and permissionedToken == address(0) + permissionedRouter.execute(data); + + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalances(key2, true, address(manager)); + + // Verify the router's balance was used (it should be 0 after the swap) + assertEq(key2.currency0.balanceOf(address(permissionedRouter)), 0); + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertGt(outputBalanceBefore - outputBalanceAfter, 0); + } + + function test_router_self_payment_permissioned_token() public { + uint256 amountIn = 100000; + + IERC20(Currency.unwrap(getPermissionedCurrency(key0.currency0))).transfer(address(permissionedRouter), amountIn); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SETTLE, abi.encode((key0.currency0), ActionConstants.CONTRACT_BALANCE, false)); + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + plan = plan.add(Actions.TAKE_ALL, abi.encode((key0.currency1), 0)); + + bytes memory data = plan.encode(); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = + getInputAndOutputBalances(key0, true, address(manager)); + + // Execute the swap - this should trigger the _pay function with payer == address(this) and permissionedToken != address(0) + permissionedRouter.execute(data); + + (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = + getInputAndOutputBalances(key0, true, address(manager)); + + // Verify the router's balance was used (it should be 0 after the swap) + assertEq(getPermissionedCurrency(key0.currency0).balanceOf(address(permissionedRouter)), 0); + assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); + assertGt(outputBalanceBefore - outputBalanceAfter, 0); + } + + function test_router_self_payment_permissioned_token_unauthorized_reverts() public { + uint256 amountIn = 100000; + + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(key0.currency0))).setAllowlist( + unauthorizedUser, true + ); + IERC20(Currency.unwrap(getPermissionedCurrency(key0.currency0))).transfer(address(unauthorizedUser), amountIn); + + vm.prank(unauthorizedUser); + IERC20(Currency.unwrap(getPermissionedCurrency(key0.currency0))).transfer(address(permissionedRouter), amountIn); + + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(key0.currency0))).setAllowlist( + unauthorizedUser, false + ); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SETTLE, abi.encode((key0.currency0), ActionConstants.CONTRACT_BALANCE, false)); + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + plan = plan.add(Actions.TAKE_ALL, abi.encode((key0.currency1), 0)); + + bytes memory data = plan.encode(); + + vm.prank(unauthorizedUser); + vm.expectRevert(); + permissionedRouter.execute(data); + } + + function test_hooks() public { + SwapParams memory swapParams = SwapParams({zeroForOne: true, amountSpecified: 100000, sqrtPriceLimitX96: 0}); + ModifyLiquidityParams memory modifyLiquidityParams = + ModifyLiquidityParams({tickLower: 0, tickUpper: 0, liquidityDelta: 0, salt: bytes32(0)}); + BalanceDelta balanceDelta = BalanceDelta.wrap(0); + + // Use manager to avoid NotPoolManager errors for hooks with OnlyPoolManager modifier + vm.startPrank(address(manager)); + vm.expectRevert(HookNotImplemented.selector); + permissionedHooks.afterSwap(address(this), key0, swapParams, balanceDelta, bytes("")); + vm.expectRevert(HookNotImplemented.selector); + permissionedHooks.beforeInitialize(address(this), key0, 0); + vm.expectRevert(HookNotImplemented.selector); + permissionedHooks.afterInitialize(address(this), key0, 0, 0); + vm.expectRevert(HookNotImplemented.selector); + permissionedHooks.beforeRemoveLiquidity(address(this), key0, modifyLiquidityParams, bytes("")); + vm.expectRevert(HookNotImplemented.selector); + permissionedHooks.afterRemoveLiquidity( + address(this), key0, modifyLiquidityParams, balanceDelta, balanceDelta, bytes("") + ); + vm.expectRevert(HookNotImplemented.selector); + permissionedHooks.afterAddLiquidity( + address(this), key0, modifyLiquidityParams, balanceDelta, balanceDelta, bytes("") + ); + vm.expectRevert(HookNotImplemented.selector); + permissionedHooks.beforeDonate(address(this), key0, 0, 0, bytes("")); + vm.expectRevert(HookNotImplemented.selector); + permissionedHooks.afterDonate(address(this), key0, 0, 0, bytes("")); + } + + receive() external payable {} + fallback() external {} +} diff --git a/test/hooks/permissionedPools/mocks/MockHooks.sol b/test/hooks/permissionedPools/mocks/MockHooks.sol new file mode 100644 index 000000000..0c8be65f3 --- /dev/null +++ b/test/hooks/permissionedPools/mocks/MockHooks.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IWrappedPermissionedTokenFactory} from + "../../../../src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol"; +import {PermissionedHooks} from "../../../../src/hooks/permissionedPools/PermissionedHooks.sol"; + +contract MockHooks is PermissionedHooks { + constructor(IPoolManager manager, IWrappedPermissionedTokenFactory wrappedTokenFactory) + PermissionedHooks(manager, wrappedTokenFactory) + {} +} + +/// @notice This contract is used in the testing of security for the permissioned pool manager +contract MockInsecureHooks { + function beforeAddLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata) + external + pure + returns (bytes4) + { + return IHooks.beforeAddLiquidity.selector; + } + + function beforeSwap(address, PoolKey calldata, SwapParams calldata, bytes calldata) + external + pure + returns (bytes4) + { + return IHooks.beforeSwap.selector; + } + + receive() external payable {} +} diff --git a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol new file mode 100644 index 000000000..5a97c9753 --- /dev/null +++ b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "forge-std/Test.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol"; +import {SortTokens} from "@uniswap/v4-core/test/utils/SortTokens.sol"; +import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; +import {PoolModifyLiquidityTestNoChecks} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTestNoChecks.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {SwapRouterNoChecks} from "@uniswap/v4-core/src/test/SwapRouterNoChecks.sol"; +import {PoolDonateTest} from "@uniswap/v4-core/src/test/PoolDonateTest.sol"; +import {PoolNestedActionsTest} from "@uniswap/v4-core/src/test/PoolNestedActionsTest.sol"; +import {PoolTakeTest} from "@uniswap/v4-core/src/test/PoolTakeTest.sol"; +import {PoolClaimsTest} from "@uniswap/v4-core/src/test/PoolClaimsTest.sol"; +import {ActionsRouter} from "@uniswap/v4-core/src/test/ActionsRouter.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; + +import {Planner} from "../../../shared/Planner.sol"; +import {Plan} from "../../../shared/Planner.sol"; +import {IV4Router} from "../../../../src/interfaces/IV4Router.sol"; +import {Actions} from "../../../../src/libraries/Actions.sol"; +import {ActionConstants} from "../../../../src/libraries/ActionConstants.sol"; +import {IWrappedPermissionedTokenFactory} from + "../../../../src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol"; +import {PermissionedV4Router} from "../../../../src/hooks/permissionedPools/PermissionedV4Router.sol"; +import {MockPermissionedToken} from "../PermissionedPoolsBase.sol"; +import {MockV4Router} from "../../../mocks/MockV4Router.sol"; +import {MockHooks} from "../mocks/MockHooks.sol"; +import {Deploy} from "../../../../test/shared/Deploy.sol"; +import {HookMiner} from "../../../../src/utils/HookMiner.sol"; +import {IWETH9} from "../../../../src/interfaces/external/IWETH9.sol"; + +/// @notice A contract that provides permissioned deployment functionality for tests +/// This moves the deployFreshManagerAndRoutersPermissioned function from v4-core to the test folder +contract PermissionedDeployers is Test { + using LPFeeLibrary for uint24; + using StateLibrary for IPoolManager; + + // Helpful test constants + bytes public constant ZERO_BYTES = Constants.ZERO_BYTES; + uint160 public constant SQRT_PRICE_1_1 = Constants.SQRT_PRICE_1_1; + uint160 public constant SQRT_PRICE_1_2 = Constants.SQRT_PRICE_1_2; + uint160 public constant SQRT_PRICE_2_1 = Constants.SQRT_PRICE_2_1; + uint160 public constant SQRT_PRICE_1_4 = Constants.SQRT_PRICE_1_4; + uint160 public constant SQRT_PRICE_4_1 = Constants.SQRT_PRICE_4_1; + + uint160 public constant MIN_PRICE_LIMIT = TickMath.MIN_SQRT_PRICE + 1; + uint160 public constant MAX_PRICE_LIMIT = TickMath.MAX_SQRT_PRICE - 1; + + ModifyLiquidityParams public LIQUIDITY_PARAMS = + ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: 1e18, salt: 0}); + ModifyLiquidityParams public REMOVE_LIQUIDITY_PARAMS = + ModifyLiquidityParams({tickLower: -120, tickUpper: 120, liquidityDelta: -1e18, salt: 0}); + SwapParams public SWAP_PARAMS = + SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_PRICE_1_2}); + + // Global variables + Currency internal currency0; + Currency internal currency1; + + IPoolManager public manager; + PoolModifyLiquidityTest public modifyLiquidityRouter; + PoolModifyLiquidityTestNoChecks public modifyLiquidityNoChecks; + SwapRouterNoChecks public swapRouterNoChecks; + PermissionedV4Router public permissionedSwapRouter; + PoolSwapTest public swapRouter; + PoolDonateTest public donateRouter; + PoolTakeTest public takeRouter; + ActionsRouter public actionsRouter; + IHooks public permissionedHooks; + IHooks public secondaryPermissionedHooks; + IWrappedPermissionedTokenFactory public wrappedTokenFactory; + + PoolClaimsTest public claimsRouter; + PoolNestedActionsTest public nestedActionRouter; + + address public feeController; + + PoolKey public nativeKey; + PoolKey public uninitializedKey; + PoolKey public uninitializedNativeKey; + + modifier noIsolate() { + if (msg.sender != address(this)) { + (bool success,) = address(this).call(msg.data); + require(success); + } else { + _; + } + } + + function deployFreshManager() internal virtual { + manager = new PoolManager(address(this)); + } + + function deployFreshManagerAndRouters() internal { + deployFreshManager(); + + swapRouter = new PoolSwapTest(manager); + deployMiscRouters(); + } + + function deployMiscRouters() internal { + swapRouterNoChecks = new SwapRouterNoChecks(manager); + modifyLiquidityRouter = new PoolModifyLiquidityTest(manager); + modifyLiquidityNoChecks = new PoolModifyLiquidityTestNoChecks(manager); + donateRouter = new PoolDonateTest(manager); + takeRouter = new PoolTakeTest(manager); + claimsRouter = new PoolClaimsTest(manager); + nestedActionRouter = new PoolNestedActionsTest(manager); + feeController = makeAddr("feeController"); + actionsRouter = new ActionsRouter(manager); + manager.setProtocolFeeController(feeController); + } + + function deployPermissionedHooks(address manager_, address wrappedTokenFactory_) + internal + returns (address deployedHooksAddr) + { + uint160 flags = (1 << 11) | (1 << 7); + (address calculatedAddr, bytes32 salt) = HookMiner.find( + address(this), + flags, + vm.getCode("MockHooks.sol:MockHooks"), + abi.encode(IPoolManager(manager_), IWrappedPermissionedTokenFactory(wrappedTokenFactory_)) + ); + address addr = Deploy.create2( + abi.encodePacked( + vm.getCode("MockHooks.sol:MockHooks"), + abi.encode(IPoolManager(manager_), IWrappedPermissionedTokenFactory(wrappedTokenFactory_)) + ), + salt + ); + assertEq(addr, calculatedAddr); + deployedHooksAddr = calculatedAddr; + } + + function deployPermissionedV4Router(address permit2_, address wrappedTokenFactory_, address weth9) + internal + returns (address deployedAddr) + { + bytes memory routerBytecode = abi.encodePacked( + vm.getCode("PermissionedV4Router.sol:PermissionedV4Router"), + abi.encode( + manager, + IAllowanceTransfer(permit2_), + IWrappedPermissionedTokenFactory(wrappedTokenFactory_), + IWETH9(address(weth9)) + ) + ); + + deployedAddr = Deploy.create2(routerBytecode, keccak256("permissionedSwapRouter")); + } + + function deployFreshManagerAndRoutersPermissioned(address permit2_, address weth9) internal { + deployFreshManager(); + + address wrappedTokenFactoryAddress = Deploy.create2( + abi.encodePacked( + vm.getCode("WrappedPermissionedTokenFactory.sol:WrappedPermissionedTokenFactory"), + abi.encode(address(manager)) + ), + keccak256("wrappedTokenFactory") + ); + + wrappedTokenFactory = IWrappedPermissionedTokenFactory(wrappedTokenFactoryAddress); + permissionedHooks = IHooks(deployPermissionedHooks(address(manager), wrappedTokenFactoryAddress)); + secondaryPermissionedHooks = IHooks(deployPermissionedHooks(address(manager), wrappedTokenFactoryAddress)); + address deployedAddr = deployPermissionedV4Router(permit2_, wrappedTokenFactoryAddress, weth9); + permissionedSwapRouter = PermissionedV4Router(payable(deployedAddr)); + swapRouter = PoolSwapTest(deployedAddr); + deployMiscRouters(); + } + + /// @dev You must have first initialised the routers with deployFreshManagerAndRouters + /// If you only need the currencies (and not approvals) call deployAndMint2Currencies + function deployMintAndApprove2Currencies(bool isPermissioned0, bool isPermissioned1) + internal + returns (Currency, Currency) + { + while (true) { + Currency _currency0; + Currency _currency1; + if (isPermissioned0) { + _currency0 = deployMintAndApproveCurrency(true); + } else { + _currency0 = deployMintAndApproveCurrency(false); + } + if (isPermissioned1) { + _currency1 = deployMintAndApproveCurrency(true); + } else { + _currency1 = deployMintAndApproveCurrency(false); + } + + (currency0, currency1) = + SortTokens.sort(MockERC20(Currency.unwrap(_currency0)), MockERC20(Currency.unwrap(_currency1))); + if (currency0 == _currency0 && currency1 == _currency1) { + break; + } + } + return (currency0, currency1); + } + + function deployMintAndApproveCurrency(bool isPermissioned) internal returns (Currency currency) { + MockERC20 token = deployTokens(1, 2 ** 255, isPermissioned)[0]; + + address[9] memory toApprove = [ + address(swapRouter), + address(swapRouterNoChecks), + address(modifyLiquidityRouter), + address(modifyLiquidityNoChecks), + address(donateRouter), + address(takeRouter), + address(claimsRouter), + address(nestedActionRouter.executor()), + address(actionsRouter) + ]; + + for (uint256 i = 0; i < toApprove.length; i++) { + token.approve(toApprove[i], Constants.MAX_UINT256); + } + + return Currency.wrap(address(token)); + } + + function deployTokens(uint8 count, uint256 totalSupply, bool arePermissioned) + internal + returns (MockERC20[] memory tokens) + { + tokens = new MockERC20[](count); + for (uint8 i = 0; i < count; i++) { + if (arePermissioned) { + tokens[i] = MockERC20(address(new MockPermissionedToken())); + MockPermissionedToken(address(tokens[i])).setAllowlist(address(this), true); + tokens[i].mint(address(this), totalSupply); + } else { + tokens[i] = new MockERC20("TEST", "TEST", 18); + tokens[i].mint(address(this), totalSupply); + } + } + } + + function initPool(Currency _currency0, Currency _currency1, IHooks hooks, uint24 fee, uint160 sqrtPriceX96) + internal + returns (PoolKey memory _key, PoolId id) + { + return initPool( + _currency0, _currency1, hooks, fee, fee.isDynamicFee() ? int24(60) : int24(fee / 100 * 2), sqrtPriceX96 + ); + } + + function initPool( + Currency _currency0, + Currency _currency1, + IHooks hooks, + uint24 fee, + int24 tickSpacing, + uint160 sqrtPriceX96 + ) internal returns (PoolKey memory _key, PoolId id) { + _key = PoolKey(_currency0, _currency1, fee, tickSpacing, hooks); + id = _key.toId(); + manager.initialize(_key, sqrtPriceX96); + } + + /// @notice Helper function for a simple ERC20 swaps that allows for unlimited price impact + function swap(PoolKey memory _key, bool zeroForOne, int256 amountSpecified) internal { + // allow native input for exact-input, guide users to the `swapNativeInput` function + bool isNativeInput = zeroForOne && _key.currency0.isAddressZero(); + if (isNativeInput) require(0 > amountSpecified, "Use swapNativeInput() for native-token exact-output swaps"); + + uint256 value = isNativeInput ? uint256(-amountSpecified) : 0; + bytes memory data = getSwapData(_key, uint256(-amountSpecified), zeroForOne); + permissionedSwapRouter.execute{value: value}(data); + } + + function getSwapData(PoolKey memory poolKey, uint256 amountIn, bool zeroForOne) + internal + pure + returns (bytes memory) + { + // Initialize the plan + Plan memory plan = Planner.init(); + + // Create swap parameters + IV4Router.ExactInputSingleParams memory params = IV4Router.ExactInputSingleParams( + poolKey, // The pool to swap in + zeroForOne, // Direction of swap + uint128(amountIn), // Amount to swap in + 0, // Minimum amount out (0 = no slippage protection) + bytes("") // Hook data + ); + + // Add the swap action to the plan + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + + // Finalize the plan - this adds SETTLE and TAKE actions + bytes memory data = plan.finalizeSwap( + zeroForOne ? poolKey.currency0 : poolKey.currency1, // Input currency + zeroForOne ? poolKey.currency1 : poolKey.currency0, // Output currency + ActionConstants.MSG_SENDER // Take recipient + ); + + return data; + } +} diff --git a/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol b/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol new file mode 100644 index 000000000..6ccaf3cc5 --- /dev/null +++ b/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IERC721} from "forge-std/interfaces/IERC721.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; +import {WETH} from "solmate/src/tokens/WETH.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {HookModifyLiquidities} from "../../../shared/HookModifyLiquidities.sol"; +import {Deploy, IPositionDescriptor} from "../../../shared/Deploy.sol"; +import {ERC721PermitHash} from "../../../../src/libraries/ERC721PermitHash.sol"; +import {IWETH9} from "../../../../src/interfaces/external/IWETH9.sol"; +import {LiquidityOperations} from "../../../shared/LiquidityOperations.sol"; +import {PositionConfig} from "../../../shared/PositionConfig.sol"; +import {PermissionedDeployers} from "./PermissionedDeployers.sol"; +import {IPositionManager} from "../../../../src/interfaces/IPositionManager.sol"; +import {Planner, Plan} from "../../../shared/Planner.sol"; +import {ActionConstants} from "../../../../src/libraries/ActionConstants.sol"; +import {Actions} from "../../../../src/libraries/Actions.sol"; + +struct BalanceInfo { + uint256 balance0; + uint256 balance1; + uint256 balance0Manager; + uint256 balance1Manager; +} + +/// @notice A shared test contract that wraps the v4-core deployers contract and exposes basic liquidity operations on posm. +contract PermissionedPosmTestSetup is Test, PermissionedDeployers, DeployPermit2, LiquidityOperations { + uint256 private constant STARTING_USER_BALANCE = 10_000_000 ether; + address public constant GOVERNANCE = address(0xABCD); + + IAllowanceTransfer public permit2; + IPositionDescriptor public positionDescriptor; + TransparentUpgradeableProxy public proxy; + IPositionDescriptor public proxyAsImplementation; + HookModifyLiquidities public hookModifyLiquidities; + Currency public currency2; + IWETH9 public _WETH9; + + WETH public wethImpl = new WETH(); + + PoolKey public wethKey; + + mapping(Currency => Currency) public wrappedToPermissioned; + + function deployAndApprovePosm( + IPoolManager poolManager, + address wrappedTokenFactory, + address permissionedHooks_, + bytes32 salt + ) public { + deployPermissionedPosm(poolManager, wrappedTokenFactory, permissionedHooks_, salt); + approvePosm(); + } + + function deployPermissionedPosm( + IPoolManager poolManager, + address wrappedTokenFactory, + address permissionedHooks_, + bytes32 salt + ) internal { + permit2 = IAllowanceTransfer(deployPermit2()); + _WETH9 = deployWETH(); + proxyAsImplementation = deployDescriptor(poolManager, "ETH"); + lpm = Deploy.permissionedPositionManager( + address(poolManager), + address(permit2), + 100_000, + address(proxyAsImplementation), + address(_WETH9), + wrappedTokenFactory, + permissionedHooks_, + abi.encode(salt) + ); + } + + function deployAndApprovePosmOnly( + IPoolManager poolManager, + address wrappedTokenFactory, + address permissionedHooks_, + bytes32 salt + ) public returns (IPositionManager secondaryPosm) { + secondaryPosm = Deploy.permissionedPositionManager( + address(poolManager), + address(permit2), + 100_000, + address(proxyAsImplementation), + address(_WETH9), + wrappedTokenFactory, + permissionedHooks_, + abi.encode(salt) + ); + approvePosm(); + } + + function deployWETH() internal returns (IWETH9) { + address wethAddr = makeAddr("WETH"); + vm.etch(wethAddr, address(wethImpl).code); + return IWETH9(wethAddr); + } + + function deployDescriptor(IPoolManager poolManager_, bytes32 label) internal returns (IPositionDescriptor) { + positionDescriptor = Deploy.positionDescriptor(address(poolManager_), address(_WETH9), label, hex"00"); + proxy = Deploy.transparentUpgradeableProxy(address(positionDescriptor), GOVERNANCE, "", hex"03"); + return IPositionDescriptor(address(proxy)); + } + + function seedBalance(address to) internal { + IERC20(Currency.unwrap(currency0)).transfer(to, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(to, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency2)).transfer(to, STARTING_USER_BALANCE); + } + + function approvePosm() internal { + approvePosmCurrency(currency0); + approvePosmCurrency(currency1); + approvePosmCurrency(currency2); + } + + function approvePosmCurrency(Currency currency) internal { + // Because POSM uses permit2, we must execute 2 permits/approvals. + // 1. First, the caller must approve permit2 on the token. + IERC20(Currency.unwrap(currency)).approve(address(permit2), type(uint256).max); + // 2. Then, the caller must approve POSM as a spender of permit2. + permit2.approve(Currency.unwrap(currency), address(lpm), type(uint160).max, type(uint48).max); + } + + // Does the same approvals as approvePosm, but for a specific address. + /// @dev Should not be in a prank when calling this function + function approvePosmFor(address addr) internal { + vm.startPrank(addr); + approvePosm(); + vm.stopPrank(); + } + + function getDigest(address spender, uint256 tokenId, uint256 nonce, uint256 deadline) + internal + view + returns (bytes32 digest) + { + digest = keccak256( + abi.encodePacked( + "\x19\x01", + lpm.DOMAIN_SEPARATOR(), + keccak256(abi.encode(ERC721PermitHash.PERMIT_TYPEHASH, spender, tokenId, nonce, deadline)) + ) + ); + } + + function getPermissionedCurrency(Currency currency) internal view returns (Currency) { + Currency permissionedCurrency = wrappedToPermissioned[currency]; + if (permissionedCurrency == Currency.wrap(address(0))) { + return currency; + } + return permissionedCurrency; + } + + function setupContractBalance(PoolKey memory key, uint256 amount0ToTransfer, uint256 amount1ToTransfer) internal { + getPermissionedCurrency(key.currency0).transfer(address(lpm), amount0ToTransfer); + getPermissionedCurrency(key.currency1).transfer(address(lpm), amount1ToTransfer); + + assertEq(getPermissionedCurrency(key.currency0).balanceOf(address(lpm)), amount0ToTransfer); + assertEq(getPermissionedCurrency(key.currency1).balanceOf(address(lpm)), amount1ToTransfer); + } + + /// @dev This function is used to avoid stack-too-deep errors + function getBalanceInfoSelfAndManager(PoolKey memory key) internal view returns (BalanceInfo memory) { + return BalanceInfo({ + balance0: getPermissionedCurrency(key.currency0).balanceOfSelf(), + balance1: getPermissionedCurrency(key.currency1).balanceOfSelf(), + balance0Manager: key.currency0.balanceOf(address(manager)), + balance1Manager: key.currency1.balanceOf(address(manager)) + }); + } + + /// @dev This function is used to avoid stack-too-deep errors + function getBalanceInfo(PoolKey memory key) internal view returns (BalanceInfo memory) { + return BalanceInfo({ + balance0: getPermissionedCurrency(key.currency0).balanceOf(address(lpm)), + balance1: getPermissionedCurrency(key.currency1).balanceOf(address(lpm)), + balance0Manager: key.currency0.balanceOf(address(manager)), + balance1Manager: key.currency1.balanceOf(address(manager)) + }); + } + + function createMintPlan(PoolKey memory key, uint256 amount0ToTransfer, uint256 amount1ToTransfer) + internal + view + returns (bytes memory) + { + int24 tickLower = -int24(key.tickSpacing); + int24 tickUpper = int24(key.tickSpacing); + uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + amount0ToTransfer, + amount1ToTransfer + ); + + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + + Plan memory planner = Planner.init(); + planner.add( + Actions.MINT_POSITION, + abi.encode( + config.poolKey, + config.tickLower, + config.tickUpper, + liquidityToAdd, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + address(this), // recipient + ZERO_BYTES // hookData + ) + ); + + planner.add(Actions.SETTLE, abi.encode(key.currency0, ActionConstants.OPEN_DELTA, false)); + planner.add(Actions.SETTLE, abi.encode(key.currency1, ActionConstants.OPEN_DELTA, false)); + + return planner.finalizeModifyLiquidityWithClose(config.poolKey); + } + + function verifyMintResults(PoolKey memory key, BalanceInfo memory balanceInfoBefore, uint256 tokenId) + internal + view + { + uint256 balance0After = getPermissionedCurrency(key.currency0).balanceOf(address(lpm)); + uint256 balance1After = getPermissionedCurrency(key.currency1).balanceOf(address(lpm)); + uint256 balance0ManagerAfter = key.currency0.balanceOf(address(manager)); + uint256 balance1ManagerAfter = key.currency1.balanceOf(address(manager)); + uint256 liquidity = lpm.getPositionLiquidity(tokenId); + + assertEq(tokenId, lpm.nextTokenId() - 1); + assertEq(IERC721(address(lpm)).ownerOf(tokenId), address(this)); + assertGt(balanceInfoBefore.balance0, balance0After); + assertGt(balanceInfoBefore.balance1, balance1After); + assertEq(balanceInfoBefore.balance0 - balance0After, balance0ManagerAfter - balanceInfoBefore.balance0Manager); + assertEq(balanceInfoBefore.balance1 - balance1After, balance1ManagerAfter - balanceInfoBefore.balance1Manager); + assertEq( + liquidity, + LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(-int24(key.tickSpacing)), + TickMath.getSqrtPriceAtTick(int24(key.tickSpacing)), + 100e18, + 100e18 + ) + ); + } +} diff --git a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol new file mode 100644 index 000000000..e1022e78f --- /dev/null +++ b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; +import {WETH} from "solmate/src/tokens/WETH.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; + +import {Deploy} from "test/shared/Deploy.sol"; +import {PermissionedV4Router} from "../../../../src/hooks/permissionedPools/PermissionedV4Router.sol"; +import {Plan, Planner} from "../../../shared/Planner.sol"; +import {PathKey} from "../../../../src/libraries/PathKey.sol"; +import {Actions} from "../../../../src/libraries/Actions.sol"; +import {WrappedPermissionedToken} from "../../../../src/hooks/permissionedPools/WrappedPermissionedToken.sol"; +import {MockAllowlistChecker, MockPermissionedToken} from "../PermissionedPoolsBase.sol"; +import {IAllowlistChecker} from "../../../../src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol"; +import {IPositionManager} from "../../../../src/interfaces/IPositionManager.sol"; +import {IWrappedPermissionedTokenFactory} from + "../../../../src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol"; +import {IWETH9} from "../../../../src/interfaces/external/IWETH9.sol"; +import {IPositionDescriptor} from "../../../../src/interfaces/IPositionDescriptor.sol"; +import {IV4Router} from "../../../../src/interfaces/IV4Router.sol"; +import {ActionConstants} from "../../../../src/libraries/ActionConstants.sol"; +import {PermissionedDeployers} from "./PermissionedDeployers.sol"; + +/// @notice A shared test contract that wraps the v4-core deployers contract and exposes basic helpers for swapping with the permissioned router. +contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 { + uint256 public constant MAX_SETTLE_AMOUNT = type(uint256).max; + uint256 public constant MIN_TAKE_AMOUNT = 0; + + // Permissioned components + PermissionedV4Router public permissionedRouter; + IAllowanceTransfer public permit2; + MockAllowlistChecker public mockAllowlistChecker; + IAllowlistChecker public allowListChecker; + IPositionDescriptor public tokenDescriptor; + IWETH9 public weth9; + + // nativeKey is already defined in Deployers.sol + PoolKey public key0; + PoolKey public key1; + PoolKey public key2; + PoolKey public key3; + + // currency0 and currency1 are defined in Deployers.sol + Currency public currency2; + Currency public currency3; + Currency public currency4; + + WrappedPermissionedToken public wrappedToken0; + WrappedPermissionedToken public wrappedToken1; + + Currency[] public tokenPath; + Plan public plan; + + address public positionManager; + + mapping(Currency wrappedCurrency => Currency permissionedCurrency) public wrappedToPermissioned; + + function setupPermissionedRouterCurrenciesAndPoolsWithLiquidity(address spender) public { + _deployFreshManager(); + _deployWETH(); + _deployPositionDescriptor(); + _deployPermit2(); + _deployWrappedTokenFactory(); + _deployPermissionedHooks(); + _deployMockPermissionedRouter(); + _deployPositionManager(); + MockERC20[] memory tokens = _deployTokensMintAndApprove(5, 2); + _deployAndSetupTokens(tokens); + _setupPermissionedTokens(spender); + _setupApprovals(tokens, spender); + _createPoolsWithLiquidity(); + } + + function approveAllCurrencies(MockERC20[] memory currencies) internal { + for (uint256 i = 0; i < currencies.length; i++) { + approveAllContracts(address(currencies[i])); + } + } + + function approveAllContracts(address token) internal { + approveBoth(token, address(permissionedHooks)); + approveBoth(token, address(permissionedRouter)); + approveBoth(token, address(positionManager)); + approveBoth(token, address(manager)); + approveBoth(token, address(permit2)); + } + + function approveBoth(address token, address approved) internal { + permit2.approve(token, address(approved), type(uint160).max, 2 ** 47); + IERC20(token).approve(address(approved), type(uint256).max); + } + + function getPermissionedCurrency(Currency currency) public view returns (Currency) { + Currency permissionedCurrency = wrappedToPermissioned[currency]; + if (permissionedCurrency == Currency.wrap(address(0))) { + return currency; + } + return permissionedCurrency; + } + + function setupPermissionedTokens(address spender) internal { + _setupMockAllowList(currency0, spender); + _setupMockAllowList(currency1, spender); + while (true) { + wrappedToken0 = WrappedPermissionedToken( + wrappedTokenFactory.createWrappedPermissionedToken( + IERC20(Currency.unwrap(currency0)), address(this), mockAllowlistChecker + ) + ); + wrappedToken1 = WrappedPermissionedToken( + wrappedTokenFactory.createWrappedPermissionedToken( + IERC20(Currency.unwrap(currency1)), address(this), mockAllowlistChecker + ) + ); + if (address(wrappedToken0) > address(wrappedToken1)) { + break; + } + } + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(address(wrappedToken0), true); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(address(wrappedToken1), true); + + // Transfer some underlying tokens to the wrapped tokens for verification + IERC20(Currency.unwrap(currency0)).transfer(address(wrappedToken0), 1); + IERC20(Currency.unwrap(currency1)).transfer(address(wrappedToken1), 1); + + verifyTokensAndAddWrappers(); + + wrappedToPermissioned[Currency.wrap(address(wrappedToken0))] = currency0; + wrappedToPermissioned[Currency.wrap(address(wrappedToken1))] = currency1; + } + + function verifyTokensAndAddWrappers() private { + wrappedTokenFactory.verifyWrappedToken(address(wrappedToken0)); + wrappedTokenFactory.verifyWrappedToken(address(wrappedToken1)); + + wrappedToken0.updateAllowedWrapper(address(this), true); + wrappedToken1.updateAllowedWrapper(address(this), true); + wrappedToken0.updateAllowedWrapper(positionManager, true); + wrappedToken1.updateAllowedWrapper(positionManager, true); + wrappedToken0.updateAllowedWrapper(address(permissionedRouter), true); + wrappedToken1.updateAllowedWrapper(address(permissionedRouter), true); + wrappedToken0.updateAllowedWrapper(address(permissionedHooks), true); + wrappedToken1.updateAllowedWrapper(address(permissionedHooks), true); + } + + function createPoolWithLiquidity(Currency currencyA, Currency currencyB, address hookAddr) + internal + returns (PoolKey memory _key) + { + if (Currency.unwrap(currencyA) > Currency.unwrap(currencyB)) (currencyA, currencyB) = (currencyB, currencyA); + _key = PoolKey(currencyA, currencyB, 3000, 60, IHooks(hookAddr)); + manager.initialize(_key, SQRT_PRICE_1_1); + MockERC20(Currency.unwrap(currencyA)).approve(positionManager, type(uint256).max); + MockERC20(Currency.unwrap(currencyB)).approve(positionManager, type(uint256).max); + modifyLiquidity(_key, ModifyLiquidityParams(-887220, 887220, 200 ether, 0), "0x", 0); + } + + function createNativePoolWithLiquidity(Currency currency, address hookAddr) + internal + returns (PoolKey memory _key) + { + _key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, currency, 3000, 60, IHooks(hookAddr)); + manager.initialize(_key, SQRT_PRICE_1_1); + MockERC20(Currency.unwrap(currency)).approve(positionManager, type(uint256).max); + modifyLiquidity(_key, ModifyLiquidityParams(-887220, 887220, 200 ether, 0), "0x", 200 ether); + } + + function _getExactInputParams(Currency[] memory _tokenPath, uint256 amountIn) + internal + pure + returns (IV4Router.ExactInputParams memory params) + { + PathKey[] memory path = new PathKey[](_tokenPath.length - 1); + for (uint256 i = 0; i < _tokenPath.length - 1; i++) { + path[i] = PathKey(_tokenPath[i + 1], 3000, 60, IHooks(address(0)), bytes("")); + } + + params.currencyIn = _tokenPath[0]; + params.path = path; + params.amountIn = uint128(amountIn); + params.amountOutMinimum = 0; + } + + function _getExactInputParamsWithHook(Currency[] memory _tokenPath, uint256 amountIn, address hookAddr) + internal + pure + returns (IV4Router.ExactInputParams memory params) + { + return _getExactInputParamsWithHook(_tokenPath, amountIn, hookAddr, 0); + } + + function _getExactInputParamsWithHook( + Currency[] memory _tokenPath, + uint256 amountIn, + address hookAddr, + uint256 amountOutMinimum + ) internal pure returns (IV4Router.ExactInputParams memory params) { + PathKey[] memory path = new PathKey[](_tokenPath.length - 1); + for (uint256 i = 0; i < _tokenPath.length - 1; i++) { + path[i] = PathKey(_tokenPath[i + 1], 3000, 60, IHooks(hookAddr), bytes("")); + } + + params.currencyIn = _tokenPath[0]; + params.path = path; + params.amountIn = uint128(amountIn); + params.amountOutMinimum = uint128(amountOutMinimum); + } + + function _getExactOutputParams(Currency[] memory _tokenPath, uint256 amountOut) + internal + pure + returns (IV4Router.ExactOutputParams memory params) + { + return _getExactOutputParamsWithHook(_tokenPath, amountOut, address(0), type(uint128).max); + } + + function _getExactOutputParamsWithHook( + Currency[] memory _tokenPath, + uint256 amountOut, + address hookAddr, + uint256 amountInMaximum + ) internal pure returns (IV4Router.ExactOutputParams memory params) { + PathKey[] memory path = new PathKey[](_tokenPath.length - 1); + for (uint256 i = _tokenPath.length - 1; i > 0; i--) { + path[i - 1] = PathKey(_tokenPath[i - 1], 3000, 60, IHooks(hookAddr), bytes("")); + } + + params.currencyOut = _tokenPath[_tokenPath.length - 1]; + params.path = path; + params.amountOut = uint128(amountOut); + params.amountInMaximum = uint128(amountInMaximum); + } + + function modifyLiquidity( + PoolKey memory key, + ModifyLiquidityParams memory params, + bytes memory hookData, + uint256 value + ) public { + // Create a plan with the mint position action + plan = Planner.init(); + plan = plan.add( + Actions.MINT_POSITION, + abi.encode(key, params.tickLower, params.tickUpper, 20000, 100000, 100000, msg.sender, hookData) + ); + + // Finalize the plan with proper settlement + bytes memory unlockData = plan.finalizeModifyLiquidityWithSettlePair(key); + uint256 deadline = block.timestamp + 3600; + + // Call modifyLiquidities with the properly encoded data + IPositionManager(positionManager).modifyLiquidities{value: value}(unlockData, deadline); + } + + function _finalizeAndExecuteSwap( + Currency inputCurrency, + Currency outputCurrency, + uint256 amountIn, + address takeRecipient + ) + internal + returns ( + uint256 inputBalanceBefore, + uint256 outputBalanceBefore, + uint256 inputBalanceAfter, + uint256 outputBalanceAfter + ) + { + inputBalanceBefore = getPermissionedCurrency(inputCurrency).balanceOfSelf(); + outputBalanceBefore = getPermissionedCurrency(outputCurrency).balanceOfSelf(); + + bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, takeRecipient); + + uint256 value = (inputCurrency.isAddressZero()) ? amountIn : 0; + + permissionedRouter.execute{value: value}(data); + + inputBalanceAfter = getPermissionedCurrency(inputCurrency).balanceOfSelf(); + outputBalanceAfter = getPermissionedCurrency(outputCurrency).balanceOfSelf(); + } + + function _finalizeAndExecuteSwap(Currency inputCurrency, Currency outputCurrency, uint256 amountIn) + internal + returns ( + uint256 inputBalanceBefore, + uint256 outputBalanceBefore, + uint256 inputBalanceAfter, + uint256 outputBalanceAfter + ) + { + return _finalizeAndExecuteSwap(inputCurrency, outputCurrency, amountIn, ActionConstants.MSG_SENDER); + } + + // ===== PRIVATE HELPER FUNCTIONS ===== + + function _deployFreshManager() private { + deployFreshManager(); + } + + function _deployWETH() private { + WETH wethImpl = new WETH(); + address wethAddr = makeAddr("WETH"); + vm.etch(wethAddr, address(wethImpl).code); + weth9 = IWETH9(wethAddr); + } + + function _deployPositionDescriptor() private { + tokenDescriptor = Deploy.positionDescriptor(address(manager), address(weth9), "ETH", hex"00"); + } + + function _deployPermit2() private { + permit2 = IAllowanceTransfer(deployPermit2()); + } + + function _deployWrappedTokenFactory() private { + bytes memory wrappedTokenFactoryBytecode = abi.encodePacked( + vm.getCode("WrappedPermissionedTokenFactory.sol:WrappedPermissionedTokenFactory"), + abi.encode(address(manager)) + ); + wrappedTokenFactory = IWrappedPermissionedTokenFactory( + Deploy.create2(wrappedTokenFactoryBytecode, keccak256("wrappedTokenFactory")) + ); + } + + function _deployPermissionedHooks() private { + permissionedHooks = IHooks(deployPermissionedHooks(address(manager), address(wrappedTokenFactory))); + } + + function _deployMockPermissionedRouter() private { + bytes memory routerBytecode = abi.encodePacked( + vm.getCode("PermissionedV4Router.sol:PermissionedV4Router"), + abi.encode(manager, permit2, wrappedTokenFactory, weth9) + ); + permissionedRouter = + PermissionedV4Router(payable(Deploy.create2(routerBytecode, keccak256("permissionedSwapRouter")))); + } + + function _deployPositionManager() private { + bytes memory posmBytecode = abi.encodePacked( + vm.getCode("PermissionedPositionManager.sol:PermissionedPositionManager"), + abi.encode( + manager, + permit2, + 1e18, + address(tokenDescriptor), + address(weth9), + wrappedTokenFactory, + address(permissionedHooks) + ) + ); + positionManager = Deploy.create2(posmBytecode, keccak256("permissionedPosm")); + } + + function _deployAndSetupTokens(MockERC20[] memory tokens) private { + currency0 = Currency.wrap(address(tokens[0])); + currency1 = Currency.wrap(address(tokens[1])); + currency2 = Currency.wrap(address(tokens[2])); + currency3 = Currency.wrap(address(tokens[3])); + currency4 = Currency.wrap(address(tokens[4])); + } + + function _setupPermissionedTokens(address spender) private { + setupPermissionedTokens(spender); + } + + function _setupApprovals(MockERC20[] memory tokens, address alice) private { + approveAllCurrencies(tokens); + vm.startPrank(alice); + approveAllCurrencies(tokens); + vm.stopPrank(); + } + + function _createPoolsWithLiquidity() private { + nativeKey = createNativePoolWithLiquidity(Currency.wrap(address(wrappedToken0)), address(permissionedHooks)); + key0 = createPoolWithLiquidity( + Currency.wrap(address(wrappedToken0)), Currency.wrap(address(wrappedToken1)), address(permissionedHooks) + ); + key1 = createPoolWithLiquidity(Currency.wrap(address(wrappedToken1)), currency2, address(permissionedHooks)); + key2 = createPoolWithLiquidity(currency2, currency3, address(permissionedHooks)); + key3 = createPoolWithLiquidity(Currency.wrap(address(wrappedToken0)), currency4, address(permissionedHooks)); + } + + function _setupMockAllowList(Currency currency, address spender) private { + MockPermissionedToken mockPermissionedToken = MockPermissionedToken(Currency.unwrap(currency)); + mockAllowlistChecker = new MockAllowlistChecker(mockPermissionedToken); + mockPermissionedToken.setAllowlist(address(this), true); + mockPermissionedToken.setAllowlist(address(permissionedRouter), true); + mockPermissionedToken.setAllowlist(address(permissionedHooks), true); + mockPermissionedToken.setAllowlist(address(positionManager), true); + mockPermissionedToken.setAllowlist(address(wrappedTokenFactory), true); + mockPermissionedToken.setAllowlist(address(manager), true); + mockPermissionedToken.setAllowlist(address(permit2), true); + mockPermissionedToken.setAllowlist(spender, true); + } + + function _deployTokensMintAndApprove(uint8 count, uint8 permissionedCount) internal returns (MockERC20[] memory) { + MockERC20[] memory permissionedTokens = deployTokens(permissionedCount, 2 ** 128, true); + MockERC20[] memory unpermissionedTokens = deployTokens(count - permissionedCount, 2 ** 128, false); + MockERC20[] memory tokens = new MockERC20[](count); + for (uint256 i = 0; i < permissionedCount; i++) { + tokens[i] = permissionedTokens[i]; + } + for (uint256 i = 0; i < count - permissionedCount; i++) { + tokens[i + permissionedCount] = unpermissionedTokens[i]; + } + for (uint256 i = 0; i < count; i++) { + tokens[i].approve(address(permissionedRouter), type(uint256).max); + tokens[i].approve(address(positionManager), type(uint256).max); + } + return tokens; + } +} diff --git a/test/shared/Deploy.sol b/test/shared/Deploy.sol index cfb9a215a..650ac990b 100644 --- a/test/shared/Deploy.sol +++ b/test/shared/Deploy.sol @@ -6,6 +6,7 @@ import {IPositionDescriptor} from "../../src/interfaces/IPositionDescriptor.sol" import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; import {IV4Quoter} from "../../src/interfaces/IV4Quoter.sol"; import {IStateView} from "../../src/interfaces/IStateView.sol"; +import {PermissionedV4Router} from "../../src/hooks/permissionedPools/PermissionedV4Router.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; library Deploy { @@ -26,6 +27,32 @@ library Deploy { } } + function permissionedPositionManager( + address poolManager, + address permit2, + uint256 unsubscribeGasLimit, + address positionDescriptor_, + address wrappedNative, + address wrappedTokenFactory, + address permissionedHooks, + bytes memory salt + ) internal returns (IPositionManager manager) { + bytes memory args = abi.encode( + poolManager, + permit2, + unsubscribeGasLimit, + positionDescriptor_, + wrappedNative, + wrappedTokenFactory, + permissionedHooks + ); + bytes memory initcode = + abi.encodePacked(vm.getCode("PermissionedPositionManager.sol:PermissionedPositionManager"), args); + assembly { + manager := create2(0, add(initcode, 0x20), mload(initcode), salt) + } + } + function stateView(address poolManager, bytes memory salt) internal returns (IStateView stateView_) { bytes memory args = abi.encode(poolManager); bytes memory initcode = abi.encodePacked(vm.getCode("StateView.sol:StateView"), args); @@ -66,4 +93,16 @@ library Deploy { descriptor := create2(0, add(initcode, 0x20), mload(initcode), salt) } } + + function create2(bytes memory initcode, bytes32 salt) internal returns (address contractAddress) { + assembly { + contractAddress := create2(0, add(initcode, 32), mload(initcode), salt) + if iszero(contractAddress) { + let ptr := mload(0x40) + let errorSize := returndatasize() + returndatacopy(ptr, 0, errorSize) + revert(ptr, errorSize) + } + } + } } From 8482d4149c8422e8897ce545ce384bbeae0ec674 Mon Sep 17 00:00:00 2001 From: ens_408 Date: Tue, 15 Jul 2025 10:49:06 -0400 Subject: [PATCH 06/33] expect specific errors (#486) --- .../PermissionedPositionManager.t.sol | 89 ++++++++++++++++--- .../PermissionedV4Router.t.sol | 80 ++++++++++++++--- 2 files changed, 143 insertions(+), 26 deletions(-) diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 70516be99..555345e27 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -13,6 +13,7 @@ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; +import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; @@ -35,6 +36,11 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq using FixedPointMathLib for uint256; using StateLibrary for IPoolManager; + // To allow testing without importing PermissionedHooks + error Unauthorized(); + error InvalidHook(); + error HookCallFailed(); + PoolId public poolId; PoolId public poolIdFake; @@ -583,19 +589,34 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); } - function test_permissioned_mint_alternate_posm_reverts() public { + function test_permissioned_mint_alt_posm_diff_hooks_reverts() public { // secondary posm uses different hooks contract - _test_permissioned_mint_alternate_posm_reverts(key0, secondaryPosm); - _test_permissioned_mint_alternate_posm_reverts(key1, secondaryPosm); - _test_permissioned_mint_alternate_posm_reverts(key2, secondaryPosm); + _test_permissioned_mint_alt_posm_diff_hooks_reverts(key0, secondaryPosm); + _test_permissioned_mint_alt_posm_diff_hooks_reverts(key1, secondaryPosm); + _test_permissioned_mint_alt_posm_diff_hooks_reverts(key2, secondaryPosm); + } + + function _test_permissioned_mint_alt_posm_diff_hooks_reverts(PoolKey memory key, IPositionManager posm) internal { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 1e18; + + // we don't use the helper, so we can choose which position manager to use + bytes memory calls = getMintEncoded(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + vm.prank(alice); + vm.expectRevert(InvalidHook.selector); + posm.modifyLiquidities(calls, block.timestamp + 1); + } + + function test_permissioned_mint_alt_posm_same_hooks_reverts() public { // tertiary posm uses the same hooks contract - _test_permissioned_mint_alternate_posm_reverts(key0, tertiaryPosm); - _test_permissioned_mint_alternate_posm_reverts(key1, tertiaryPosm); - _test_permissioned_mint_alternate_posm_reverts(key2, tertiaryPosm); + _test_permissioned_mint_alt_posm_same_hooks_reverts(key0, tertiaryPosm); + _test_permissioned_mint_alt_posm_same_hooks_reverts(key1, tertiaryPosm); + _test_permissioned_mint_alt_posm_same_hooks_reverts(key2, tertiaryPosm); } - function _test_permissioned_mint_alternate_posm_reverts(PoolKey memory key, IPositionManager posm) internal { + function _test_permissioned_mint_alt_posm_same_hooks_reverts(PoolKey memory key, IPositionManager posm) internal { PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); uint256 liquidity = 1e18; @@ -604,7 +625,15 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq bytes memory calls = getMintEncoded(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); vm.prank(alice); - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeAddLiquidity.selector, + abi.encodeWithSelector(Unauthorized.selector), + abi.encodeWithSelector(HookCallFailed.selector) + ) + ); posm.modifyLiquidities(calls, block.timestamp + 1); } @@ -634,7 +663,15 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq permit2.approve(Currency.unwrap(currency1), address(lpm), type(uint160).max, type(uint48).max); // This should revert because the user is not in the allowlist - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeAddLiquidity.selector, + abi.encodeWithSelector(Unauthorized.selector), + abi.encodeWithSelector(HookCallFailed.selector) + ) + ); mint(config, liquidity, unauthorizedUser, ZERO_BYTES); vm.stopPrank(); } @@ -692,7 +729,15 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq tokenId = lpm.nextTokenId(); // Increasing liquidity should revert because the user is no longer in the allowlist - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeAddLiquidity.selector, + abi.encodeWithSelector(Unauthorized.selector), + abi.encodeWithSelector(HookCallFailed.selector) + ) + ); mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); vm.stopPrank(); @@ -730,7 +775,15 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq permit2.approve(Currency.unwrap(currencyUnpermissioned), address(lpm), type(uint160).max, type(uint48).max); // This should revert because the user is not in the allowlist - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeAddLiquidity.selector, + abi.encodeWithSelector(Unauthorized.selector), + abi.encodeWithSelector(HookCallFailed.selector) + ) + ); mint(config, liquidity, unauthorizedUser, ZERO_BYTES); vm.stopPrank(); } @@ -800,7 +853,15 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq permit2.approve(Currency.unwrap(currencyPermissioned), address(lpm), type(uint160).max, type(uint48).max); // This should revert because the user is not in the allowlist - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeAddLiquidity.selector, + abi.encodeWithSelector(Unauthorized.selector), + abi.encodeWithSelector(HookCallFailed.selector) + ) + ); mint(config, liquidity, unauthorizedUser, ZERO_BYTES); vm.stopPrank(); } @@ -952,7 +1013,7 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - vm.expectRevert(); + vm.expectRevert(abi.encode("Transfer disabled")); IERC721(address(lpm)).transferFrom(alice, address(this), tokenId); vm.stopPrank(); } diff --git a/test/hooks/permissionedPools/PermissionedV4Router.t.sol b/test/hooks/permissionedPools/PermissionedV4Router.t.sol index bda977c45..8f4022279 100644 --- a/test/hooks/permissionedPools/PermissionedV4Router.t.sol +++ b/test/hooks/permissionedPools/PermissionedV4Router.t.sol @@ -6,6 +6,8 @@ import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/Pool import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; import {IV4Router} from "../../../src/interfaces/IV4Router.sol"; import {PermissionedRoutingTestHelpers} from "./shared/PermissionedRoutingTestHelpers.sol"; import {Planner} from "../../shared/Planner.sol"; @@ -16,6 +18,8 @@ import {MockPermissionedToken} from "./PermissionedPoolsBase.sol"; contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { // To allow testing without importing PermissionedV4Router error HookNotImplemented(); + error Unauthorized(); + error HookCallFailed(); Currency public wrappedCurrency0; Currency public wrappedCurrency1; @@ -106,7 +110,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PERMISSION TESTS //////////////////////////////////////////////////////////////*/ - function test_swap_reverts_unauthorized_user() public { + function test_swap_unauthorized_user_reverts() public { MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, true); MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, true); IERC20(Currency.unwrap(currency0)).transfer(unauthorizedUser, 2 ether); @@ -114,21 +118,33 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, false); MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, false); + Currency currencyA = wrappedCurrency0; + Currency currencyB = wrappedCurrency1; + if (Currency.unwrap(currencyA) > Currency.unwrap(currencyB)) (currencyA, currencyB) = (currencyB, currencyA); + uint256 amountIn = 1 ether; - PoolKey memory wrappedKey = PoolKey(currency0, currency1, 3000, 60, permissionedHooks); + PoolKey memory wrappedKey = PoolKey(currencyA, currencyB, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(currency0, currency1, ActionConstants.MSG_SENDER); + bytes memory data = plan.finalizeSwap(currencyA, currencyB, ActionConstants.MSG_SENDER); vm.prank(unauthorizedUser); - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeSwap.selector, + abi.encodeWithSelector(Unauthorized.selector), + abi.encodeWithSelector(HookCallFailed.selector) + ) + ); permissionedRouter.execute(data); } - function test_swap_succeeds_authorized_user() public { + function test_swap_authorized_user() public { IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); @@ -187,7 +203,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { wrappedToken1.updateAllowedWrapper(address(permissionedRouter), false); vm.prank(alice); - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeSwap.selector, + abi.encodeWithSelector(Unauthorized.selector), + abi.encodeWithSelector(HookCallFailed.selector) + ) + ); permissionedRouter.execute(data); } @@ -289,11 +313,19 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, ActionConstants.MSG_SENDER); vm.prank(unauthorizedUser); - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeSwap.selector, + abi.encodeWithSelector(Unauthorized.selector), + abi.encodeWithSelector(HookCallFailed.selector) + ) + ); permissionedRouter.execute(data); } - function test_swap_succeeds_unauthorized_user_mixed_exit() public { + function test_swap_unauthorized_user_mixed_exit_reverts() public { IERC20(Currency.unwrap(currency2)).transfer(unauthorizedUser, 2 ether); uint256 amountIn = 100; @@ -318,7 +350,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, ActionConstants.MSG_SENDER); vm.prank(unauthorizedUser); - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeSwap.selector, + abi.encodeWithSelector(Unauthorized.selector), + abi.encodeWithSelector(HookCallFailed.selector) + ) + ); permissionedRouter.execute(data); } @@ -1147,7 +1187,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.finalizeSwap(currency4, currency2, ActionConstants.MSG_SENDER); vm.prank(unauthorizedUser); - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeSwap.selector, + abi.encodeWithSelector(Unauthorized.selector), + abi.encodeWithSelector(HookCallFailed.selector) + ) + ); permissionedRouter.execute{value: 0}(data); } @@ -1208,7 +1256,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.finalizeSwap(nativeKey.currency0, currency2, ActionConstants.MSG_SENDER); vm.prank(unauthorizedUser); - vm.expectRevert(); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeSwap.selector, + abi.encodeWithSelector(Unauthorized.selector), + abi.encodeWithSelector(HookCallFailed.selector) + ) + ); permissionedRouter.execute{value: (nativeKey.currency0.isAddressZero()) ? expectedAmountIn : 0}(data); } @@ -1414,7 +1470,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.encode(); vm.prank(unauthorizedUser); - vm.expectRevert(); + vm.expectRevert(Unauthorized.selector); permissionedRouter.execute(data); } From 8affcc9fedb726a901672c9b79de78911df89d69 Mon Sep 17 00:00:00 2001 From: Daniel Gretzke Date: Wed, 16 Jul 2025 14:19:40 +0200 Subject: [PATCH 07/33] Permissioned Pools: Transfers by admins (#483) * Initial commit: forced transfers by admins * fix merge conflict * remove event --- .../PermissionedPositionManager.sol | 31 ++++-- .../WrappedPermissionedToken.sol | 4 + .../interfaces/IWrappedPermissionedToken.sol | 3 + .../PermissionedPositionManager.t.sol | 97 ++++++++++++++++--- 4 files changed, 113 insertions(+), 22 deletions(-) diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index e7dae84cc..8ad422429 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -21,6 +21,7 @@ contract PermissionedPositionManager is PositionManager { IHooks public immutable PERMISSIONED_HOOKS; error InvalidHook(); + error SafeTransferDisabled(); /// @dev as this contract must know the hooks address in advance, it must be passed in as a constructor argument constructor( @@ -36,17 +37,24 @@ contract PermissionedPositionManager is PositionManager { PERMISSIONED_HOOKS = _permissionedHooks; } - /// @dev Disables transfers of the ERC721 liquidity position tokens - function transferFrom(address, address, uint256) public pure override { - revert("Transfer disabled"); + /// @dev Only allow admins of permissioned tokens to transfer positions that contain their tokens + function transferFrom(address from, address to, uint256 id) public override onlyIfPoolManagerLocked { + (PoolKey memory poolKey,) = getPoolAndPositionInfo(id); + address admin1 = _getOwner(poolKey.currency0); + address admin2 = _getOwner(poolKey.currency1); + if (msg.sender != admin1 && msg.sender != admin2) { + revert Unauthorized(); + } + getApproved[id] = msg.sender; + super.transferFrom(from, to, id); } function safeTransferFrom(address, address, uint256) public pure override { - revert("Transfer disabled"); + revert SafeTransferDisabled(); } function safeTransferFrom(address, address, uint256, bytes calldata) public pure override { - revert("Transfer disabled"); + revert SafeTransferDisabled(); } /// @dev When minting a position, verify that the sender is allowed to mint the position. This prevents a disallowed user from minting one sided liquidity. @@ -67,7 +75,7 @@ contract PermissionedPositionManager is PositionManager { /// @dev When paying to settle, if the currency is a permissioned token, wrap the token and transfer it to the pool manager. function _pay(Currency currency, address payer, uint256 amount) internal virtual override { - address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(currency)); + address permissionedToken = _verifiedPermissionedTokenOf(currency); if (permissionedToken == address(0)) { // token is not a permissioned token, use the default implementation super._pay(currency, payer, amount); @@ -88,4 +96,15 @@ contract PermissionedPositionManager is PositionManager { wrappedPermissionedToken.wrapToPoolManager(amount); } } + + function _verifiedPermissionedTokenOf(Currency currency) internal view returns (address) { + return WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(currency)); + } + + function _getOwner(Currency currency) internal view returns (address) { + address wrappedPermissionedToken = Currency.unwrap(currency); + address permissionedToken = _verifiedPermissionedTokenOf(currency); + if (permissionedToken == address(0)) return address(0); + return IWrappedPermissionedToken(wrappedPermissionedToken).owner(); + } } diff --git a/src/hooks/permissionedPools/WrappedPermissionedToken.sol b/src/hooks/permissionedPools/WrappedPermissionedToken.sol index 4bc13bb41..8c0130b77 100644 --- a/src/hooks/permissionedPools/WrappedPermissionedToken.sol +++ b/src/hooks/permissionedPools/WrappedPermissionedToken.sol @@ -110,4 +110,8 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo function decimals() public view override returns (uint8) { return ERC20(address(PERMISSIONED_TOKEN)).decimals(); } + + function owner() public view override(Ownable, IWrappedPermissionedToken) returns (address) { + return super.owner(); + } } diff --git a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol index 7549c0460..0f889c942 100644 --- a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol +++ b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol @@ -57,4 +57,7 @@ interface IWrappedPermissionedToken is IERC20 { /// @notice Returns the permissioned token that is wrapped by this contract function PERMISSIONED_TOKEN() external view returns (IERC20); + + /// @notice Returns the admin of the wrapped permissioned token + function owner() external view returns (address); } diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 555345e27..285f4f8fe 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -53,9 +53,14 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq IPositionManager public secondaryPosm; IPositionManager public tertiaryPosm; + // permissioned / normal PoolKey public key0; + // normal / permissioned PoolKey public key1; + // permissioned / permissioned PoolKey public key2; + // normal / normal + PoolKey public key3; PoolKey public keyFake0; PoolKey public keyFake1; PoolKey public keyFake2; @@ -108,6 +113,7 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq 3000, SQRT_PRICE_1_1 ); + (key3, poolId) = initPool(currency2, currency0, permissionedHooks, 3000, SQRT_PRICE_1_1); (keyFake0, poolId) = initPool(Currency.wrap(address(wrappedToken0)), currency1, secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1); (keyFake1, poolId) = @@ -995,29 +1001,88 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq assertEq(lpFee, fee); } - function test_liquidity_token_transfer_reverts() public { - _test_liquidity_token_transfer_reverts(key0); - _test_liquidity_token_transfer_reverts(key1); - _test_liquidity_token_transfer_reverts(key2); - } + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); - function _test_liquidity_token_transfer_reverts(PoolKey memory key) internal { - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + function test_transferFrom_success_seize_by_admin() public { + _testRevert_transferFrom_success_seize_by_admin(key0); + _testRevert_transferFrom_success_seize_by_admin(key1); + } - uint256 liquidity = 1e18; + function _testRevert_transferFrom_success_seize_by_admin(PoolKey memory poolKey) internal { uint256 tokenId = lpm.nextTokenId(); + _test_permissioned_mint_allowed_user(poolKey); - vm.startPrank(alice); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, address(this), tokenId); + // address(this) is admin + IERC721(address(lpm)).transferFrom(alice, address(this), tokenId); + } - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + function test_transferFrom_success_seize_by_either_admin() public { + uint256 tokenId0 = lpm.nextTokenId(); + _test_permissioned_mint_allowed_user(key2); + uint256 tokenId1 = lpm.nextTokenId(); + _test_permissioned_mint_allowed_user(key2); + uint256 tokenId2 = lpm.nextTokenId(); + _test_permissioned_mint_allowed_user(key2); - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, address(this), tokenId0); + // address(this) is admin of both permissioned tokens + IERC721(address(lpm)).transferFrom(alice, address(this), tokenId0); + + address owner0 = makeAddr("owner0"); + address owner1 = makeAddr("owner1"); + wrappedToken0.transferOwnership(owner0); + vm.prank(owner0); + wrappedToken0.acceptOwnership(); + wrappedToken2.transferOwnership(owner1); + vm.prank(owner1); + wrappedToken2.acceptOwnership(); + + vm.prank(owner0); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, address(this), tokenId1); + // owner0 is admin of wrappedToken0 + IERC721(address(lpm)).transferFrom(alice, address(this), tokenId1); + + vm.prank(owner1); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, address(this), tokenId2); + // owner1 is admin of wrappedToken2 + IERC721(address(lpm)).transferFrom(alice, address(this), tokenId2); + } + + function test_transferFrom_revert_not_admin(address notAdmin) public { + vm.assume(notAdmin != address(this) && notAdmin != address(0)); + uint256 tokenId0 = lpm.nextTokenId(); + _test_permissioned_mint_allowed_user(key0); + uint256 tokenId1 = lpm.nextTokenId(); + _test_permissioned_mint_allowed_user(key1); + uint256 tokenId2 = lpm.nextTokenId(); + _test_permissioned_mint_allowed_user(key2); + uint256 tokenId3 = lpm.nextTokenId(); + _test_permissioned_mint_allowed_user(key2); - vm.expectRevert(abi.encode("Transfer disabled")); - IERC721(address(lpm)).transferFrom(alice, address(this), tokenId); - vm.stopPrank(); + vm.prank(notAdmin); + vm.expectRevert(Unauthorized.selector); + IERC721(address(lpm)).transferFrom(alice, address(this), tokenId0); + + vm.prank(notAdmin); + vm.expectRevert(Unauthorized.selector); + IERC721(address(lpm)).transferFrom(alice, address(this), tokenId1); + + vm.prank(notAdmin); + vm.expectRevert(Unauthorized.selector); + IERC721(address(lpm)).transferFrom(alice, address(this), tokenId2); + + vm.prank(notAdmin); + vm.expectRevert(Unauthorized.selector); + IERC721(address(lpm)).transferFrom(alice, address(this), tokenId3); } + error SafeTransferDisabled(); + function test_safe_transfer_from_reverts() public { bytes memory encodedCall = abi.encodeWithSignature("safeTransferFrom(address,address,uint256)", alice, address(this), 1); @@ -1025,7 +1090,7 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq address target = address(lpm); vm.startPrank(alice); - vm.expectRevert(abi.encode("Transfer disabled")); + vm.expectRevert(SafeTransferDisabled.selector); bool success; assembly ("memory-safe") { @@ -1040,7 +1105,7 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq address target = address(lpm); vm.startPrank(alice); - vm.expectRevert(abi.encode("Transfer disabled")); + vm.expectRevert(SafeTransferDisabled.selector); bool success; assembly ("memory-safe") { From a8ae2052a481e7e20c5d398300be1945bcdfa3ca Mon Sep 17 00:00:00 2001 From: ens_408 Date: Wed, 16 Jul 2025 09:47:54 -0400 Subject: [PATCH 08/33] add granular allow list and multiple hooks and fix revert tests (#484) * Initial commit: forced transfers by admins * add granular allow list and multiple hooks and fix revert tests * add coverage for receive * permissioned v4 router test spacing nits * change allowed hook check * allowedHooks on pposm * nit * add inheritdoc to transferFrom * fix merge conflict * make setAllowedHook external * fix test resulting for resolving conflict * simplify checkAllowedHooks * fix snapshot --------- Co-authored-by: gretzke --- snapshots/PermissionedV4RouterTest.json | 2 +- .../BaseAllowListChecker.sol | 4 +- .../permissionedPools/PermissionedHooks.sol | 35 +- .../PermissionedPositionManager.sol | 37 +- .../PermissionedV4Router.sol | 11 +- .../WrappedPermissionedToken.sol | 7 +- .../interfaces/IAllowlistChecker.sol | 3 +- .../interfaces/IWrappedPermissionedToken.sol | 5 +- .../libraries/PermissionFlags.sol | 31 ++ .../PermissionedPoolsBase.sol | 18 +- .../PermissionedPositionManager.t.sol | 279 +++++++++++++-- .../PermissionedV4Router.t.sol | 336 +++++++++++++++++- .../WrappedPermissionedToken.t.sol | 9 +- .../WrappedPermissionedTokenFactory.t.sol | 5 +- .../shared/PermissionedDeployers.sol | 3 +- .../shared/PermissionedPosmTestSetup.sol | 29 +- .../shared/PermissionedRoutingTestHelpers.sol | 46 ++- test/shared/Deploy.sol | 9 +- 18 files changed, 720 insertions(+), 149 deletions(-) create mode 100644 src/hooks/permissionedPools/libraries/PermissionFlags.sol diff --git a/snapshots/PermissionedV4RouterTest.json b/snapshots/PermissionedV4RouterTest.json index ecfe63058..4d6e178ed 100644 --- a/snapshots/PermissionedV4RouterTest.json +++ b/snapshots/PermissionedV4RouterTest.json @@ -1,3 +1,3 @@ { - "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "223119" + "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "223575" } \ No newline at end of file diff --git a/src/hooks/permissionedPools/BaseAllowListChecker.sol b/src/hooks/permissionedPools/BaseAllowListChecker.sol index 9e19364e7..642d2fdc0 100644 --- a/src/hooks/permissionedPools/BaseAllowListChecker.sol +++ b/src/hooks/permissionedPools/BaseAllowListChecker.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IAllowlistChecker, IERC165} from "./interfaces/IAllowlistChecker.sol"; +import {IAllowlistChecker, PermissionFlag, IERC165} from "./interfaces/IAllowlistChecker.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; abstract contract BaseAllowlistChecker is IAllowlistChecker, ERC165 { - function checkAllowlist(address account) public view virtual returns (bool); + function checkAllowlist(address account) public view virtual returns (PermissionFlag); function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { return interfaceId == type(IAllowlistChecker).interfaceId || super.supportsInterface(interfaceId); diff --git a/src/hooks/permissionedPools/PermissionedHooks.sol b/src/hooks/permissionedPools/PermissionedHooks.sol index 1736737b8..7c4fbbfac 100644 --- a/src/hooks/permissionedPools/PermissionedHooks.sol +++ b/src/hooks/permissionedPools/PermissionedHooks.sol @@ -7,15 +7,13 @@ import { IWrappedPermissionedToken } from "./interfaces/IWrappedPermissionedTokenFactory.sol"; import {IMsgSender} from "../../interfaces/IMsgSender.sol"; -import {ActionConstants} from "../../libraries/ActionConstants.sol"; import {ReentrancyLock} from "../../base/ReentrancyLock.sol"; -import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; import {Hooks, IHooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {BaseHook} from "../../utils/BaseHook.sol"; +import {PermissionFlags, PermissionFlag} from "./libraries/PermissionFlags.sol"; contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; @@ -38,10 +36,11 @@ contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { internal view override - returns (bytes4, BeforeSwapDelta, uint24) + returns (bytes4 selector, BeforeSwapDelta, uint24) { - _verifyAllowlist(IMsgSender(sender), key); - return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + selector = IHooks.beforeSwap.selector; + _verifyAllowlist(IMsgSender(sender), key, selector); + return (selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } /// @dev Does not need to verify msg.sender address directly, as verifying the allowlist is sufficient due to the fact that any valid senders are allowed wrappers @@ -49,24 +48,32 @@ contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { internal view override - returns (bytes4) + returns (bytes4 selector) { - _verifyAllowlist(IMsgSender(sender), key); - return IHooks.beforeAddLiquidity.selector; + selector = IHooks.beforeAddLiquidity.selector; + _verifyAllowlist(IMsgSender(sender), key, selector); } /// @dev checks if the sender is allowed to access both tokens in the pool - function _verifyAllowlist(IMsgSender sender, PoolKey calldata poolKey) internal view { - _isAllowed(Currency.unwrap(poolKey.currency0), sender.msgSender(), address(sender)); - _isAllowed(Currency.unwrap(poolKey.currency1), sender.msgSender(), address(sender)); + function _verifyAllowlist(IMsgSender sender, PoolKey calldata poolKey, bytes4 selector) internal view { + _isAllowed(Currency.unwrap(poolKey.currency0), sender.msgSender(), address(sender), selector); + _isAllowed(Currency.unwrap(poolKey.currency1), sender.msgSender(), address(sender), selector); } /// @dev checks if the provided token is a wrapped token by checking if it has a verified permissioned token, if yes, check the allowlist - function _isAllowed(address wrappedToken, address sender, address router) internal view { + function _isAllowed(address wrappedToken, address sender, address router, bytes4 selector) internal view { address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(wrappedToken); if (permissionedToken == address(0)) return; + + PermissionFlag permission = PermissionFlags.NONE; + if (selector == this.beforeSwap.selector) { + permission = PermissionFlags.SWAP_ALLOWED; + } else if (selector == this.beforeAddLiquidity.selector) { + permission = PermissionFlags.LIQUIDITY_ALLOWED; + } + if ( - !IWrappedPermissionedToken(wrappedToken).isAllowed(sender) + !IWrappedPermissionedToken(wrappedToken).isAllowed(sender, permission) || !IWrappedPermissionedToken(wrappedToken).allowedWrappers(router) ) { revert Unauthorized(); diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index 8ad422429..7b32fb42b 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -15,13 +15,16 @@ import { IWrappedPermissionedToken } from "./interfaces/IWrappedPermissionedTokenFactory.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {PermissionFlags} from "./libraries/PermissionFlags.sol"; contract PermissionedPositionManager is PositionManager { IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; - IHooks public immutable PERMISSIONED_HOOKS; + + mapping(Currency currency => mapping(IHooks hooks => bool)) public isAllowedHooks; error InvalidHook(); error SafeTransferDisabled(); + error NotWrappedAdmin(); /// @dev as this contract must know the hooks address in advance, it must be passed in as a constructor argument constructor( @@ -30,13 +33,24 @@ contract PermissionedPositionManager is PositionManager { uint256 _unsubscribeGasLimit, IPositionDescriptor _tokenDescriptor, IWETH9 _weth9, - IWrappedPermissionedTokenFactory _wrappedTokenFactory, - IHooks _permissionedHooks + IWrappedPermissionedTokenFactory _wrappedTokenFactory ) PositionManager(_poolManager, _permit2, _unsubscribeGasLimit, _tokenDescriptor, _weth9) { WRAPPED_TOKEN_FACTORY = _wrappedTokenFactory; - PERMISSIONED_HOOKS = _permissionedHooks; } + /// @notice Sets the allowed hook for a given wrapped permissioned token + /// @dev Sets which hooks are allowed to be used with a wrapped permissioned token. Only callable by the owner of the wrapped permissioned token + /// @param currency The currency of the wrapped permissioned token + /// @param hooks The hook to set the allowance for + /// @param allowed Whether the hook is allowed to be used with the wrapped permissioned token + function setAllowedHook(Currency currency, IHooks hooks, bool allowed) external { + if (_getOwner(currency) != msg.sender) { + revert NotWrappedAdmin(); + } + isAllowedHooks[currency][hooks] = allowed; + } + + /// @inheritdoc PositionManager /// @dev Only allow admins of permissioned tokens to transfer positions that contain their tokens function transferFrom(address from, address to, uint256 id) public override onlyIfPoolManagerLocked { (PoolKey memory poolKey,) = getPoolAndPositionInfo(id); @@ -69,10 +83,21 @@ contract PermissionedPositionManager is PositionManager { bytes calldata hookData ) internal override { // allowlist is verified in the hook call - if (poolKey.hooks != PERMISSIONED_HOOKS) revert InvalidHook(); + if (!_checkAllowedHooks(poolKey)) revert InvalidHook(); super._mint(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData); } + function _checkAllowedHooks(PoolKey calldata poolKey) internal view returns (bool) { + return + _checkAllowedHook(poolKey.currency0, poolKey.hooks) && _checkAllowedHook(poolKey.currency1, poolKey.hooks); + } + + function _checkAllowedHook(Currency currency, IHooks hooks) internal view returns (bool) { + address permissionedToken = _verifiedPermissionedTokenOf(currency); + if (permissionedToken == address(0)) return true; + return isAllowedHooks[currency][hooks]; + } + /// @dev When paying to settle, if the currency is a permissioned token, wrap the token and transfer it to the pool manager. function _pay(Currency currency, address payer, uint256 amount) internal virtual override { address permissionedToken = _verifiedPermissionedTokenOf(currency); @@ -85,7 +110,7 @@ contract PermissionedPositionManager is PositionManager { IWrappedPermissionedToken wrappedPermissionedToken = IWrappedPermissionedToken(Currency.unwrap(currency)); if (payer == address(this)) { // @audit is it necessary to check the allowlist here? - if (!wrappedPermissionedToken.isAllowed(msgSender())) { + if (!wrappedPermissionedToken.isAllowed(msgSender(), PermissionFlags.LIQUIDITY_ALLOWED)) { revert Unauthorized(); } Currency.wrap(permissionedToken).transfer(address(wrappedPermissionedToken), amount); diff --git a/src/hooks/permissionedPools/PermissionedV4Router.sol b/src/hooks/permissionedPools/PermissionedV4Router.sol index 8212d9666..902704ce0 100644 --- a/src/hooks/permissionedPools/PermissionedV4Router.sol +++ b/src/hooks/permissionedPools/PermissionedV4Router.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IMsgSender} from "../../interfaces/IMsgSender.sol"; import {ActionConstants} from "../../libraries/ActionConstants.sol"; import {ReentrancyLock} from "../../base/ReentrancyLock.sol"; import {V4Router, IPoolManager, Currency} from "../../V4Router.sol"; @@ -9,15 +8,9 @@ import { IWrappedPermissionedTokenFactory, IWrappedPermissionedToken } from "./interfaces/IWrappedPermissionedTokenFactory.sol"; -import {PermissionedHooks} from "./PermissionedHooks.sol"; import {IWETH9} from "../../interfaces/external/IWETH9.sol"; - import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; -import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {Hooks, IHooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {PermissionFlags} from "./libraries/PermissionFlags.sol"; contract PermissionedV4Router is V4Router, ReentrancyLock { IAllowanceTransfer public immutable PERMIT2; @@ -66,7 +59,7 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { IWrappedPermissionedToken wrappedPermissionedToken = IWrappedPermissionedToken(Currency.unwrap(currency)); if (payer == address(this)) { // allowlist check necessary to ensure a disallowed user cannot sell a permissioned token - if (!wrappedPermissionedToken.isAllowed(msgSender())) { + if (!wrappedPermissionedToken.isAllowed(msgSender(), PermissionFlags.SWAP_ALLOWED)) { revert Unauthorized(); } Currency.wrap(permissionedToken).transfer(address(wrappedPermissionedToken), amount); diff --git a/src/hooks/permissionedPools/WrappedPermissionedToken.sol b/src/hooks/permissionedPools/WrappedPermissionedToken.sol index 8c0130b77..1eb23851d 100644 --- a/src/hooks/permissionedPools/WrappedPermissionedToken.sol +++ b/src/hooks/permissionedPools/WrappedPermissionedToken.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {IWrappedPermissionedToken} from "./interfaces/IWrappedPermissionedToken.sol"; import {IAllowlistChecker} from "./interfaces/IAllowlistChecker.sol"; +import {PermissionFlag} from "./libraries/PermissionFlags.sol"; contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedToken { /// @inheritdoc IWrappedPermissionedToken @@ -50,8 +51,8 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo } /// @inheritdoc IWrappedPermissionedToken - function isAllowed(address account) public view returns (bool) { - return allowListChecker.checkAllowlist(account); + function isAllowed(address account, PermissionFlag permission) public view returns (bool) { + return ((allowListChecker.checkAllowlist(account)) & (permission)) == (permission); } function _updateAllowListChecker(IAllowlistChecker newAllowListChecker) internal { diff --git a/src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol b/src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol index faed3b725..6cde1833b 100644 --- a/src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol +++ b/src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol @@ -2,7 +2,8 @@ pragma solidity ^0.8.0; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {PermissionFlag} from "../libraries/PermissionFlags.sol"; interface IAllowlistChecker is IERC165 { - function checkAllowlist(address account) external view returns (bool); + function checkAllowlist(address account) external view returns (PermissionFlag); } diff --git a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol index 0f889c942..3d7b64193 100644 --- a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol +++ b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IAllowlistChecker} from "./IAllowlistChecker.sol"; +import {IAllowlistChecker, PermissionFlag} from "./IAllowlistChecker.sol"; interface IWrappedPermissionedToken is IERC20 { /// @notice Emitted when the allow list checker is updated @@ -43,7 +44,7 @@ interface IWrappedPermissionedToken is IERC20 { /// @notice Returns whether a transfer is allowed /// @param account The account to check - function isAllowed(address account) external view returns (bool); + function isAllowed(address account, PermissionFlag permission) external view returns (bool); /// @notice Returns the allow list checker function allowListChecker() external view returns (IAllowlistChecker); diff --git a/src/hooks/permissionedPools/libraries/PermissionFlags.sol b/src/hooks/permissionedPools/libraries/PermissionFlags.sol new file mode 100644 index 000000000..3c95decb9 --- /dev/null +++ b/src/hooks/permissionedPools/libraries/PermissionFlags.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +type PermissionFlag is bytes2; + +using {or as |} for PermissionFlag global; +using {and as &} for PermissionFlag global; +using {eq as ==} for PermissionFlag global; + +function or(PermissionFlag a, PermissionFlag b) pure returns (PermissionFlag) { + return PermissionFlag.wrap(PermissionFlag.unwrap(a) | PermissionFlag.unwrap(b)); +} + +function and(PermissionFlag a, PermissionFlag b) pure returns (PermissionFlag) { + return PermissionFlag.wrap(PermissionFlag.unwrap(a) & PermissionFlag.unwrap(b)); +} + +function eq(PermissionFlag a, PermissionFlag b) pure returns (bool) { + return PermissionFlag.unwrap(a) == PermissionFlag.unwrap(b); +} + +library PermissionFlags { + PermissionFlag constant NONE = PermissionFlag.wrap(0x0000); + PermissionFlag constant SWAP_ALLOWED = PermissionFlag.wrap(0x0001); + PermissionFlag constant LIQUIDITY_ALLOWED = PermissionFlag.wrap(0x0002); + PermissionFlag constant ALL_ALLOWED = PermissionFlag.wrap(0xFFFF); + + function hasFlag(PermissionFlag permissions, PermissionFlag flag) internal pure returns (bool) { + return PermissionFlag.unwrap(and(permissions, flag)) != 0; + } +} diff --git a/test/hooks/permissionedPools/PermissionedPoolsBase.sol b/test/hooks/permissionedPools/PermissionedPoolsBase.sol index d3cc14e31..1bf7987ab 100644 --- a/test/hooks/permissionedPools/PermissionedPoolsBase.sol +++ b/test/hooks/permissionedPools/PermissionedPoolsBase.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {BaseAllowlistChecker} from "../../../src/hooks/permissionedPools/BaseAllowListChecker.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {PermissionFlag} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; contract MockToken is ERC20 { constructor() ERC20("MockToken", "MT") {} @@ -22,11 +23,20 @@ contract MockToken is ERC20 { } contract MockPermissionedToken is MockToken { - mapping(address account => bool allowed) public isAllowed; + // this mapping is the allow list for permissioned pools + mapping(address account => PermissionFlag permissions) public accountPermissions; + // this mapping represents if the account is allowed to interact with the token + mapping(address account => bool isAllowed) public isAllowed; error Unauthorized(); - function setAllowlist(address account, bool allowed) public { + function setAllowlist(address account, PermissionFlag permissions) public { + accountPermissions[account] = permissions; + // allowed to transfer the token + isAllowed[account] = true; + } + + function setTokenAllowlist(address account, bool allowed) public { isAllowed[account] = allowed; } @@ -43,8 +53,8 @@ contract MockAllowlistChecker is BaseAllowlistChecker { token = token_; } - function checkAllowlist(address account) public view override returns (bool) { - return token.isAllowed(account); + function checkAllowlist(address account) public view override returns (PermissionFlag) { + return token.accountPermissions(account); } } diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 285f4f8fe..49c17e41e 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -31,6 +31,7 @@ import {ReentrantToken} from "../../mocks/ReentrantToken.sol"; import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; import {MockAllowlistChecker, MockPermissionedToken} from "./PermissionedPoolsBase.sol"; import {WrappedPermissionedToken, IERC20} from "../../../src/hooks/permissionedPools/WrappedPermissionedToken.sol"; +import {PermissionFlags, PermissionFlag} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, LiquidityFuzzers { using FixedPointMathLib for uint256; @@ -61,6 +62,7 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq PoolKey public key2; // normal / normal PoolKey public key3; + PoolKey public keyFake0; PoolKey public keyFake1; PoolKey public keyFake2; @@ -86,19 +88,13 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq } function setUpPosms() internal { - deployAndApprovePosm( - manager, address(wrappedTokenFactory), address(permissionedHooks), keccak256("permissionedPosm") - ); + deployAndApprovePosm(manager, address(wrappedTokenFactory), keccak256("permissionedPosm")); // alternate position manager using different hooks - secondaryPosm = deployAndApprovePosmOnly( - manager, address(wrappedTokenFactory), address(secondaryPermissionedHooks), keccak256("secondaryPosm") - ); + secondaryPosm = deployAndApprovePosmOnly(manager, address(wrappedTokenFactory), keccak256("secondaryPosm")); // alternate position manager using the same hooks - tertiaryPosm = deployAndApprovePosmOnly( - manager, address(wrappedTokenFactory), address(permissionedHooks), keccak256("tertiaryPosm") - ); + tertiaryPosm = deployAndApprovePosmOnly(manager, address(wrappedTokenFactory), keccak256("tertiaryPosm")); } function setupPool() internal { @@ -113,7 +109,15 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq 3000, SQRT_PRICE_1_1 ); - (key3, poolId) = initPool(currency2, currency0, permissionedHooks, 3000, SQRT_PRICE_1_1); + + Currency currencyA = currency2; + Currency currencyB = currency0; + if (currencyA > currencyB) { + currencyA = currency0; + currencyB = currency2; + } + + (key3, poolId) = initPool(currencyA, currencyB, permissionedHooks, 3000, SQRT_PRICE_1_1); (keyFake0, poolId) = initPool(Currency.wrap(address(wrappedToken0)), currency1, secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1); (keyFake1, poolId) = @@ -169,32 +173,59 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq } function setUpAllowlist(Currency currency) internal { - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(this), true); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(alice, true); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), true); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(secondaryPosm), true); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(wrappedTokenFactory), true); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), true); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(manager), true); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(permissionedSwapRouter), true); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(address(permissionedHooks), true); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(this), PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( + address(secondaryPosm), PermissionFlags.ALL_ALLOWED + ); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( + address(wrappedTokenFactory), PermissionFlags.ALL_ALLOWED + ); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(manager), PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( + address(permissionedSwapRouter), PermissionFlags.ALL_ALLOWED + ); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist( + address(permissionedHooks), PermissionFlags.ALL_ALLOWED + ); } function setUpWrappedToken(WrappedPermissionedToken wrappedToken, Currency currency) internal { wrappedToPermissioned[Currency.wrap(address(wrappedToken))] = currency; MockPermissionedToken(Currency.unwrap(currency)).mint(address(this), 1000 ether); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(wrappedToken), true); + MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( + address(wrappedToken), PermissionFlags.ALL_ALLOWED + ); // wrapped token contract must have a non-zero balance of the permissioned token currency.transfer(address(wrappedToken), 1); wrappedToken.updateAllowedWrapper(address(manager), true); - wrappedToken.updateAllowedWrapper(address(secondaryPosm), true); wrappedToken.updateAllowedWrapper(address(lpm), true); + wrappedToken.updateAllowedWrapper(address(secondaryPosm), true); wrappedToken.updateAllowedWrapper(address(permissionedSwapRouter), true); wrappedTokenFactory.verifyWrappedToken(address(wrappedToken)); + + Currency wrappedCurrency = Currency.wrap(address(wrappedToken)); + + setAllowedHooks(lpm, wrappedCurrency, permissionedHooks); + setAllowedHooks(tertiaryPosm, wrappedCurrency, permissionedHooks); + + setAllowedHooks(lpm, wrappedCurrency, secondaryPermissionedHooks); + setAllowedHooks(secondaryPosm, wrappedCurrency, secondaryPermissionedHooks); + setAllowedHooks(tertiaryPosm, wrappedCurrency, secondaryPermissionedHooks); + } + + function setAllowedHooks(IPositionManager posm, Currency currency, IHooks permissionedHooks_) internal { + // addPermissionedHooks selector + bytes4 selector = 0xb5cdc484; + bytes memory data = abi.encodeWithSelector(selector, currency, permissionedHooks_, true); + (bool success,) = address(posm).call(data); + require(success, "Failed to set hooks"); } function test_modifyLiquidities_reverts_deadlinePassed() public { @@ -655,9 +686,9 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq uint256 liquidity = 1e18; // Add some tokens to unauthorized user - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, true); + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.ALL_ALLOWED); MockPermissionedToken(Currency.unwrap(currency0)).mint(unauthorizedUser, 1000e18); - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, false); + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); MockERC20(Currency.unwrap(currency1)).mint(unauthorizedUser, 1000e18); vm.startPrank(unauthorizedUser); @@ -729,8 +760,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, false); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, false); + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.NONE); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.NONE); tokenId = lpm.nextTokenId(); @@ -747,8 +778,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); vm.stopPrank(); - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, true); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, true); + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); } function test_unpermissioned_sided_mint_disallowed_user_reverts() public { @@ -848,9 +879,13 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq } // Add some tokens to unauthorized user - MockPermissionedToken(Currency.unwrap(currencyPermissioned)).setAllowlist(unauthorizedUser, true); + MockPermissionedToken(Currency.unwrap(currencyPermissioned)).setAllowlist( + unauthorizedUser, PermissionFlags.ALL_ALLOWED + ); MockPermissionedToken(Currency.unwrap(currencyPermissioned)).mint(unauthorizedUser, 1000e18); - MockPermissionedToken(Currency.unwrap(currencyPermissioned)).setAllowlist(unauthorizedUser, false); + MockPermissionedToken(Currency.unwrap(currencyPermissioned)).setAllowlist( + unauthorizedUser, PermissionFlags.NONE + ); vm.startPrank(unauthorizedUser); @@ -1090,12 +1125,10 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq address target = address(lpm); vm.startPrank(alice); - vm.expectRevert(SafeTransferDisabled.selector); + (bool success, bytes memory data) = address(target).call(encodedCall); - bool success; - assembly ("memory-safe") { - success := call(gas(), target, 0, add(encodedCall, 0x20), mload(encodedCall), 0, 0) - } + assertEq(success, false); + assertEq(bytes4(data), SafeTransferDisabled.selector); vm.stopPrank(); } @@ -1105,12 +1138,10 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq address target = address(lpm); vm.startPrank(alice); - vm.expectRevert(SafeTransferDisabled.selector); + (bool success, bytes memory data) = address(target).call(encodedCall); - bool success; - assembly ("memory-safe") { - success := call(gas(), target, 0, add(encodedCall, 0x20), mload(encodedCall), 0, 0) - } + assertEq(success, false); + assertEq(bytes4(data), SafeTransferDisabled.selector); vm.stopPrank(); } @@ -1203,4 +1234,174 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq vm.expectRevert(); secondaryPosm.modifyLiquidities(calls, _deadline); } + + // ===== PERMISSION FLAG TESTS ===== + + function test_permission_flag_none() public { + // Test that NONE permissions prevent all operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.NONE); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.NONE); + + // Should revert when trying to mint with NONE permissions + PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + + vm.prank(alice); + vm.expectRevert(); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + } + + function test_permission_flag_swap_allowed() public { + // Test that SWAP_ALLOWED only allows swaps, not liquidity operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + + // Should revert when trying to mint (liquidity operation) with only SWAP_ALLOWED + PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + + vm.prank(alice); + vm.expectRevert(); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + } + + function test_permission_flag_liquidity_allowed() public { + // Test that LIQUIDITY_ALLOWED allows liquidity operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + + // Should succeed when trying to mint with LIQUIDITY_ALLOWED + PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + uint256 tokenId = lpm.nextTokenId(); + + vm.prank(alice); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + } + + function test_permission_flag_all_allowed() public { + // Test that ALL_ALLOWED allows all operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); + + // Should succeed when trying to mint with ALL_ALLOWED + PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + uint256 tokenId = lpm.nextTokenId(); + + vm.prank(alice); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + } + + function test_permission_flag_combinations() public { + // Test various combinations of permissions + PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + + // Test SWAP_ALLOWED + LIQUIDITY_ALLOWED (should work like ALL_ALLOWED) + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist( + alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED) + ); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist( + alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED) + ); + + uint256 tokenId = lpm.nextTokenId(); + vm.prank(alice); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + } + + function test_permission_flag_partial_permissions() public { + // Test that having permissions on only one token is enough + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.NONE); + + PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + + vm.prank(alice); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + } + + function test_permission_flag_dynamic_changes() public { + // Test that permission changes take effect immediately + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + + PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + + // Should succeed initially + uint256 tokenId = lpm.nextTokenId(); + vm.prank(alice); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + + // Remove permissions + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.NONE); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.NONE); + + // Should fail on subsequent operations + tokenId = lpm.nextTokenId(); + vm.prank(alice); + vm.expectRevert(); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + } + + function test_permission_flag_edge_cases() public { + // Test edge cases with permission flags + PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + + // Test with zero permissions (should be same as NONE) + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlag.wrap(0x0000)); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlag.wrap(0x0000)); + + vm.prank(alice); + vm.expectRevert(); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + + // Test with maximum permissions (should be same as ALL_ALLOWED) + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlag.wrap(0xFFFF)); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlag.wrap(0xFFFF)); + + uint256 tokenId = lpm.nextTokenId(); + vm.prank(alice); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + } + + function test_permission_flag_all_pools() public { + // Test permission flags work across all pool types + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + + PositionConfig memory config0 = PositionConfig({poolKey: key0, tickLower: -120, tickUpper: 120}); + PositionConfig memory config1 = PositionConfig({poolKey: key1, tickLower: -120, tickUpper: 120}); + PositionConfig memory config2 = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + + // Test permissioned/normal pool + uint256 tokenId0 = lpm.nextTokenId(); + vm.prank(alice); + mint(config0, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + assertEq(IERC721(address(lpm)).ownerOf(tokenId0), alice); + + // Test normal/permissioned pool + uint256 tokenId1 = lpm.nextTokenId(); + vm.prank(alice); + mint(config1, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + assertEq(IERC721(address(lpm)).ownerOf(tokenId1), alice); + + // Test permissioned/permissioned pool + uint256 tokenId2 = lpm.nextTokenId(); + vm.prank(alice); + mint(config2, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + assertEq(IERC721(address(lpm)).ownerOf(tokenId2), alice); + } } diff --git a/test/hooks/permissionedPools/PermissionedV4Router.t.sol b/test/hooks/permissionedPools/PermissionedV4Router.t.sol index 8f4022279..334d8b392 100644 --- a/test/hooks/permissionedPools/PermissionedV4Router.t.sol +++ b/test/hooks/permissionedPools/PermissionedV4Router.t.sol @@ -14,6 +14,8 @@ import {Planner} from "../../shared/Planner.sol"; import {Actions} from "../../../src/libraries/Actions.sol"; import {ActionConstants} from "../../../src/libraries/ActionConstants.sol"; import {MockPermissionedToken} from "./PermissionedPoolsBase.sol"; +import {PermissionFlags, PermissionFlag} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; +import {PathKey} from "../../../src/libraries/PathKey.sol"; contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { // To allow testing without importing PermissionedV4Router @@ -110,13 +112,13 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PERMISSION TESTS //////////////////////////////////////////////////////////////*/ - function test_swap_unauthorized_user_reverts() public { - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, true); - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, true); + function test_swap_reverts_unauthorized_user() public { + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, PermissionFlags.ALL_ALLOWED); IERC20(Currency.unwrap(currency0)).transfer(unauthorizedUser, 2 ether); IERC20(Currency.unwrap(currency1)).transfer(unauthorizedUser, 2 ether); - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, false); - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, false); + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); Currency currencyA = wrappedCurrency0; Currency currencyB = wrappedCurrency1; @@ -300,9 +302,9 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { } // Give unauthorized user the permissioned currency - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, true); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, PermissionFlags.ALL_ALLOWED); IERC20(Currency.unwrap(currency1)).transfer(unauthorizedUser, 2 ether); - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, false); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); uint256 amountIn = 100; @@ -362,6 +364,283 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { permissionedRouter.execute(data); } + function test_permission_flag_none_swap_reverts() public { + // Test that NONE permissions prevent swap operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.NONE); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.NONE); + + uint256 amountIn = 100; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + vm.prank(alice); + vm.expectRevert(); + permissionedRouter.execute(data); + } + + function test_permission_flag_swap_allowed_succeeds() public { + // Test that SWAP_ALLOWED allows swap operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + vm.prank(alice); + permissionedRouter.execute(data); + } + + function test_permission_flag_liquidity_allowed_swap_reverts() public { + // Test that LIQUIDITY_ALLOWED does not allow swap operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + vm.prank(alice); + vm.expectRevert(); + permissionedRouter.execute(data); + } + + function test_permission_flag_all_allowed_swap_succeeds() public { + // Test that ALL_ALLOWED allows swap operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); + + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + vm.prank(alice); + permissionedRouter.execute(data); + } + + function test_permission_flag_combinations_swap() public { + // Test various combinations of permissions for swaps + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + // Test SWAP_ALLOWED + LIQUIDITY_ALLOWED (should work like ALL_ALLOWED) + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist( + alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED) + ); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist( + alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED) + ); + + vm.prank(alice); + permissionedRouter.execute(data); + } + + function test_permission_flag_partial_permissions_swap() public { + // Test that having permissions on only one token is not enough for swaps + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.NONE); + + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + vm.prank(alice); + vm.expectRevert(); + permissionedRouter.execute(data); + } + + function test_permission_flag_dynamic_changes_swap() public { + // Test that permission changes take effect immediately for swaps + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + // Should succeed initially + vm.prank(alice); + permissionedRouter.execute(data); + + // Remove permissions + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.NONE); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.NONE); + + // Should fail on subsequent operations + vm.prank(alice); + vm.expectRevert(); + permissionedRouter.execute(data); + } + + function test_permission_flag_edge_cases_swap() public { + // Test edge cases with permission flags for swaps + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + // Test with zero permissions (should be same as NONE) + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlag.wrap(0x0000)); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlag.wrap(0x0000)); + + vm.prank(alice); + vm.expectRevert(); + permissionedRouter.execute(data); + + // Test with maximum permissions (should be same as ALL_ALLOWED) + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlag.wrap(0xFFFF)); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlag.wrap(0xFFFF)); + + vm.prank(alice); + permissionedRouter.execute(data); + } + + function test_permission_flag_all_pool_types_swap() public { + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(wrappedCurrency0))).setAllowlist( + alice, PermissionFlags.SWAP_ALLOWED + ); + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(wrappedCurrency1))).setAllowlist( + alice, PermissionFlags.SWAP_ALLOWED + ); + + _test_permission_flag_all_pool_types_swap(key0); + _test_permission_flag_all_pool_types_swap(key1); + _test_permission_flag_all_pool_types_swap(key2); + } + + function _test_permission_flag_all_pool_types_swap(PoolKey memory poolKey) public { + IERC20(Currency.unwrap(getPermissionedCurrency(poolKey.currency0))).transfer(alice, 2 ether); + IERC20(Currency.unwrap(getPermissionedCurrency(poolKey.currency1))).transfer(alice, 2 ether); + + uint256 amountIn = 100; + plan = Planner.init(); + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(poolKey, true, uint128(amountIn), 0, bytes("")); + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + plan = plan.add(Actions.SETTLE, abi.encode(poolKey.currency0, ActionConstants.CONTRACT_BALANCE, false)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(poolKey.currency1, 0)); + bytes memory data = plan.finalizeSwap(poolKey.currency0, poolKey.currency1, ActionConstants.MSG_SENDER); + + vm.prank(alice); + permissionedRouter.execute(data); + } + + function test_permission_flag_exact_output_swap() public { + // Test permission flags with exact output swaps + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountOut = 50; + uint256 amountInMaximum = 100; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactOutputSingleParams memory params = + IV4Router.ExactOutputSingleParams(wrappedKey, true, uint128(amountOut), uint128(amountInMaximum), bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + vm.prank(alice); + permissionedRouter.execute(data); + } + + function test_permission_flag_multi_hop_swap() public { + // Test permission flags with multi-hop swaps + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + Currency[] memory path = new Currency[](2); + path[0] = wrappedCurrency1; + path[1] = wrappedCurrency0; + + // Create path keys for the multi-hop swap + PathKey[] memory pathKeys = new PathKey[](1); + pathKeys[0] = PathKey({ + intermediateCurrency: wrappedCurrency0, + fee: 3000, + tickSpacing: 60, + hooks: permissionedHooks, + hookData: bytes("") + }); + + IV4Router.ExactInputParams memory params = + IV4Router.ExactInputParams(wrappedCurrency1, pathKeys, uint128(amountIn), 0); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + vm.prank(alice); + permissionedRouter.execute(data); + } + /*////////////////////////////////////////////////////////////// ERC20 -> ERC20 EXACT INPUT //////////////////////////////////////////////////////////////*/ @@ -1176,8 +1455,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { tokenPath.push(wrappedCurrency1); tokenPath.push(currency2); - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, false); - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, false); + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); IV4Router.ExactOutputParams memory params = _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); @@ -1245,8 +1524,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { tokenPath.push(wrappedCurrency1); tokenPath.push(currency2); - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, false); - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, false); + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); IV4Router.ExactOutputParams memory params = _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); @@ -1449,7 +1728,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountIn = 100000; MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(key0.currency0))).setAllowlist( - unauthorizedUser, true + unauthorizedUser, PermissionFlags.ALL_ALLOWED ); IERC20(Currency.unwrap(getPermissionedCurrency(key0.currency0))).transfer(address(unauthorizedUser), amountIn); @@ -1457,7 +1736,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(getPermissionedCurrency(key0.currency0))).transfer(address(permissionedRouter), amountIn); MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(key0.currency0))).setAllowlist( - unauthorizedUser, false + unauthorizedUser, PermissionFlags.NONE ); IV4Router.ExactInputSingleParams memory params = @@ -1504,6 +1783,37 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { permissionedHooks.afterDonate(address(this), key0, 0, 0, bytes("")); } + /*////////////////////////////////////////////////////////////// + RECEIVE FALLBACK TESTS + //////////////////////////////////////////////////////////////*/ + + error InvalidEthSender(); + + function test_receive_fallback_reverts() public { + (bool success, bytes memory data) = address(permissionedRouter).call{value: 1 ether}(""); + + assertEq(success, false); + assertEq(bytes4(data), InvalidEthSender.selector); + } + + function test_receive_fallback_from_posm() public { + vm.deal(address(manager), 1 ether); + + vm.prank(address(manager)); + (bool success,) = address(permissionedRouter).call{value: 1 ether}(""); + assertEq(success, true); + } + + function test_receive_fallback_from_weth() public { + vm.deal(address(permissionedRouter), 1 ether); + + vm.startPrank(address(permissionedRouter)); + weth9.deposit{value: 1 ether}(); + weth9.approve(address(permissionedRouter), 1 ether); + weth9.withdraw(1 ether); + vm.stopPrank(); + } + receive() external payable {} fallback() external {} } diff --git a/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol b/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol index ded897dc1..f27162716 100644 --- a/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol +++ b/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol @@ -8,6 +8,7 @@ import { import {IERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {PermissionedPoolsBase, MockAllowlistChecker} from "./PermissionedPoolsBase.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {PermissionFlags, PermissionFlag} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; contract WrappedPermissionedTokenTest is PermissionedPoolsBase { IWrappedPermissionedToken public wrappedPermissionedToken; @@ -24,7 +25,7 @@ contract WrappedPermissionedTokenTest is PermissionedPoolsBase { assembly { sstore(wrappedPermissionedToken.slot, create(0, add(initcode, 0x20), mload(initcode))) } - permissionedToken.setAllowlist(address(wrappedPermissionedToken), true); + permissionedToken.setTokenAllowlist(address(wrappedPermissionedToken), true); vm.prank(owner); wrappedPermissionedToken.updateAllowedWrapper(address(this), true); } @@ -128,7 +129,7 @@ contract WrappedPermissionedTokenTest is PermissionedPoolsBase { recipient != address(0) && recipient != mockPoolManager && recipient != address(wrappedPermissionedToken) ); assertEq(permissionedToken.balanceOf(recipient), 0); - permissionedToken.setAllowlist(recipient, true); + permissionedToken.setTokenAllowlist(recipient, true); transferAmount = bound(transferAmount, 0, mintAmount); permissionedToken.mint(address(wrappedPermissionedToken), mintAmount); wrappedPermissionedToken.wrapToPoolManager(mintAmount); @@ -144,8 +145,8 @@ contract WrappedPermissionedTokenTest is PermissionedPoolsBase { } contract ImproperAllowlistChecker is IAllowlistChecker { - function checkAllowlist(address) public pure returns (bool) { - return true; + function checkAllowlist(address) public pure returns (PermissionFlag) { + return PermissionFlags.ALL_ALLOWED; } function supportsInterface(bytes4) external pure returns (bool) { diff --git a/test/hooks/permissionedPools/WrappedPermissionedTokenFactory.t.sol b/test/hooks/permissionedPools/WrappedPermissionedTokenFactory.t.sol index a36faeb9a..4767251ab 100644 --- a/test/hooks/permissionedPools/WrappedPermissionedTokenFactory.t.sol +++ b/test/hooks/permissionedPools/WrappedPermissionedTokenFactory.t.sol @@ -10,6 +10,7 @@ import {IWrappedPermissionedTokenFactory} from import {IERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {PermissionedPoolsBase, MockAllowlistChecker} from "./PermissionedPoolsBase.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {PermissionFlags} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; contract WrappedPermissionedTokenFactoryTest is PermissionedPoolsBase { IWrappedPermissionedTokenFactory public wrappedPermissionedTokenFactory; @@ -70,7 +71,7 @@ contract WrappedPermissionedTokenFactoryTest is PermissionedPoolsBase { address wrappedPermissionedToken = wrappedPermissionedTokenFactory.createWrappedPermissionedToken( permissionedToken, makeAddr("initialOwner"), allowlistChecker ); - permissionedToken.setAllowlist(wrappedPermissionedToken, true); + permissionedToken.setAllowlist(wrappedPermissionedToken, PermissionFlags.ALL_ALLOWED); permissionedToken.mint(wrappedPermissionedToken, 1); vm.expectEmit(true, true, true, true); emit IWrappedPermissionedTokenFactory.WrappedTokenVerified(wrappedPermissionedToken, address(permissionedToken)); @@ -85,7 +86,7 @@ contract WrappedPermissionedTokenFactoryTest is PermissionedPoolsBase { address wrappedPermissionedToken = wrappedPermissionedTokenFactory.createWrappedPermissionedToken( permissionedToken, makeAddr("initialOwner"), allowlistChecker ); - permissionedToken.setAllowlist(wrappedPermissionedToken, true); + permissionedToken.setAllowlist(wrappedPermissionedToken, PermissionFlags.ALL_ALLOWED); permissionedToken.mint(wrappedPermissionedToken, 1); wrappedPermissionedTokenFactory.verifyWrappedToken(wrappedPermissionedToken); vm.expectRevert( diff --git a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol index 5a97c9753..285cdd0e4 100644 --- a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol @@ -40,6 +40,7 @@ import {MockHooks} from "../mocks/MockHooks.sol"; import {Deploy} from "../../../../test/shared/Deploy.sol"; import {HookMiner} from "../../../../src/utils/HookMiner.sol"; import {IWETH9} from "../../../../src/interfaces/external/IWETH9.sol"; +import {PermissionFlags} from "../../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; /// @notice A contract that provides permissioned deployment functionality for tests /// This moves the deployFreshManagerAndRoutersPermissioned function from v4-core to the test folder @@ -242,7 +243,7 @@ contract PermissionedDeployers is Test { for (uint8 i = 0; i < count; i++) { if (arePermissioned) { tokens[i] = MockERC20(address(new MockPermissionedToken())); - MockPermissionedToken(address(tokens[i])).setAllowlist(address(this), true); + MockPermissionedToken(address(tokens[i])).setAllowlist(address(this), PermissionFlags.ALL_ALLOWED); tokens[i].mint(address(this), totalSupply); } else { tokens[i] = new MockERC20("TEST", "TEST", 18); diff --git a/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol b/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol index 6ccaf3cc5..124e89ce4 100644 --- a/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol +++ b/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol @@ -9,6 +9,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {WETH} from "solmate/src/tokens/WETH.sol"; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; @@ -52,22 +53,12 @@ contract PermissionedPosmTestSetup is Test, PermissionedDeployers, DeployPermit2 mapping(Currency => Currency) public wrappedToPermissioned; - function deployAndApprovePosm( - IPoolManager poolManager, - address wrappedTokenFactory, - address permissionedHooks_, - bytes32 salt - ) public { - deployPermissionedPosm(poolManager, wrappedTokenFactory, permissionedHooks_, salt); + function deployAndApprovePosm(IPoolManager poolManager, address wrappedTokenFactory, bytes32 salt) public { + deployPermissionedPosm(poolManager, wrappedTokenFactory, salt); approvePosm(); } - function deployPermissionedPosm( - IPoolManager poolManager, - address wrappedTokenFactory, - address permissionedHooks_, - bytes32 salt - ) internal { + function deployPermissionedPosm(IPoolManager poolManager, address wrappedTokenFactory, bytes32 salt) internal { permit2 = IAllowanceTransfer(deployPermit2()); _WETH9 = deployWETH(); proxyAsImplementation = deployDescriptor(poolManager, "ETH"); @@ -78,17 +69,14 @@ contract PermissionedPosmTestSetup is Test, PermissionedDeployers, DeployPermit2 address(proxyAsImplementation), address(_WETH9), wrappedTokenFactory, - permissionedHooks_, abi.encode(salt) ); } - function deployAndApprovePosmOnly( - IPoolManager poolManager, - address wrappedTokenFactory, - address permissionedHooks_, - bytes32 salt - ) public returns (IPositionManager secondaryPosm) { + function deployAndApprovePosmOnly(IPoolManager poolManager, address wrappedTokenFactory, bytes32 salt) + public + returns (IPositionManager secondaryPosm) + { secondaryPosm = Deploy.permissionedPositionManager( address(poolManager), address(permit2), @@ -96,7 +84,6 @@ contract PermissionedPosmTestSetup is Test, PermissionedDeployers, DeployPermit2 address(proxyAsImplementation), address(_WETH9), wrappedTokenFactory, - permissionedHooks_, abi.encode(salt) ); approvePosm(); diff --git a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol index e1022e78f..8eac58f30 100644 --- a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol @@ -27,6 +27,7 @@ import {IPositionDescriptor} from "../../../../src/interfaces/IPositionDescripto import {IV4Router} from "../../../../src/interfaces/IV4Router.sol"; import {ActionConstants} from "../../../../src/libraries/ActionConstants.sol"; import {PermissionedDeployers} from "./PermissionedDeployers.sol"; +import {PermissionFlags} from "../../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; /// @notice A shared test contract that wraps the v4-core deployers contract and exposes basic helpers for swapping with the permissioned router. contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 { @@ -123,8 +124,12 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 break; } } - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(address(wrappedToken0), true); - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(address(wrappedToken1), true); + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist( + address(wrappedToken0), PermissionFlags.ALL_ALLOWED + ); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist( + address(wrappedToken1), PermissionFlags.ALL_ALLOWED + ); // Transfer some underlying tokens to the wrapped tokens for verification IERC20(Currency.unwrap(currency0)).transfer(address(wrappedToken0), 1); @@ -148,6 +153,17 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 wrappedToken1.updateAllowedWrapper(address(permissionedRouter), true); wrappedToken0.updateAllowedWrapper(address(permissionedHooks), true); wrappedToken1.updateAllowedWrapper(address(permissionedHooks), true); + + setAllowedHooks(IPositionManager(positionManager), Currency.wrap(address(wrappedToken0)), permissionedHooks); + setAllowedHooks(IPositionManager(positionManager), Currency.wrap(address(wrappedToken1)), permissionedHooks); + } + + function setAllowedHooks(IPositionManager posm, Currency currency, IHooks permissionedHooks_) internal { + // addPermissionedHooks selector + bytes4 selector = 0xb5cdc484; + bytes memory data = abi.encodeWithSelector(selector, currency, permissionedHooks_, true); + (bool success,) = address(posm).call(data); + require(success, "Failed to set hooks"); } function createPoolWithLiquidity(Currency currencyA, Currency currencyB, address hookAddr) @@ -345,15 +361,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 function _deployPositionManager() private { bytes memory posmBytecode = abi.encodePacked( vm.getCode("PermissionedPositionManager.sol:PermissionedPositionManager"), - abi.encode( - manager, - permit2, - 1e18, - address(tokenDescriptor), - address(weth9), - wrappedTokenFactory, - address(permissionedHooks) - ) + abi.encode(manager, permit2, 1e18, address(tokenDescriptor), address(weth9), wrappedTokenFactory) ); positionManager = Deploy.create2(posmBytecode, keccak256("permissionedPosm")); } @@ -390,14 +398,14 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 function _setupMockAllowList(Currency currency, address spender) private { MockPermissionedToken mockPermissionedToken = MockPermissionedToken(Currency.unwrap(currency)); mockAllowlistChecker = new MockAllowlistChecker(mockPermissionedToken); - mockPermissionedToken.setAllowlist(address(this), true); - mockPermissionedToken.setAllowlist(address(permissionedRouter), true); - mockPermissionedToken.setAllowlist(address(permissionedHooks), true); - mockPermissionedToken.setAllowlist(address(positionManager), true); - mockPermissionedToken.setAllowlist(address(wrappedTokenFactory), true); - mockPermissionedToken.setAllowlist(address(manager), true); - mockPermissionedToken.setAllowlist(address(permit2), true); - mockPermissionedToken.setAllowlist(spender, true); + mockPermissionedToken.setAllowlist(address(this), PermissionFlags.ALL_ALLOWED); + mockPermissionedToken.setAllowlist(address(permissionedRouter), PermissionFlags.ALL_ALLOWED); + mockPermissionedToken.setAllowlist(address(permissionedHooks), PermissionFlags.ALL_ALLOWED); + mockPermissionedToken.setAllowlist(address(positionManager), PermissionFlags.ALL_ALLOWED); + mockPermissionedToken.setAllowlist(address(wrappedTokenFactory), PermissionFlags.ALL_ALLOWED); + mockPermissionedToken.setAllowlist(address(manager), PermissionFlags.ALL_ALLOWED); + mockPermissionedToken.setAllowlist(address(permit2), PermissionFlags.ALL_ALLOWED); + mockPermissionedToken.setAllowlist(spender, PermissionFlags.ALL_ALLOWED); } function _deployTokensMintAndApprove(uint8 count, uint8 permissionedCount) internal returns (MockERC20[] memory) { diff --git a/test/shared/Deploy.sol b/test/shared/Deploy.sol index 650ac990b..1a8d64962 100644 --- a/test/shared/Deploy.sol +++ b/test/shared/Deploy.sol @@ -34,17 +34,10 @@ library Deploy { address positionDescriptor_, address wrappedNative, address wrappedTokenFactory, - address permissionedHooks, bytes memory salt ) internal returns (IPositionManager manager) { bytes memory args = abi.encode( - poolManager, - permit2, - unsubscribeGasLimit, - positionDescriptor_, - wrappedNative, - wrappedTokenFactory, - permissionedHooks + poolManager, permit2, unsubscribeGasLimit, positionDescriptor_, wrappedNative, wrappedTokenFactory ); bytes memory initcode = abi.encodePacked(vm.getCode("PermissionedPositionManager.sol:PermissionedPositionManager"), args); From f596e8243c4884586c3e947f0865d5d7d9c87064 Mon Sep 17 00:00:00 2001 From: ens_408 Date: Tue, 22 Jul 2025 14:23:23 -0400 Subject: [PATCH 09/33] permissioned-pools: match universalrouter interface (#488) * initial commit * linted * make dispatch internal * changed revert for command not implemented * add underscore to internal/prive functions on permissioned v4 router * applied fmt to permissioned v4 router --- snapshots/PermissionedV4RouterTest.json | 2 +- .../PermissionedV4Router.sol | 121 ++++++++++++++- .../PermissionedV4Router.t.sol | 140 +++++++++++++----- .../shared/PermissionedDeployers.sol | 9 +- .../shared/PermissionedRoutingTestHelpers.sol | 35 ++++- 5 files changed, 263 insertions(+), 44 deletions(-) diff --git a/snapshots/PermissionedV4RouterTest.json b/snapshots/PermissionedV4RouterTest.json index 4d6e178ed..73a44ea80 100644 --- a/snapshots/PermissionedV4RouterTest.json +++ b/snapshots/PermissionedV4RouterTest.json @@ -1,3 +1,3 @@ { - "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "223575" + "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "225905" } \ No newline at end of file diff --git a/src/hooks/permissionedPools/PermissionedV4Router.sol b/src/hooks/permissionedPools/PermissionedV4Router.sol index 902704ce0..a32069f79 100644 --- a/src/hooks/permissionedPools/PermissionedV4Router.sol +++ b/src/hooks/permissionedPools/PermissionedV4Router.sol @@ -20,6 +20,18 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { error Unauthorized(); error HookNotImplemented(); error InvalidEthSender(); + error TransactionDeadlinePassed(); + error LengthMismatch(); + error SliceOutOfBounds(); + error InvalidCommandType(uint256 commandType); + /// @notice Thrown when a required command has failed + error ExecutionFailed(uint256 commandIndex, bytes message); + + // Commands + uint256 constant COMMAND_V4_SWAP = 0x10; + uint256 constant COMMAND_PERMIT2_PERMIT = 0x0a; + bytes1 internal constant COMMAND_FLAG_ALLOW_REVERT = 0x80; + bytes1 internal constant COMMAND_TYPE_MASK = 0x3f; constructor( IPoolManager poolManager_, @@ -32,8 +44,49 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { WETH9 = weth9; } - function execute(bytes calldata input) public payable isNotLocked { - _executeActions(input); + modifier checkDeadline(uint256 deadline) { + if (block.timestamp > deadline) revert TransactionDeadlinePassed(); + _; + } + + /// @notice To receive ETH from WETH + receive() external payable { + if (msg.sender != address(WETH9) && msg.sender != address(poolManager)) revert InvalidEthSender(); + } + + /// @notice Executes encoded commands along with provided inputs. Reverts if deadline has expired. + /// @param commands A set of concatenated commands, each 1 byte in length + /// @param inputs An array of byte strings containing abi encoded inputs for each command + /// @param deadline The deadline by which the transaction must be executed + function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) + external + payable + checkDeadline(deadline) + { + execute(commands, inputs); + } + + /// @notice Executes encoded commands along with provided inputs. + /// @param commands A set of concatenated commands, each 1 byte in length + /// @param inputs An array of byte strings containing abi encoded inputs for each command + function execute(bytes calldata commands, bytes[] calldata inputs) public payable isNotLocked { + bool success; + bytes memory output; + uint256 numCommands = commands.length; + if (inputs.length != numCommands) revert LengthMismatch(); + + // loop through all given commands, execute them and pass along outputs as defined + for (uint256 commandIndex = 0; commandIndex < numCommands; commandIndex++) { + bytes1 command = commands[commandIndex]; + + bytes calldata input = inputs[commandIndex]; + + (success, output) = _dispatch(command, input); + + if (!success && _successRequired(command)) { + revert ExecutionFailed({commandIndex: commandIndex, message: output}); + } + } } /// @notice Public view function to be used instead of msg.sender, as the contract performs self-reentrancy and at @@ -43,6 +96,36 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { return _getLocker(); } + /// @notice Decodes and executes the given command with the given inputs + function _dispatch(bytes1 commandType, bytes calldata inputs) + internal + returns (bool success, bytes memory output) + { + uint256 command = uint8(commandType & COMMAND_TYPE_MASK); + success = true; + if (command == COMMAND_PERMIT2_PERMIT) { + // equivalent: abi.decode(inputs, (IAllowanceTransfer.PermitSingle, bytes)) + IAllowanceTransfer.PermitSingle calldata permitSingle; + assembly { + permitSingle := inputs.offset + } + bytes calldata data = _toBytes(inputs, 6); // PermitSingle takes first 6 slots (0..5) + + (success, output) = address(PERMIT2).call( + abi.encodeWithSignature( + "permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)", + msgSender(), + permitSingle, + data + ) + ); + } else if (command == COMMAND_V4_SWAP) { + _executeActions(inputs); + } else { + revert InvalidCommandType(command); + } + } + function _pay(Currency currency, address payer, uint256 amount) internal override { address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(currency)); if (permissionedToken == address(0)) { @@ -81,8 +164,36 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { return Currency.wrap(permissionedToken).balanceOfSelf(); } - /// @notice To receive ETH from WETH - receive() external payable { - if (msg.sender != address(WETH9) && msg.sender != address(poolManager)) revert InvalidEthSender(); + function _successRequired(bytes1 command) internal pure returns (bool) { + return command & COMMAND_FLAG_ALLOW_REVERT == 0; + } + + /// @notice Decode the `_arg`-th element in `_bytes` as a dynamic array + /// @dev This function is copied from BytesLib in universal-router to avoid adding it into the repositoriy + function _toLengthOffset(bytes calldata _bytes, uint256 _arg) + private + pure + returns (uint256 length, uint256 offset) + { + uint256 relativeOffset; + assembly { + // The offset of the `_arg`-th element is `32 * arg`, which stores the offset of the length pointer. + // shl(5, x) is equivalent to mul(32, x) + let lengthPtr := add(_bytes.offset, calldataload(add(_bytes.offset, shl(5, _arg)))) + length := calldataload(lengthPtr) + offset := add(lengthPtr, 0x20) + relativeOffset := sub(offset, _bytes.offset) + } + if (_bytes.length < length + relativeOffset) revert SliceOutOfBounds(); + } + + /// @notice Decode the `_arg`-th element in `_bytes` as `bytes` + /// @dev This function is copied from BytesLib in universal-router to avoid adding it into the repositoriy + function _toBytes(bytes calldata _bytes, uint256 _arg) private pure returns (bytes calldata res) { + (uint256 length, uint256 offset) = _toLengthOffset(_bytes, _arg); + assembly { + res.length := length + res.offset := offset + } } } diff --git a/test/hooks/permissionedPools/PermissionedV4Router.t.sol b/test/hooks/permissionedPools/PermissionedV4Router.t.sol index 334d8b392..e140a3e06 100644 --- a/test/hooks/permissionedPools/PermissionedV4Router.t.sol +++ b/test/hooks/permissionedPools/PermissionedV4Router.t.sol @@ -8,6 +8,8 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {PermitHash} from "permit2/src/libraries/PermitHash.sol"; import {IV4Router} from "../../../src/interfaces/IV4Router.sol"; import {PermissionedRoutingTestHelpers} from "./shared/PermissionedRoutingTestHelpers.sol"; import {Planner} from "../../shared/Planner.sol"; @@ -18,10 +20,15 @@ import {PermissionFlags, PermissionFlag} from "../../../src/hooks/permissionedPo import {PathKey} from "../../../src/libraries/PathKey.sol"; contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { + using PermitHash for IAllowanceTransfer.PermitSingle; + // To allow testing without importing PermissionedV4Router error HookNotImplemented(); error Unauthorized(); error HookCallFailed(); + error SliceOutOfBounds(); + error InvalidCommandType(uint256 commandType); + error ExecutionFailed(uint256 commandIndex, bytes output); Currency public wrappedCurrency0; Currency public wrappedCurrency1; @@ -30,6 +37,10 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { address public alice = makeAddr("ALICE"); address public unauthorizedUser = makeAddr("UNAUTHORIZED"); + // Commands + bytes public COMMAND_V4_SWAP = hex"10"; + bytes public COMMAND_PERMIT2_PERMIT = hex"0a"; + function setUp() public { setupPermissionedRouterCurrenciesAndPoolsWithLiquidity(alice); @@ -104,7 +115,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); vm.snapshotGasLastCall("PermissionedV4Router_ExactInputSingle_PermissionedTokens"); } @@ -143,7 +154,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { abi.encodeWithSelector(HookCallFailed.selector) ) ); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_swap_authorized_user() public { @@ -160,7 +171,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); vm.prank(alice); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_swap_authorized_router() public { @@ -180,7 +191,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = getInputAndOutputBalances(wrappedKey, true, address(manager)); vm.prank(alice); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalances(wrappedKey, true, address(manager)); @@ -214,7 +225,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { abi.encodeWithSelector(HookCallFailed.selector) ) ); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_swap_succeeds_authorized_user_mixed_entry() public { @@ -242,7 +253,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = getInputAndOutputBalances(key1, zeroToOne, address(manager)); vm.prank(alice); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalances(key1, zeroToOne, address(manager)); @@ -278,7 +289,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = getInputAndOutputBalances(key1, zeroToOne, address(manager)); vm.prank(alice); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalances(key1, zeroToOne, address(manager)); @@ -324,7 +335,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { abi.encodeWithSelector(HookCallFailed.selector) ) ); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_swap_unauthorized_user_mixed_exit_reverts() public { @@ -361,7 +372,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { abi.encodeWithSelector(HookCallFailed.selector) ) ); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_permission_flag_none_swap_reverts() public { @@ -380,7 +391,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.prank(alice); vm.expectRevert(); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_permission_flag_swap_allowed_succeeds() public { @@ -401,7 +412,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); vm.prank(alice); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_permission_flag_liquidity_allowed_swap_reverts() public { @@ -423,7 +434,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.prank(alice); vm.expectRevert(); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_permission_flag_all_allowed_swap_succeeds() public { @@ -444,7 +455,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); vm.prank(alice); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_permission_flag_combinations_swap() public { @@ -470,7 +481,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { ); vm.prank(alice); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_permission_flag_partial_permissions_swap() public { @@ -492,7 +503,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.prank(alice); vm.expectRevert(); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_permission_flag_dynamic_changes_swap() public { @@ -514,7 +525,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { // Should succeed initially vm.prank(alice); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); // Remove permissions MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.NONE); @@ -523,7 +534,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { // Should fail on subsequent operations vm.prank(alice); vm.expectRevert(); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_permission_flag_edge_cases_swap() public { @@ -546,14 +557,14 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.prank(alice); vm.expectRevert(); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); // Test with maximum permissions (should be same as ALL_ALLOWED) MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlag.wrap(0xFFFF)); MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlag.wrap(0xFFFF)); vm.prank(alice); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_permission_flag_all_pool_types_swap() public { @@ -583,7 +594,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.finalizeSwap(poolKey.currency0, poolKey.currency1, ActionConstants.MSG_SENDER); vm.prank(alice); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_permission_flag_exact_output_swap() public { @@ -605,7 +616,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); vm.prank(alice); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_permission_flag_multi_hop_swap() public { @@ -638,7 +649,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); vm.prank(alice); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } /*////////////////////////////////////////////////////////////// @@ -658,7 +669,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.expectRevert( abi.encodeWithSelector(IV4Router.V4TooLittleReceived.selector, expectedAmountOut + 1, expectedAmountOut) ); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_swapExactInputSingle_zeroForOne_takeToMsgSender() public { @@ -759,7 +770,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { assertEq(inputCurrency.balanceOf(address(permissionedRouter)), 0); assertEq(outputCurrency.balanceOf(address(permissionedRouter)), 0); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); // the output tokens have been left in the router assertEq(outputCurrency.balanceOf(address(permissionedRouter)), expectedAmountOut); assertEq(inputCurrency.balanceOf(address(permissionedRouter)), 0); @@ -811,7 +822,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.expectRevert( abi.encodeWithSelector(IV4Router.V4TooLittleReceived.selector, expectedAmountOut + 1, expectedAmountOut) ); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_swapExactIn_1Hop_zeroForOne() public { @@ -1165,7 +1176,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 ethBalanceBefore) = getInputAndOutputBalances(nativeKey, zeroForOne, address(manager)); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); (uint256 inputBalanceAfter, uint256 outputBalanceAfter, uint256 ethBalanceAfter) = getInputAndOutputBalances(nativeKey, zeroForOne, address(manager)); @@ -1194,7 +1205,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IV4Router.V4TooMuchRequested.selector, expectedAmountIn - 1, 369070324193623892281288 ) ); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_swapExactOutputSingle_zeroForOne() public { @@ -1276,7 +1287,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.expectRevert( abi.encodeWithSelector(IV4Router.V4TooMuchRequested.selector, expectedAmountIn - 1, expectedAmountIn) ); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_swapExactOut_1Hop_zeroForOne() public { @@ -1475,7 +1486,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { abi.encodeWithSelector(HookCallFailed.selector) ) ); - permissionedRouter.execute{value: 0}(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } function test_swapExactOut_native_3Hops_permissioned_middle() public { @@ -1544,7 +1555,9 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { abi.encodeWithSelector(HookCallFailed.selector) ) ); - permissionedRouter.execute{value: (nativeKey.currency0.isAddressZero()) ? expectedAmountIn : 0}(data); + permissionedRouter.execute{value: (nativeKey.currency0.isAddressZero()) ? expectedAmountIn : 0}( + COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max + ); } function test_swapExactOutputSingle_swapOpenDelta() public { @@ -1563,7 +1576,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = getInputAndOutputBalances(key0, true, address(manager)); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalances(key0, true, address(manager)); @@ -1590,7 +1603,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = getInputAndOutputBalancesPath(tokenPath, address(manager)); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalancesPath(tokenPath, address(manager)); @@ -1684,7 +1697,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { getInputAndOutputBalances(key2, true, address(manager)); // Execute the swap - this should trigger the _pay function with payer == address(this) and permissionedToken == address(0) - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalances(key2, true, address(manager)); @@ -1713,7 +1726,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { getInputAndOutputBalances(key0, true, address(manager)); // Execute the swap - this should trigger the _pay function with payer == address(this) and permissionedToken != address(0) - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalances(key0, true, address(manager)); @@ -1750,7 +1763,64 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.prank(unauthorizedUser); vm.expectRevert(Unauthorized.selector); - permissionedRouter.execute(data); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); + } + + function test_command_permit2_permit() public { + IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({ + details: IAllowanceTransfer.PermitDetails({ + token: Currency.unwrap(currency0), + amount: 1e18, + expiration: uint48(block.timestamp + 3600), + nonce: 0 + }), + spender: address(permissionedRouter), + sigDeadline: block.timestamp + 3600 + }); + + uint256 testKey = uint256(0x22); + address testSigner = vm.addr(testKey); + + bytes32 digest = generatePermitDigest(permitSingle); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(testKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory data = abi.encode(permitSingle, signature); + + vm.prank(testSigner); + permissionedRouter.execute(COMMAND_PERMIT2_PERMIT, toBytesArray(data), type(uint256).max); + } + + function test_invalid_command_reverts() public { + bytes memory data = hex"1234567890"; + vm.expectRevert(abi.encodeWithSelector(InvalidCommandType.selector, uint8(0x07))); + permissionedRouter.execute(hex"07", toBytesArray(data), type(uint256).max); + } + + function test_failed_call_reverts() public { + IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({ + details: IAllowanceTransfer.PermitDetails({ + token: Currency.unwrap(currency0), + amount: 1e18, + expiration: uint48(block.timestamp + 3600), + nonce: 0 + }), + spender: address(permissionedRouter), + sigDeadline: block.timestamp + 3600 + }); + uint256 testKey = uint256(0x22); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(testKey, hex"aa"); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory data = abi.encode(permitSingle, signature); + + vm.expectRevert(abi.encodeWithSelector(ExecutionFailed.selector, 0, hex"")); + permissionedRouter.execute(COMMAND_PERMIT2_PERMIT, toBytesArray(data), type(uint256).max); + } + + function test_slice_out_of_bounds_reverts() public { + bytes memory data = hex"1234567890"; + vm.expectRevert(SliceOutOfBounds.selector); + permissionedRouter.execute(COMMAND_PERMIT2_PERMIT, toBytesArray(data), type(uint256).max); } function test_hooks() public { diff --git a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol index 285cdd0e4..5e2a0186d 100644 --- a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol @@ -282,7 +282,8 @@ contract PermissionedDeployers is Test { uint256 value = isNativeInput ? uint256(-amountSpecified) : 0; bytes memory data = getSwapData(_key, uint256(-amountSpecified), zeroForOne); - permissionedSwapRouter.execute{value: value}(data); + bytes memory command = hex"10"; + permissionedSwapRouter.execute{value: value}(command, toBytesArray(data), type(uint256).max); } function getSwapData(PoolKey memory poolKey, uint256 amountIn, bool zeroForOne) @@ -314,4 +315,10 @@ contract PermissionedDeployers is Test { return data; } + + function toBytesArray(bytes memory data) internal pure returns (bytes[] memory) { + bytes[] memory result = new bytes[](1); + result[0] = data; + return result; + } } diff --git a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol index 8eac58f30..1907a5ac7 100644 --- a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol @@ -188,6 +188,37 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 modifyLiquidity(_key, ModifyLiquidityParams(-887220, 887220, 200 ether, 0), "0x", 200 ether); } + function generatePermitDigest(IAllowanceTransfer.PermitSingle memory permitSingle) + internal + view + returns (bytes32) + { + bytes32 domainSeparator = permit2.DOMAIN_SEPARATOR(); + + bytes32 PERMIT_SINGLE_TYPEHASH = keccak256( + "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" + ); + + // Hash the permit details + bytes32 permitDetailsHash = keccak256( + abi.encode( + keccak256("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"), + permitSingle.details.token, + permitSingle.details.amount, + permitSingle.details.expiration, + permitSingle.details.nonce + ) + ); + + // Hash the permit single data + bytes32 permitSingleHash = keccak256( + abi.encode(PERMIT_SINGLE_TYPEHASH, permitDetailsHash, permitSingle.spender, permitSingle.sigDeadline) + ); + + // Create the final digest + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, permitSingleHash)); + } + function _getExactInputParams(Currency[] memory _tokenPath, uint256 amountIn) internal pure @@ -293,10 +324,10 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 outputBalanceBefore = getPermissionedCurrency(outputCurrency).balanceOfSelf(); bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, takeRecipient); - uint256 value = (inputCurrency.isAddressZero()) ? amountIn : 0; + bytes memory command = hex"10"; - permissionedRouter.execute{value: value}(data); + permissionedRouter.execute{value: value}(command, toBytesArray(data), type(uint256).max); inputBalanceAfter = getPermissionedCurrency(inputCurrency).balanceOfSelf(); outputBalanceAfter = getPermissionedCurrency(outputCurrency).balanceOfSelf(); From 3fc39d1ca56059f9d0a56e2b065c48aa309ceec1 Mon Sep 17 00:00:00 2001 From: Daniel Gretzke Date: Tue, 22 Jul 2025 22:21:16 +0200 Subject: [PATCH 10/33] Permissioned Pools: Ability to pause swapping (#485) * Add check whether swapping is enabled * Gas snapshot * Add natspec to event --- snapshots/PermissionedV4RouterTest.json | 2 +- .../permissionedPools/PermissionedHooks.sol | 8 +-- .../WrappedPermissionedToken.sol | 13 +++++ .../interfaces/IWrappedPermissionedToken.sol | 21 ++++++-- .../PermissionedPositionManager.t.sol | 2 + .../PermissionedV4Router.t.sol | 51 ++++++++++++++++++- .../WrappedPermissionedToken.t.sol | 10 ++++ .../shared/PermissionedPosmTestSetup.sol | 12 ++--- 8 files changed, 101 insertions(+), 18 deletions(-) diff --git a/snapshots/PermissionedV4RouterTest.json b/snapshots/PermissionedV4RouterTest.json index 73a44ea80..594c781d9 100644 --- a/snapshots/PermissionedV4RouterTest.json +++ b/snapshots/PermissionedV4RouterTest.json @@ -1,3 +1,3 @@ { - "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "225905" + "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "228021" } \ No newline at end of file diff --git a/src/hooks/permissionedPools/PermissionedHooks.sol b/src/hooks/permissionedPools/PermissionedHooks.sol index 7c4fbbfac..d56954473 100644 --- a/src/hooks/permissionedPools/PermissionedHooks.sol +++ b/src/hooks/permissionedPools/PermissionedHooks.sol @@ -19,6 +19,7 @@ contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; error Unauthorized(); + error SwappingDisabled(); constructor(IPoolManager manager, IWrappedPermissionedTokenFactory wrappedTokenFactory) BaseHook(manager) { WRAPPED_TOKEN_FACTORY = wrappedTokenFactory; @@ -60,7 +61,7 @@ contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { _isAllowed(Currency.unwrap(poolKey.currency1), sender.msgSender(), address(sender), selector); } - /// @dev checks if the provided token is a wrapped token by checking if it has a verified permissioned token, if yes, check the allowlist + /// @dev checks if the provided token is a wrapped token by checking if it has a verified permissioned token, if yes, check the allowlist and check whether swapping is enabled function _isAllowed(address wrappedToken, address sender, address router, bytes4 selector) internal view { address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(wrappedToken); if (permissionedToken == address(0)) return; @@ -68,6 +69,7 @@ contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { PermissionFlag permission = PermissionFlags.NONE; if (selector == this.beforeSwap.selector) { permission = PermissionFlags.SWAP_ALLOWED; + if (!IWrappedPermissionedToken(wrappedToken).swappingEnabled()) revert SwappingDisabled(); } else if (selector == this.beforeAddLiquidity.selector) { permission = PermissionFlags.LIQUIDITY_ALLOWED; } @@ -75,8 +77,6 @@ contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { if ( !IWrappedPermissionedToken(wrappedToken).isAllowed(sender, permission) || !IWrappedPermissionedToken(wrappedToken).allowedWrappers(router) - ) { - revert Unauthorized(); - } + ) revert Unauthorized(); } } diff --git a/src/hooks/permissionedPools/WrappedPermissionedToken.sol b/src/hooks/permissionedPools/WrappedPermissionedToken.sol index 1eb23851d..8679b6d52 100644 --- a/src/hooks/permissionedPools/WrappedPermissionedToken.sol +++ b/src/hooks/permissionedPools/WrappedPermissionedToken.sol @@ -18,6 +18,9 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo /// @inheritdoc IWrappedPermissionedToken IAllowlistChecker public allowListChecker; + /// @inheritdoc IWrappedPermissionedToken + bool public swappingEnabled; + /// @inheritdoc IWrappedPermissionedToken mapping(address wrapper => bool) public allowedWrappers; @@ -50,6 +53,11 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo _updateAllowedWrapper(wrapper, allowed); } + /// @inheritdoc IWrappedPermissionedToken + function updateSwappingEnabled(bool enabled) external onlyOwner { + _updateSwappingEnabled(enabled); + } + /// @inheritdoc IWrappedPermissionedToken function isAllowed(address account, PermissionFlag permission) public view returns (bool) { return ((allowListChecker.checkAllowlist(account)) & (permission)) == (permission); @@ -68,6 +76,11 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo emit AllowedWrapperUpdated(wrapper, allowed); } + function _updateSwappingEnabled(bool enabled) internal { + swappingEnabled = enabled; + emit SwappingEnabledUpdated(enabled); + } + /// @dev Overrides the ERC20._update function to add the following checks and logic: /// - Before `settle` is called on the pool manager, the token is wrapped and minted to the pool manager /// - When `take` is called on the pool manager, the token is automatically unwrapped when the pool manager transfers the token to the recipient diff --git a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol index 3d7b64193..00158d0e9 100644 --- a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol +++ b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol @@ -12,6 +12,9 @@ interface IWrappedPermissionedToken is IERC20 { /// @notice Emitted when an allowed wrapper is updated event AllowedWrapperUpdated(address indexed wrapper, bool allowed); + /// @notice Emitted when the swapping enabled status is updated + event SwappingEnabledUpdated(bool enabled); + /// @notice Thrown when the allow list checker does not implement the IAllowListChecker interface error InvalidAllowListChecker(IAllowlistChecker newAllowListChecker); @@ -24,17 +27,17 @@ interface IWrappedPermissionedToken is IERC20 { /// @notice Thrown when there is an insufficient amount of permissioned tokens available to wrap error InsufficientBalance(uint256 amount, uint256 availableBalance); - /// @notice Updates the allow list checker - /// @param newAllowListChecker The new allow list checker - /// @dev Only callable by the owner - function updateAllowListChecker(IAllowlistChecker newAllowListChecker) external; - /// @notice Wraps the permissioned token to the pool manager /// @param amount The amount of permissioned tokens to wrap /// @dev Only callable by allowed wrappers /// @dev The `amount` must be sent to this contract before calling this function function wrapToPoolManager(uint256 amount) external; + /// @notice Updates the allow list checker + /// @param newAllowListChecker The new allow list checker + /// @dev Only callable by the owner + function updateAllowListChecker(IAllowlistChecker newAllowListChecker) external; + /// @notice Updates the allowed wrapper that can wrap the permissioned token /// @param wrapper The wrapper to update /// @param allowed Whether the wrapper is allowed @@ -42,6 +45,11 @@ interface IWrappedPermissionedToken is IERC20 { /// @dev To ensure the wrapped token cannot be wrapped in an ERC6909 token on the PoolManager, the wrapper must only implement `swap` or `modifyLiquidity` functions function updateAllowedWrapper(address wrapper, bool allowed) external; + /// @notice Updates the swapping enabled status + /// @param enabled Whether swapping is enabled + /// @dev Only callable by the owner + function updateSwappingEnabled(bool enabled) external; + /// @notice Returns whether a transfer is allowed /// @param account The account to check function isAllowed(address account, PermissionFlag permission) external view returns (bool); @@ -53,6 +61,9 @@ interface IWrappedPermissionedToken is IERC20 { /// @dev e.g., the permissioned pool manager, quoters or the swap router function allowedWrappers(address wrapper) external view returns (bool); + /// @notice Returns the swapping enabled status + function swappingEnabled() external view returns (bool); + /// @notice Returns the Uniswap v4 pool manager function POOL_MANAGER() external view returns (address); diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 49c17e41e..91886dccd 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -85,6 +85,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq // set up approvals for alice seedBalance(alice); approvePosmFor(alice); + wrappedToken0.updateSwappingEnabled(true); + wrappedToken2.updateSwappingEnabled(true); } function setUpPosms() internal { diff --git a/test/hooks/permissionedPools/PermissionedV4Router.t.sol b/test/hooks/permissionedPools/PermissionedV4Router.t.sol index e140a3e06..706955ce7 100644 --- a/test/hooks/permissionedPools/PermissionedV4Router.t.sol +++ b/test/hooks/permissionedPools/PermissionedV4Router.t.sol @@ -6,8 +6,8 @@ import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/Pool import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; +import {Hooks, IHooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; import {PermitHash} from "permit2/src/libraries/PermitHash.sol"; import {IV4Router} from "../../../src/interfaces/IV4Router.sol"; @@ -46,6 +46,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { wrappedCurrency0 = Currency.wrap(address(wrappedToken0)); wrappedCurrency1 = Currency.wrap(address(wrappedToken1)); + wrappedToken0.updateSwappingEnabled(true); + wrappedToken1.updateSwappingEnabled(true); plan = Planner.init(); @@ -157,7 +159,52 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } - function test_swap_authorized_user() public { + error SwappingDisabled(); + + function test_swap_revert_trading_disabled() public { + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + + wrappedToken0.updateSwappingEnabled(false); + + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeSwap.selector, + abi.encodeWithSelector(SwappingDisabled.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) + ) + ); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); + + wrappedToken0.updateSwappingEnabled(true); + wrappedToken1.updateSwappingEnabled(false); + + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(permissionedHooks), + IHooks.beforeSwap.selector, + abi.encodeWithSelector(SwappingDisabled.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) + ) + ); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); + } + + function test_swap_succeeds_authorized_user() public { IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); diff --git a/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol b/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol index f27162716..07e176e85 100644 --- a/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol +++ b/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol @@ -48,6 +48,8 @@ contract WrappedPermissionedTokenTest is PermissionedPoolsBase { wrappedPermissionedToken.updateAllowedWrapper(account, true); vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, account)); wrappedPermissionedToken.updateAllowListChecker(allowlistChecker); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, account)); + wrappedPermissionedToken.updateSwappingEnabled(true); vm.stopPrank(); } @@ -116,6 +118,14 @@ contract WrappedPermissionedTokenTest is PermissionedPoolsBase { assertEq(address(wrappedPermissionedToken.allowListChecker()), address(newAllowListChecker)); } + function test_UpdateSwappingEnabled(bool enabled) public { + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit IWrappedPermissionedToken.SwappingEnabledUpdated(enabled); + wrappedPermissionedToken.updateSwappingEnabled(enabled); + assertEq(wrappedPermissionedToken.swappingEnabled(), enabled); + } + function testRevert_WhenInvalidTransfer(address from, address to) public { vm.assume(from != address(0) && from != mockPoolManager); vm.assume(to != address(0) && to != mockPoolManager); diff --git a/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol b/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol index 124e89ce4..51998501e 100644 --- a/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol +++ b/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol @@ -53,12 +53,12 @@ contract PermissionedPosmTestSetup is Test, PermissionedDeployers, DeployPermit2 mapping(Currency => Currency) public wrappedToPermissioned; - function deployAndApprovePosm(IPoolManager poolManager, address wrappedTokenFactory, bytes32 salt) public { - deployPermissionedPosm(poolManager, wrappedTokenFactory, salt); + function deployAndApprovePosm(IPoolManager poolManager, address wrappedTokenFactory_, bytes32 salt) public { + deployPermissionedPosm(poolManager, wrappedTokenFactory_, salt); approvePosm(); } - function deployPermissionedPosm(IPoolManager poolManager, address wrappedTokenFactory, bytes32 salt) internal { + function deployPermissionedPosm(IPoolManager poolManager, address wrappedTokenFactory_, bytes32 salt) internal { permit2 = IAllowanceTransfer(deployPermit2()); _WETH9 = deployWETH(); proxyAsImplementation = deployDescriptor(poolManager, "ETH"); @@ -68,12 +68,12 @@ contract PermissionedPosmTestSetup is Test, PermissionedDeployers, DeployPermit2 100_000, address(proxyAsImplementation), address(_WETH9), - wrappedTokenFactory, + wrappedTokenFactory_, abi.encode(salt) ); } - function deployAndApprovePosmOnly(IPoolManager poolManager, address wrappedTokenFactory, bytes32 salt) + function deployAndApprovePosmOnly(IPoolManager poolManager, address wrappedTokenFactory_, bytes32 salt) public returns (IPositionManager secondaryPosm) { @@ -83,7 +83,7 @@ contract PermissionedPosmTestSetup is Test, PermissionedDeployers, DeployPermit2 100_000, address(proxyAsImplementation), address(_WETH9), - wrappedTokenFactory, + wrappedTokenFactory_, abi.encode(salt) ); approvePosm(); From 5ad787723cdb5322da2c2cd7040a7ee7b0c945b6 Mon Sep 17 00:00:00 2001 From: Daniel Gretzke Date: Wed, 23 Jul 2025 17:19:20 +0200 Subject: [PATCH 11/33] Rename to permissions adapter (#489) --- snapshots/PermissionedV4RouterTest.json | 2 +- .../permissionedPools/PermissionedHooks.sol | 24 +- .../PermissionedPositionManager.sol | 42 ++- .../PermissionedV4Router.sol | 28 +- ...sionedToken.sol => PermissionsAdapter.sol} | 42 +-- .../PermissionsAdapterFactory.sol | 45 +++ .../WrappedPermissionedTokenFactory.sol | 43 --- ...ionedToken.sol => IPermissionsAdapter.sol} | 10 +- .../interfaces/IPermissionsAdapterFactory.sol | 55 ++++ .../IWrappedPermissionedTokenFactory.sol | 52 --- .../PermissionedPositionManager.t.sol | 125 ++++---- .../PermissionedV4Router.t.sol | 303 ++++++++++-------- ...edToken.t.sol => PermissionsAdapter.t.sol} | 101 +++--- .../PermissionsAdapterFactory.t.sol | 91 ++++++ .../WrappedPermissionedTokenFactory.t.sol | 99 ------ .../permissionedPools/mocks/MockHooks.sol | 8 +- .../shared/PermissionedDeployers.sol | 31 +- .../shared/PermissionedPosmTestSetup.sol | 18 +- .../shared/PermissionedRoutingTestHelpers.sol | 104 +++--- test/shared/Deploy.sol | 4 +- 20 files changed, 627 insertions(+), 600 deletions(-) rename src/hooks/permissionedPools/{WrappedPermissionedToken.sol => PermissionsAdapter.sol} (75%) create mode 100644 src/hooks/permissionedPools/PermissionsAdapterFactory.sol delete mode 100644 src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol rename src/hooks/permissionedPools/interfaces/{IWrappedPermissionedToken.sol => IPermissionsAdapter.sol} (88%) create mode 100644 src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol delete mode 100644 src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol rename test/hooks/permissionedPools/{WrappedPermissionedToken.t.sol => PermissionsAdapter.t.sol} (51%) create mode 100644 test/hooks/permissionedPools/PermissionsAdapterFactory.t.sol delete mode 100644 test/hooks/permissionedPools/WrappedPermissionedTokenFactory.t.sol diff --git a/snapshots/PermissionedV4RouterTest.json b/snapshots/PermissionedV4RouterTest.json index 594c781d9..9bcfabe17 100644 --- a/snapshots/PermissionedV4RouterTest.json +++ b/snapshots/PermissionedV4RouterTest.json @@ -1,3 +1,3 @@ { - "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "228021" + "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "227931" } \ No newline at end of file diff --git a/src/hooks/permissionedPools/PermissionedHooks.sol b/src/hooks/permissionedPools/PermissionedHooks.sol index d56954473..06175abe0 100644 --- a/src/hooks/permissionedPools/PermissionedHooks.sol +++ b/src/hooks/permissionedPools/PermissionedHooks.sol @@ -2,10 +2,8 @@ pragma solidity ^0.8.0; import {IPoolManager, Currency} from "../../V4Router.sol"; -import { - IWrappedPermissionedTokenFactory, - IWrappedPermissionedToken -} from "./interfaces/IWrappedPermissionedTokenFactory.sol"; +import {IPermissionsAdapter} from "./interfaces/IPermissionsAdapter.sol"; +import {IPermissionsAdapterFactory} from "./interfaces/IPermissionsAdapterFactory.sol"; import {IMsgSender} from "../../interfaces/IMsgSender.sol"; import {ReentrancyLock} from "../../base/ReentrancyLock.sol"; import {Hooks, IHooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; @@ -16,13 +14,13 @@ import {BaseHook} from "../../utils/BaseHook.sol"; import {PermissionFlags, PermissionFlag} from "./libraries/PermissionFlags.sol"; contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { - IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; + IPermissionsAdapterFactory public immutable PERMISSIONS_ADAPTER_FACTORY; error Unauthorized(); error SwappingDisabled(); - constructor(IPoolManager manager, IWrappedPermissionedTokenFactory wrappedTokenFactory) BaseHook(manager) { - WRAPPED_TOKEN_FACTORY = wrappedTokenFactory; + constructor(IPoolManager manager, IPermissionsAdapterFactory permissionsAdapterFactory) BaseHook(manager) { + PERMISSIONS_ADAPTER_FACTORY = permissionsAdapterFactory; Hooks.validateHookPermissions(this, getHookPermissions()); } @@ -61,22 +59,22 @@ contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { _isAllowed(Currency.unwrap(poolKey.currency1), sender.msgSender(), address(sender), selector); } - /// @dev checks if the provided token is a wrapped token by checking if it has a verified permissioned token, if yes, check the allowlist and check whether swapping is enabled - function _isAllowed(address wrappedToken, address sender, address router, bytes4 selector) internal view { - address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(wrappedToken); + /// @dev checks if the provided token is a permissioned token by checking if it has a verified permissions adapter, if yes, check the allowlist and check whether swapping is enabled + function _isAllowed(address pemissionsAdapter, address sender, address router, bytes4 selector) internal view { + address permissionedToken = PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(pemissionsAdapter); if (permissionedToken == address(0)) return; PermissionFlag permission = PermissionFlags.NONE; if (selector == this.beforeSwap.selector) { permission = PermissionFlags.SWAP_ALLOWED; - if (!IWrappedPermissionedToken(wrappedToken).swappingEnabled()) revert SwappingDisabled(); + if (!IPermissionsAdapter(pemissionsAdapter).swappingEnabled()) revert SwappingDisabled(); } else if (selector == this.beforeAddLiquidity.selector) { permission = PermissionFlags.LIQUIDITY_ALLOWED; } if ( - !IWrappedPermissionedToken(wrappedToken).isAllowed(sender, permission) - || !IWrappedPermissionedToken(wrappedToken).allowedWrappers(router) + !IPermissionsAdapter(pemissionsAdapter).isAllowed(sender, permission) + || !IPermissionsAdapter(pemissionsAdapter).allowedWrappers(router) ) revert Unauthorized(); } } diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index 7b32fb42b..6973eec94 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -10,21 +10,19 @@ import { IWETH9, Currency } from "../../PositionManager.sol"; -import { - IWrappedPermissionedTokenFactory, - IWrappedPermissionedToken -} from "./interfaces/IWrappedPermissionedTokenFactory.sol"; +import {IPermissionsAdapter} from "./interfaces/IPermissionsAdapter.sol"; +import {IPermissionsAdapterFactory} from "./interfaces/IPermissionsAdapterFactory.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {PermissionFlags} from "./libraries/PermissionFlags.sol"; contract PermissionedPositionManager is PositionManager { - IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; + IPermissionsAdapterFactory public immutable PERMISSIONS_ADAPTER_FACTORY; mapping(Currency currency => mapping(IHooks hooks => bool)) public isAllowedHooks; error InvalidHook(); error SafeTransferDisabled(); - error NotWrappedAdmin(); + error NotPermissionsAdapterAdmin(); /// @dev as this contract must know the hooks address in advance, it must be passed in as a constructor argument constructor( @@ -33,19 +31,19 @@ contract PermissionedPositionManager is PositionManager { uint256 _unsubscribeGasLimit, IPositionDescriptor _tokenDescriptor, IWETH9 _weth9, - IWrappedPermissionedTokenFactory _wrappedTokenFactory + IPermissionsAdapterFactory _permissionsAdapterFactory ) PositionManager(_poolManager, _permit2, _unsubscribeGasLimit, _tokenDescriptor, _weth9) { - WRAPPED_TOKEN_FACTORY = _wrappedTokenFactory; + PERMISSIONS_ADAPTER_FACTORY = _permissionsAdapterFactory; } - /// @notice Sets the allowed hook for a given wrapped permissioned token - /// @dev Sets which hooks are allowed to be used with a wrapped permissioned token. Only callable by the owner of the wrapped permissioned token - /// @param currency The currency of the wrapped permissioned token + /// @notice Sets the allowed hook for a given permissions adapter + /// @dev Sets which hooks are allowed to be used with a permissions adapter. Only callable by the owner of the permissions adapter + /// @param currency The currency of the permissions adapter /// @param hooks The hook to set the allowance for - /// @param allowed Whether the hook is allowed to be used with the wrapped permissioned token + /// @param allowed Whether the hook is allowed to be used with the permissions adapter function setAllowedHook(Currency currency, IHooks hooks, bool allowed) external { if (_getOwner(currency) != msg.sender) { - revert NotWrappedAdmin(); + revert NotPermissionsAdapterAdmin(); } isAllowedHooks[currency][hooks] = allowed; } @@ -107,29 +105,29 @@ contract PermissionedPositionManager is PositionManager { return; } // token is permissioned, wrap the token and transfer it to the pool manager - IWrappedPermissionedToken wrappedPermissionedToken = IWrappedPermissionedToken(Currency.unwrap(currency)); + IPermissionsAdapter pemissionsAdapter = IPermissionsAdapter(Currency.unwrap(currency)); if (payer == address(this)) { // @audit is it necessary to check the allowlist here? - if (!wrappedPermissionedToken.isAllowed(msgSender(), PermissionFlags.LIQUIDITY_ALLOWED)) { + if (!pemissionsAdapter.isAllowed(msgSender(), PermissionFlags.LIQUIDITY_ALLOWED)) { revert Unauthorized(); } - Currency.wrap(permissionedToken).transfer(address(wrappedPermissionedToken), amount); - wrappedPermissionedToken.wrapToPoolManager(amount); + Currency.wrap(permissionedToken).transfer(address(pemissionsAdapter), amount); + pemissionsAdapter.wrapToPoolManager(amount); } else { // token is a permissioned token, wrap the token - permit2.transferFrom(payer, address(wrappedPermissionedToken), uint160(amount), permissionedToken); - wrappedPermissionedToken.wrapToPoolManager(amount); + permit2.transferFrom(payer, address(pemissionsAdapter), uint160(amount), permissionedToken); + pemissionsAdapter.wrapToPoolManager(amount); } } function _verifiedPermissionedTokenOf(Currency currency) internal view returns (address) { - return WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(currency)); + return PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(currency)); } function _getOwner(Currency currency) internal view returns (address) { - address wrappedPermissionedToken = Currency.unwrap(currency); + address pemissionsAdapter = Currency.unwrap(currency); address permissionedToken = _verifiedPermissionedTokenOf(currency); if (permissionedToken == address(0)) return address(0); - return IWrappedPermissionedToken(wrappedPermissionedToken).owner(); + return IPermissionsAdapter(pemissionsAdapter).owner(); } } diff --git a/src/hooks/permissionedPools/PermissionedV4Router.sol b/src/hooks/permissionedPools/PermissionedV4Router.sol index a32069f79..c755f06b7 100644 --- a/src/hooks/permissionedPools/PermissionedV4Router.sol +++ b/src/hooks/permissionedPools/PermissionedV4Router.sol @@ -4,17 +4,15 @@ pragma solidity ^0.8.0; import {ActionConstants} from "../../libraries/ActionConstants.sol"; import {ReentrancyLock} from "../../base/ReentrancyLock.sol"; import {V4Router, IPoolManager, Currency} from "../../V4Router.sol"; -import { - IWrappedPermissionedTokenFactory, - IWrappedPermissionedToken -} from "./interfaces/IWrappedPermissionedTokenFactory.sol"; +import {IPermissionsAdapter} from "./interfaces/IPermissionsAdapter.sol"; +import {IPermissionsAdapterFactory} from "./interfaces/IPermissionsAdapterFactory.sol"; import {IWETH9} from "../../interfaces/external/IWETH9.sol"; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; import {PermissionFlags} from "./libraries/PermissionFlags.sol"; contract PermissionedV4Router is V4Router, ReentrancyLock { IAllowanceTransfer public immutable PERMIT2; - IWrappedPermissionedTokenFactory public immutable WRAPPED_TOKEN_FACTORY; + IPermissionsAdapterFactory public immutable PERMISSIONS_ADAPTER_FACTORY; IWETH9 public immutable WETH9; error Unauthorized(); @@ -36,11 +34,11 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { constructor( IPoolManager poolManager_, IAllowanceTransfer permit2, - IWrappedPermissionedTokenFactory wrappedTokenFactory, + IPermissionsAdapterFactory permissionsAdapterFactory, IWETH9 weth9 ) V4Router(poolManager_) { PERMIT2 = permit2; - WRAPPED_TOKEN_FACTORY = wrappedTokenFactory; + PERMISSIONS_ADAPTER_FACTORY = permissionsAdapterFactory; WETH9 = weth9; } @@ -127,7 +125,7 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { } function _pay(Currency currency, address payer, uint256 amount) internal override { - address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(currency)); + address permissionedToken = PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(currency)); if (permissionedToken == address(0)) { // token is not a permissioned token, use the default implementation if (payer == address(this)) { @@ -139,24 +137,24 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { return; } // token is permissioned, wrap the token and transfer it to the pool manager - IWrappedPermissionedToken wrappedPermissionedToken = IWrappedPermissionedToken(Currency.unwrap(currency)); + IPermissionsAdapter pemissionsAdapter = IPermissionsAdapter(Currency.unwrap(currency)); if (payer == address(this)) { // allowlist check necessary to ensure a disallowed user cannot sell a permissioned token - if (!wrappedPermissionedToken.isAllowed(msgSender(), PermissionFlags.SWAP_ALLOWED)) { + if (!pemissionsAdapter.isAllowed(msgSender(), PermissionFlags.SWAP_ALLOWED)) { revert Unauthorized(); } - Currency.wrap(permissionedToken).transfer(address(wrappedPermissionedToken), amount); - wrappedPermissionedToken.wrapToPoolManager(amount); + Currency.wrap(permissionedToken).transfer(address(pemissionsAdapter), amount); + pemissionsAdapter.wrapToPoolManager(amount); } else { // token is a permissioned token, wrap the token - PERMIT2.transferFrom(payer, address(wrappedPermissionedToken), uint160(amount), permissionedToken); - wrappedPermissionedToken.wrapToPoolManager(amount); + PERMIT2.transferFrom(payer, address(pemissionsAdapter), uint160(amount), permissionedToken); + pemissionsAdapter.wrapToPoolManager(amount); } } /// @notice Calculates the amount for a settle action function _mapSettleAmount(uint256 amount, Currency currency) internal view override returns (uint256) { - address permissionedToken = WRAPPED_TOKEN_FACTORY.verifiedPermissionedTokenOf(Currency.unwrap(currency)); + address permissionedToken = PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(currency)); // use the default implementation unless the currency is a permissioned token with a balance on the router if (permissionedToken == address(0) || amount != ActionConstants.CONTRACT_BALANCE) { return super._mapSettleAmount(amount, currency); diff --git a/src/hooks/permissionedPools/WrappedPermissionedToken.sol b/src/hooks/permissionedPools/PermissionsAdapter.sol similarity index 75% rename from src/hooks/permissionedPools/WrappedPermissionedToken.sol rename to src/hooks/permissionedPools/PermissionsAdapter.sol index 8679b6d52..dc4facdb8 100644 --- a/src/hooks/permissionedPools/WrappedPermissionedToken.sol +++ b/src/hooks/permissionedPools/PermissionsAdapter.sol @@ -4,24 +4,24 @@ pragma solidity 0.8.26; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {IWrappedPermissionedToken} from "./interfaces/IWrappedPermissionedToken.sol"; +import {IPermissionsAdapter} from "./interfaces/IPermissionsAdapter.sol"; import {IAllowlistChecker} from "./interfaces/IAllowlistChecker.sol"; import {PermissionFlag} from "./libraries/PermissionFlags.sol"; -contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedToken { - /// @inheritdoc IWrappedPermissionedToken +contract PermissionsAdapter is ERC20, Ownable2Step, IPermissionsAdapter { + /// @inheritdoc IPermissionsAdapter address public immutable POOL_MANAGER; - /// @inheritdoc IWrappedPermissionedToken + /// @inheritdoc IPermissionsAdapter IERC20 public immutable PERMISSIONED_TOKEN; - /// @inheritdoc IWrappedPermissionedToken + /// @inheritdoc IPermissionsAdapter IAllowlistChecker public allowListChecker; - /// @inheritdoc IWrappedPermissionedToken + /// @inheritdoc IPermissionsAdapter bool public swappingEnabled; - /// @inheritdoc IWrappedPermissionedToken + /// @inheritdoc IPermissionsAdapter mapping(address wrapper => bool) public allowedWrappers; constructor( @@ -35,7 +35,7 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo _updateAllowListChecker(allowListChecker_); } - /// @inheritdoc IWrappedPermissionedToken + /// @inheritdoc IPermissionsAdapter function wrapToPoolManager(uint256 amount) external { if (!allowedWrappers[msg.sender]) revert UnauthorizedWrapper(msg.sender); uint256 availableBalance = PERMISSIONED_TOKEN.balanceOf(address(this)) - totalSupply(); @@ -43,22 +43,22 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo _mint(POOL_MANAGER, amount); } - /// @inheritdoc IWrappedPermissionedToken + /// @inheritdoc IPermissionsAdapter function updateAllowListChecker(IAllowlistChecker newAllowListChecker) external onlyOwner { _updateAllowListChecker(newAllowListChecker); } - /// @inheritdoc IWrappedPermissionedToken + /// @inheritdoc IPermissionsAdapter function updateAllowedWrapper(address wrapper, bool allowed) external onlyOwner { _updateAllowedWrapper(wrapper, allowed); } - /// @inheritdoc IWrappedPermissionedToken + /// @inheritdoc IPermissionsAdapter function updateSwappingEnabled(bool enabled) external onlyOwner { _updateSwappingEnabled(enabled); } - /// @inheritdoc IWrappedPermissionedToken + /// @inheritdoc IPermissionsAdapter function isAllowed(address account, PermissionFlag permission) public view returns (bool) { return ((allowListChecker.checkAllowlist(account)) & (permission)) == (permission); } @@ -82,9 +82,9 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo } /// @dev Overrides the ERC20._update function to add the following checks and logic: - /// - Before `settle` is called on the pool manager, the token is wrapped and minted to the pool manager - /// - When `take` is called on the pool manager, the token is automatically unwrapped when the pool manager transfers the token to the recipient - /// - Enforces that the pool manager is the only holder of the wrapped token + /// - Before `settle` is called on the pool manager, the permissioned token is deposited and the permissions adapter is minted to the pool manager + /// - When `take` is called on the pool manager, the permissioned token is automatically released when the pool manager transfers the permissions adapter to the recipient + /// - Enforces that the pool manager is the only holder of the permissions adapter function _update(address from, address to, uint256 amount) internal override { if (to == address(0)) { // prevents infinite loop when burning @@ -93,18 +93,18 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo } if (from == address(0)) { assert(to == POOL_MANAGER); - // token is being wrapped + // permissioned token is being deposited super._update(from, to, amount); return; } else if (from != POOL_MANAGER) { - // if the pool manager is the sender, the token is automatically unwrapped, skip the checks + // if the pool manager is the sender, the permissioned token is automatically released, skip the checks revert InvalidTransfer(from, to); } super._update(from, to, amount); if (from == POOL_MANAGER) { _unwrap(to, amount); } - // the pool manager must always be the only holder of the wrapped token + // the pool manager must always be the only holder of the permissions adapter assert(balanceOf(POOL_MANAGER) == totalSupply()); } @@ -114,18 +114,18 @@ contract WrappedPermissionedToken is ERC20, Ownable2Step, IWrappedPermissionedTo } function _getName(IERC20 permissionedToken) private view returns (string memory) { - return string.concat("Uniswap v4 Wrapped ", ERC20(address(permissionedToken)).name()); + return string.concat("Uniswap v4 ", ERC20(address(permissionedToken)).name()); } function _getSymbol(IERC20 permissionedToken) private view returns (string memory) { - return string.concat("uw", ERC20(address(permissionedToken)).symbol()); + return string.concat("v4", ERC20(address(permissionedToken)).symbol()); } function decimals() public view override returns (uint8) { return ERC20(address(PERMISSIONED_TOKEN)).decimals(); } - function owner() public view override(Ownable, IWrappedPermissionedToken) returns (address) { + function owner() public view override(Ownable, IPermissionsAdapter) returns (address) { return super.owner(); } } diff --git a/src/hooks/permissionedPools/PermissionsAdapterFactory.sol b/src/hooks/permissionedPools/PermissionsAdapterFactory.sol new file mode 100644 index 000000000..83c0a3942 --- /dev/null +++ b/src/hooks/permissionedPools/PermissionsAdapterFactory.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IPermissionsAdapter, IERC20} from "./interfaces/IPermissionsAdapter.sol"; +import {IAllowlistChecker} from "./interfaces/IAllowlistChecker.sol"; +import {PermissionsAdapter} from "./PermissionsAdapter.sol"; +import {IPermissionsAdapterFactory} from "./interfaces/IPermissionsAdapterFactory.sol"; + +contract PermissionsAdapterFactory is IPermissionsAdapterFactory { + address public immutable POOL_MANAGER; + + /// @inheritdoc IPermissionsAdapterFactory + mapping(address pemissionsAdapter => address permissionedToken) public permissionsAdapterOf; + /// @inheritdoc IPermissionsAdapterFactory + mapping(address pemissionsAdapter => address permissionedToken) public verifiedPermissionsAdapterOf; + + constructor(address poolManager) { + POOL_MANAGER = poolManager; + } + + /// @inheritdoc IPermissionsAdapterFactory + function createPermissionsAdapter( + IERC20 permissionedToken, + address initialOwner, + IAllowlistChecker allowListChecker + ) external returns (address pemissionsAdapter) { + pemissionsAdapter = + address(new PermissionsAdapter(permissionedToken, POOL_MANAGER, initialOwner, allowListChecker)); + permissionsAdapterOf[pemissionsAdapter] = address(permissionedToken); + emit PermissionsAdapterCreated(pemissionsAdapter, address(permissionedToken)); + } + + /// @inheritdoc IPermissionsAdapterFactory + function verifyPermissionsAdapter(address pemissionsAdapter) external { + IERC20 permissionedToken = IERC20(permissionsAdapterOf[pemissionsAdapter]); + if (address(permissionedToken) == address(0)) revert PemissionsAdapterNotFound(pemissionsAdapter); + if (verifiedPermissionsAdapterOf[pemissionsAdapter] != address(0)) { + revert PemissionsAdapterAlreadyVerified(pemissionsAdapter); + } + // this requires that the verifier has some comntrol or ownership of the permissioned token + if (permissionedToken.balanceOf(pemissionsAdapter) == 0) revert PemissionsAdapterNotVerified(pemissionsAdapter); + verifiedPermissionsAdapterOf[pemissionsAdapter] = address(permissionedToken); + emit PemissionsAdapterVerified(pemissionsAdapter, address(permissionedToken)); + } +} diff --git a/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol b/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol deleted file mode 100644 index 8a5568adb..000000000 --- a/src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {IWrappedPermissionedToken, IERC20} from "./interfaces/IWrappedPermissionedToken.sol"; -import {IAllowlistChecker} from "./interfaces/IAllowlistChecker.sol"; -import {WrappedPermissionedToken} from "./WrappedPermissionedToken.sol"; -import {IWrappedPermissionedTokenFactory} from "./interfaces/IWrappedPermissionedTokenFactory.sol"; - -contract WrappedPermissionedTokenFactory is IWrappedPermissionedTokenFactory { - address public immutable POOL_MANAGER; - - /// @inheritdoc IWrappedPermissionedTokenFactory - mapping(address wrappedToken => address permissionedToken) public permissionedTokenOf; - /// @inheritdoc IWrappedPermissionedTokenFactory - mapping(address wrappedToken => address permissionedToken) public verifiedPermissionedTokenOf; - - constructor(address poolManager) { - POOL_MANAGER = poolManager; - } - - /// @inheritdoc IWrappedPermissionedTokenFactory - function createWrappedPermissionedToken( - IERC20 permissionedToken, - address initialOwner, - IAllowlistChecker allowListChecker - ) external returns (address wrappedPermissionedToken) { - wrappedPermissionedToken = - address(new WrappedPermissionedToken(permissionedToken, POOL_MANAGER, initialOwner, allowListChecker)); - permissionedTokenOf[wrappedPermissionedToken] = address(permissionedToken); - emit WrappedPermissionedTokenCreated(wrappedPermissionedToken, address(permissionedToken)); - } - - /// @inheritdoc IWrappedPermissionedTokenFactory - function verifyWrappedToken(address wrappedToken) external { - IERC20 permissionedToken = IERC20(permissionedTokenOf[wrappedToken]); - if (address(permissionedToken) == address(0)) revert WrappedTokenNotFound(wrappedToken); - if (verifiedPermissionedTokenOf[wrappedToken] != address(0)) revert WrappedTokenAlreadyVerified(wrappedToken); - // this requires that the verifier has some comntrol or ownership of the permissioned token - if (permissionedToken.balanceOf(wrappedToken) == 0) revert WrappedTokenNotVerified(wrappedToken); - verifiedPermissionedTokenOf[wrappedToken] = address(permissionedToken); - emit WrappedTokenVerified(wrappedToken, address(permissionedToken)); - } -} diff --git a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol b/src/hooks/permissionedPools/interfaces/IPermissionsAdapter.sol similarity index 88% rename from src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol rename to src/hooks/permissionedPools/interfaces/IPermissionsAdapter.sol index 00158d0e9..2a057c135 100644 --- a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol +++ b/src/hooks/permissionedPools/interfaces/IPermissionsAdapter.sol @@ -5,7 +5,7 @@ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IAllowlistChecker, PermissionFlag} from "./IAllowlistChecker.sol"; -interface IWrappedPermissionedToken is IERC20 { +interface IPermissionsAdapter is IERC20 { /// @notice Emitted when the allow list checker is updated event AllowListCheckerUpdated(IAllowlistChecker indexed newAllowListChecker); @@ -21,7 +21,7 @@ interface IWrappedPermissionedToken is IERC20 { /// @notice Thrown when the transfer is not interacting with the pool manager error InvalidTransfer(address from, address to); - /// @notice Thrown when the wrapper is not allowed to trigger transfers on the wrapped token + /// @notice Thrown when the wrapper is not allowed to trigger transfers on the permissions adapter error UnauthorizedWrapper(address wrapper); /// @notice Thrown when there is an insufficient amount of permissioned tokens available to wrap @@ -42,7 +42,7 @@ interface IWrappedPermissionedToken is IERC20 { /// @param wrapper The wrapper to update /// @param allowed Whether the wrapper is allowed /// @dev Only callable by the owner - /// @dev To ensure the wrapped token cannot be wrapped in an ERC6909 token on the PoolManager, the wrapper must only implement `swap` or `modifyLiquidity` functions + /// @dev To ensure the permissions adapter cannot be wrapped in an ERC6909 token on the PoolManager, the wrapper must only implement `swap` or `modifyLiquidity` functions function updateAllowedWrapper(address wrapper, bool allowed) external; /// @notice Updates the swapping enabled status @@ -67,9 +67,9 @@ interface IWrappedPermissionedToken is IERC20 { /// @notice Returns the Uniswap v4 pool manager function POOL_MANAGER() external view returns (address); - /// @notice Returns the permissioned token that is wrapped by this contract + /// @notice Returns the permissioned token that is deposited in this contract function PERMISSIONED_TOKEN() external view returns (IERC20); - /// @notice Returns the admin of the wrapped permissioned token + /// @notice Returns the admin of the permissions adapter function owner() external view returns (address); } diff --git a/src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol b/src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol new file mode 100644 index 000000000..26b6f30fb --- /dev/null +++ b/src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from "./IPermissionsAdapter.sol"; +import {IAllowlistChecker} from "./IAllowlistChecker.sol"; + +interface IPermissionsAdapterFactory { + /// @notice Emitted when a permissions adapter is created + event PermissionsAdapterCreated(address indexed pemissionsAdapter, address indexed permissionedToken); + + /// @notice Emitted when a permissions adapter is verified + event PemissionsAdapterVerified(address indexed pemissionsAdapter, address indexed permissionedToken); + + /// @notice Thrown when the permissions adapter does not exist + error PemissionsAdapterNotFound(address pemissionsAdapter); + + /// @notice Thrown when the permissions adapter is already verified + error PemissionsAdapterAlreadyVerified(address pemissionsAdapter); + + /// @notice Thrown when the permissions adapter is not verified + error PemissionsAdapterNotVerified(address pemissionsAdapter); + + /// @notice Creates a new permissions adapter + /// @param permissionedToken The permissioned token to wrap + /// @param initialOwner The initial owner of the permissions adapter + /// @param allowListChecker The allow list checker that will be used to check if transfers are allowed + function createPermissionsAdapter( + IERC20 permissionedToken, + address initialOwner, + IAllowlistChecker allowListChecker + ) external returns (address); + + /// @notice Verifies a permissions adapter + /// @param pemissionsAdapter The permissions adapter + /// @dev This function verifies that the permissions adapter has a balance of the permissioned token. This means that the permissions adapter is on the allow list of the permissioned token and can be used to wrap and unwrap the permissioned token. + function verifyPermissionsAdapter(address pemissionsAdapter) external; + + /// @notice Returns the permissioned token of a permissions adapter + /// @param pemissionsAdapter The permissions adapter + /// @return permissionedToken The permissioned token + function permissionsAdapterOf(address pemissionsAdapter) external view returns (address permissionedToken); + + /// @notice Returns the verified permissioned token of a permissions adapter + /// @param pemissionsAdapter The permissions adapter + /// @return permissionedToken The verified permissioned token + /// @dev A reverse lookup of the permissioned token is required, otherwise anyone could create a permissions adapter for a non-permissioned token + function verifiedPermissionsAdapterOf(address pemissionsAdapter) + external + view + returns (address permissionedToken); + + /// @notice Returns the v4 pool manager + /// @return poolManager The v4 pool manager + function POOL_MANAGER() external view returns (address poolManager); +} diff --git a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol b/src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol deleted file mode 100644 index b0ad31e17..000000000 --- a/src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {IWrappedPermissionedToken, IERC20} from "./IWrappedPermissionedToken.sol"; -import {IAllowlistChecker} from "./IAllowlistChecker.sol"; - -interface IWrappedPermissionedTokenFactory { - /// @notice Emitted when a wrapped permissioned token is created - event WrappedPermissionedTokenCreated(address indexed wrappedToken, address indexed permissionedToken); - - /// @notice Emitted when a wrapped permissioned token is verified - event WrappedTokenVerified(address indexed wrappedToken, address indexed permissionedToken); - - /// @notice Thrown when the wrapped token does not exist - error WrappedTokenNotFound(address wrappedToken); - - /// @notice Thrown when the wrapped token is already verified - error WrappedTokenAlreadyVerified(address wrappedToken); - - /// @notice Thrown when the wrapped token is not verified - error WrappedTokenNotVerified(address wrappedToken); - - /// @notice Creates a new wrapped permissioned token - /// @param permissionedToken The permissioned token to wrap - /// @param initialOwner The initial owner of the wrapped permissioned token - /// @param allowListChecker The allow list checker that will be used to check if transfers are allowed - function createWrappedPermissionedToken( - IERC20 permissionedToken, - address initialOwner, - IAllowlistChecker allowListChecker - ) external returns (address); - - /// @notice Verifies a wrapped permissioned token - /// @param wrappedToken The wrapped permissioned token - /// @dev This function verifies that the wrapped token has a balance of the permissioned token. This means that the wrapped token is on the allow list of the permissioned token and can be used to wrap and unwrap the permissioned token. - function verifyWrappedToken(address wrappedToken) external; - - /// @notice Returns the permissioned token of a wrapped permissioned token - /// @param wrappedToken The wrapped permissioned token - /// @return permissionedToken The permissioned token - function permissionedTokenOf(address wrappedToken) external view returns (address permissionedToken); - - /// @notice Returns the verified permissioned token of a wrapped permissioned token - /// @param wrappedToken The wrapped permissioned token - /// @return permissionedToken The verified permissioned token - /// @dev A reverse lookup of the permissioned token is required, otherwise anyone could create a wrapped token for a non-permissioned token - function verifiedPermissionedTokenOf(address wrappedToken) external view returns (address permissionedToken); - - /// @notice Returns the v4 pool manager - /// @return poolManager The v4 pool manager - function POOL_MANAGER() external view returns (address poolManager); -} diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 91886dccd..0008641bf 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -30,7 +30,7 @@ import {PermissionedPosmTestSetup, BalanceInfo} from "./shared/PermissionedPosmT import {ReentrantToken} from "../../mocks/ReentrantToken.sol"; import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; import {MockAllowlistChecker, MockPermissionedToken} from "./PermissionedPoolsBase.sol"; -import {WrappedPermissionedToken, IERC20} from "../../../src/hooks/permissionedPools/WrappedPermissionedToken.sol"; +import {PermissionsAdapter, IERC20} from "../../../src/hooks/permissionedPools/PermissionsAdapter.sol"; import {PermissionFlags, PermissionFlag} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, LiquidityFuzzers { @@ -47,8 +47,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq // Permissioned components MockAllowlistChecker public mockAllowListChecker; - WrappedPermissionedToken public wrappedToken0; - WrappedPermissionedToken public wrappedToken2; + PermissionsAdapter public permissionsAdapter0; + PermissionsAdapter public permissionsAdapter2; MockERC20 public originalToken0; MockERC20 public originalToken2; IPositionManager public secondaryPosm; @@ -85,28 +85,29 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq // set up approvals for alice seedBalance(alice); approvePosmFor(alice); - wrappedToken0.updateSwappingEnabled(true); - wrappedToken2.updateSwappingEnabled(true); + permissionsAdapter0.updateSwappingEnabled(true); + permissionsAdapter2.updateSwappingEnabled(true); } function setUpPosms() internal { - deployAndApprovePosm(manager, address(wrappedTokenFactory), keccak256("permissionedPosm")); + deployAndApprovePosm(manager, address(permissionsAdapterFactory), keccak256("permissionedPosm")); // alternate position manager using different hooks - secondaryPosm = deployAndApprovePosmOnly(manager, address(wrappedTokenFactory), keccak256("secondaryPosm")); + secondaryPosm = + deployAndApprovePosmOnly(manager, address(permissionsAdapterFactory), keccak256("secondaryPosm")); // alternate position manager using the same hooks - tertiaryPosm = deployAndApprovePosmOnly(manager, address(wrappedTokenFactory), keccak256("tertiaryPosm")); + tertiaryPosm = deployAndApprovePosmOnly(manager, address(permissionsAdapterFactory), keccak256("tertiaryPosm")); } function setupPool() internal { (key0, poolId) = - initPool(Currency.wrap(address(wrappedToken0)), currency1, permissionedHooks, 3000, SQRT_PRICE_1_1); + initPool(Currency.wrap(address(permissionsAdapter0)), currency1, permissionedHooks, 3000, SQRT_PRICE_1_1); (key1, poolId) = - initPool(currency1, Currency.wrap(address(wrappedToken2)), permissionedHooks, 3000, SQRT_PRICE_1_1); + initPool(currency1, Currency.wrap(address(permissionsAdapter2)), permissionedHooks, 3000, SQRT_PRICE_1_1); (key2, poolId) = initPool( - Currency.wrap(address(wrappedToken0)), - Currency.wrap(address(wrappedToken2)), + Currency.wrap(address(permissionsAdapter0)), + Currency.wrap(address(permissionsAdapter2)), permissionedHooks, 3000, SQRT_PRICE_1_1 @@ -120,13 +121,15 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq } (key3, poolId) = initPool(currencyA, currencyB, permissionedHooks, 3000, SQRT_PRICE_1_1); - (keyFake0, poolId) = - initPool(Currency.wrap(address(wrappedToken0)), currency1, secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1); - (keyFake1, poolId) = - initPool(currency1, Currency.wrap(address(wrappedToken2)), secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1); + (keyFake0, poolId) = initPool( + Currency.wrap(address(permissionsAdapter0)), currency1, secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1 + ); + (keyFake1, poolId) = initPool( + currency1, Currency.wrap(address(permissionsAdapter2)), secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1 + ); (keyFake2, poolId) = initPool( - Currency.wrap(address(wrappedToken0)), - Currency.wrap(address(wrappedToken2)), + Currency.wrap(address(permissionsAdapter0)), + Currency.wrap(address(permissionsAdapter2)), secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1 @@ -144,16 +147,16 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq originalToken0 = MockERC20(Currency.unwrap(currency0)); // ensure expected ordering while (true) { - wrappedToken0 = WrappedPermissionedToken( - wrappedTokenFactory.createWrappedPermissionedToken( + permissionsAdapter0 = PermissionsAdapter( + permissionsAdapterFactory.createPermissionsAdapter( IERC20(address(originalToken0)), address(this), mockAllowListChecker ) ); - if (address(wrappedToken0) < Currency.unwrap(currency1)) { + if (address(permissionsAdapter0) < Currency.unwrap(currency1)) { break; } } - setUpWrappedToken(wrappedToken0, currency0); + setUpPemissionsAdapter(permissionsAdapter0, currency0); } function setUpCurrencyTwo() internal { @@ -162,16 +165,16 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq // ensure expected ordering while (true) { - wrappedToken2 = WrappedPermissionedToken( - wrappedTokenFactory.createWrappedPermissionedToken( + permissionsAdapter2 = PermissionsAdapter( + permissionsAdapterFactory.createPermissionsAdapter( IERC20(address(originalToken2)), address(this), mockAllowListChecker ) ); - if (Currency.unwrap(currency1) < address(wrappedToken2)) { + if (Currency.unwrap(currency1) < address(permissionsAdapter2)) { break; } } - setUpWrappedToken(wrappedToken2, currency2); + setUpPemissionsAdapter(permissionsAdapter2, currency2); } function setUpAllowlist(Currency currency) internal { @@ -182,7 +185,7 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq address(secondaryPosm), PermissionFlags.ALL_ALLOWED ); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( - address(wrappedTokenFactory), PermissionFlags.ALL_ALLOWED + address(permissionsAdapterFactory), PermissionFlags.ALL_ALLOWED ); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), PermissionFlags.ALL_ALLOWED); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(manager), PermissionFlags.ALL_ALLOWED); @@ -194,32 +197,32 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq ); } - function setUpWrappedToken(WrappedPermissionedToken wrappedToken, Currency currency) internal { - wrappedToPermissioned[Currency.wrap(address(wrappedToken))] = currency; + function setUpPemissionsAdapter(PermissionsAdapter pemissionsAdapter, Currency currency) internal { + adapterToPermissioned[Currency.wrap(address(pemissionsAdapter))] = currency; MockPermissionedToken(Currency.unwrap(currency)).mint(address(this), 1000 ether); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( - address(wrappedToken), PermissionFlags.ALL_ALLOWED + address(pemissionsAdapter), PermissionFlags.ALL_ALLOWED ); - // wrapped token contract must have a non-zero balance of the permissioned token - currency.transfer(address(wrappedToken), 1); + // permissions adapter contract must have a non-zero balance of the permissioned token + currency.transfer(address(pemissionsAdapter), 1); - wrappedToken.updateAllowedWrapper(address(manager), true); - wrappedToken.updateAllowedWrapper(address(lpm), true); - wrappedToken.updateAllowedWrapper(address(secondaryPosm), true); - wrappedToken.updateAllowedWrapper(address(permissionedSwapRouter), true); + pemissionsAdapter.updateAllowedWrapper(address(manager), true); + pemissionsAdapter.updateAllowedWrapper(address(lpm), true); + pemissionsAdapter.updateAllowedWrapper(address(secondaryPosm), true); + pemissionsAdapter.updateAllowedWrapper(address(permissionedSwapRouter), true); - wrappedTokenFactory.verifyWrappedToken(address(wrappedToken)); + permissionsAdapterFactory.verifyPermissionsAdapter(address(pemissionsAdapter)); - Currency wrappedCurrency = Currency.wrap(address(wrappedToken)); + Currency permissionsAdapter = Currency.wrap(address(pemissionsAdapter)); - setAllowedHooks(lpm, wrappedCurrency, permissionedHooks); - setAllowedHooks(tertiaryPosm, wrappedCurrency, permissionedHooks); + setAllowedHooks(lpm, permissionsAdapter, permissionedHooks); + setAllowedHooks(tertiaryPosm, permissionsAdapter, permissionedHooks); - setAllowedHooks(lpm, wrappedCurrency, secondaryPermissionedHooks); - setAllowedHooks(secondaryPosm, wrappedCurrency, secondaryPermissionedHooks); - setAllowedHooks(tertiaryPosm, wrappedCurrency, secondaryPermissionedHooks); + setAllowedHooks(lpm, permissionsAdapter, secondaryPermissionedHooks); + setAllowedHooks(secondaryPosm, permissionsAdapter, secondaryPermissionedHooks); + setAllowedHooks(tertiaryPosm, permissionsAdapter, secondaryPermissionedHooks); } function setAllowedHooks(IPositionManager posm, Currency currency, IHooks permissionedHooks_) internal { @@ -426,20 +429,20 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq Currency currency0_ = getPermissionedCurrency(key.currency0); Currency currency1_ = getPermissionedCurrency(key.currency1); - Currency currency0wrapped = key.currency0; - Currency currency1wrapped = key.currency1; + Currency currency0Adapter = key.currency0; + Currency currency1Adapter = key.currency1; uint256 tokenId = lpm.nextTokenId(); uint256 balance0Before = currency0_.balanceOfSelf(); uint256 balance1Before = currency1_.balanceOfSelf(); uint256 balance0BeforeAlice = currency0_.balanceOf(alice); uint256 balance1BeforeAlice = currency1_.balanceOf(alice); - uint256 balance0ManagerBefore = currency0wrapped.balanceOf(address(manager)); - uint256 balance1ManagerBefore = currency1wrapped.balanceOf(address(manager)); + uint256 balance0ManagerBefore = currency0Adapter.balanceOf(address(manager)); + uint256 balance1ManagerBefore = currency1Adapter.balanceOf(address(manager)); mint(config, liquidityToAdd, alice, ZERO_BYTES); - uint256 balance0ManagerAfter = currency0wrapped.balanceOf(address(manager)); - uint256 balance1ManagerAfter = currency1wrapped.balanceOf(address(manager)); + uint256 balance0ManagerAfter = currency0Adapter.balanceOf(address(manager)); + uint256 balance1ManagerAfter = currency1Adapter.balanceOf(address(manager)); // alice was not the payer assertEq(tokenId, lpm.nextTokenId() - 1); @@ -987,9 +990,9 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq function test_initialize() public { _test_initialize(currency0, currency1); - _test_initialize(Currency.wrap(address(wrappedToken0)), currency1); - _test_initialize(currency1, Currency.wrap(address(wrappedToken2))); - _test_initialize(Currency.wrap(address(wrappedToken0)), Currency.wrap(address(wrappedToken2))); + _test_initialize(Currency.wrap(address(permissionsAdapter0)), currency1); + _test_initialize(currency1, Currency.wrap(address(permissionsAdapter2))); + _test_initialize(Currency.wrap(address(permissionsAdapter0)), Currency.wrap(address(permissionsAdapter2))); } function _test_initialize(Currency currency0_, Currency currency1_) internal { @@ -1009,10 +1012,10 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq function test_fuzz_initialize(uint160 sqrtPrice, uint24 fee) public { _test_fuzz_initialize(currency0, currency1, sqrtPrice, fee); - _test_fuzz_initialize(Currency.wrap(address(wrappedToken0)), currency1, sqrtPrice, fee); - _test_fuzz_initialize(currency1, Currency.wrap(address(wrappedToken2)), sqrtPrice, fee); + _test_fuzz_initialize(Currency.wrap(address(permissionsAdapter0)), currency1, sqrtPrice, fee); + _test_fuzz_initialize(currency1, Currency.wrap(address(permissionsAdapter2)), sqrtPrice, fee); _test_fuzz_initialize( - Currency.wrap(address(wrappedToken0)), Currency.wrap(address(wrappedToken2)), sqrtPrice, fee + Currency.wrap(address(permissionsAdapter0)), Currency.wrap(address(permissionsAdapter2)), sqrtPrice, fee ); } @@ -1070,23 +1073,23 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq address owner0 = makeAddr("owner0"); address owner1 = makeAddr("owner1"); - wrappedToken0.transferOwnership(owner0); + permissionsAdapter0.transferOwnership(owner0); vm.prank(owner0); - wrappedToken0.acceptOwnership(); - wrappedToken2.transferOwnership(owner1); + permissionsAdapter0.acceptOwnership(); + permissionsAdapter2.transferOwnership(owner1); vm.prank(owner1); - wrappedToken2.acceptOwnership(); + permissionsAdapter2.acceptOwnership(); vm.prank(owner0); vm.expectEmit(true, true, true, true); emit Transfer(alice, address(this), tokenId1); - // owner0 is admin of wrappedToken0 + // owner0 is admin of permissionsAdapter0 IERC721(address(lpm)).transferFrom(alice, address(this), tokenId1); vm.prank(owner1); vm.expectEmit(true, true, true, true); emit Transfer(alice, address(this), tokenId2); - // owner1 is admin of wrappedToken2 + // owner1 is admin of permissionsAdapter2 IERC721(address(lpm)).transferFrom(alice, address(this), tokenId2); } diff --git a/test/hooks/permissionedPools/PermissionedV4Router.t.sol b/test/hooks/permissionedPools/PermissionedV4Router.t.sol index 706955ce7..59feea5f3 100644 --- a/test/hooks/permissionedPools/PermissionedV4Router.t.sol +++ b/test/hooks/permissionedPools/PermissionedV4Router.t.sol @@ -30,8 +30,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { error InvalidCommandType(uint256 commandType); error ExecutionFailed(uint256 commandIndex, bytes output); - Currency public wrappedCurrency0; - Currency public wrappedCurrency1; + Currency public permissionsAdapter0Currency; + Currency public permissionsAdapter1Currency; // Test Users address public alice = makeAddr("ALICE"); @@ -44,10 +44,10 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { function setUp() public { setupPermissionedRouterCurrenciesAndPoolsWithLiquidity(alice); - wrappedCurrency0 = Currency.wrap(address(wrappedToken0)); - wrappedCurrency1 = Currency.wrap(address(wrappedToken1)); - wrappedToken0.updateSwappingEnabled(true); - wrappedToken1.updateSwappingEnabled(true); + permissionsAdapter0Currency = Currency.wrap(address(permissionsAdapter0)); + permissionsAdapter1Currency = Currency.wrap(address(permissionsAdapter1)); + permissionsAdapter0.updateSwappingEnabled(true); + permissionsAdapter1.updateSwappingEnabled(true); plan = Planner.init(); @@ -109,13 +109,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { function test_gas_swapExactInputSingle_permissionedTokens() public { uint256 amountIn = 1000; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); vm.snapshotGasLastCall("PermissionedV4Router_ExactInputSingle_PermissionedTokens"); @@ -133,15 +135,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); - Currency currencyA = wrappedCurrency0; - Currency currencyB = wrappedCurrency1; + Currency currencyA = permissionsAdapter0Currency; + Currency currencyB = permissionsAdapter1Currency; if (Currency.unwrap(currencyA) > Currency.unwrap(currencyB)) (currencyA, currencyB) = (currencyB, currencyA); uint256 amountIn = 1 ether; - PoolKey memory wrappedKey = PoolKey(currencyA, currencyB, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = PoolKey(currencyA, currencyB, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(currencyA, currencyB, ActionConstants.MSG_SENDER); @@ -166,15 +168,17 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); uint256 amountIn = 100; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); - wrappedToken0.updateSwappingEnabled(false); + permissionsAdapter0.updateSwappingEnabled(false); vm.prank(alice); vm.expectRevert( @@ -188,8 +192,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { ); permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); - wrappedToken0.updateSwappingEnabled(true); - wrappedToken1.updateSwappingEnabled(false); + permissionsAdapter0.updateSwappingEnabled(true); + permissionsAdapter1.updateSwappingEnabled(false); vm.prank(alice); vm.expectRevert( @@ -209,13 +213,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); uint256 amountIn = 100; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); vm.prank(alice); permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); @@ -227,20 +233,22 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountIn = 100; uint256 expectedAmountOut = 98; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = - getInputAndOutputBalances(wrappedKey, true, address(manager)); + getInputAndOutputBalances(adapterKey, true, address(manager)); vm.prank(alice); permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = - getInputAndOutputBalances(wrappedKey, true, address(manager)); + getInputAndOutputBalances(adapterKey, true, address(manager)); assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); @@ -251,16 +259,18 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); uint256 amountIn = 100; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); - wrappedToken0.updateAllowedWrapper(address(permissionedRouter), false); - wrappedToken1.updateAllowedWrapper(address(permissionedRouter), false); + permissionsAdapter0.updateAllowedWrapper(address(permissionedRouter), false); + permissionsAdapter1.updateAllowedWrapper(address(permissionedRouter), false); vm.prank(alice); vm.expectRevert( @@ -278,7 +288,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { function test_swap_succeeds_authorized_user_mixed_entry() public { uint256 amountIn = 100; uint256 expectedAmountOut = 98; - bool zeroToOne = wrappedCurrency1 == key1.currency0; + bool zeroToOne = permissionsAdapter1Currency == key1.currency0; Currency inputCurrency; Currency outputCurrency; @@ -313,7 +323,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountIn = 100; uint256 expectedAmountOut = 98; - bool zeroToOne = !(wrappedCurrency1 == key1.currency0); + bool zeroToOne = !(permissionsAdapter1Currency == key1.currency0); Currency inputCurrency; Currency outputCurrency; @@ -345,7 +355,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { } function test_swap_succeeds_unauthorized_user_mixed_entry() public { - bool zeroToOne = wrappedCurrency1 == key1.currency0; + bool zeroToOne = permissionsAdapter1Currency == key1.currency0; Currency inputCurrency; Currency outputCurrency; @@ -389,7 +399,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(currency2)).transfer(unauthorizedUser, 2 ether); uint256 amountIn = 100; - bool zeroToOne = !(wrappedCurrency1 == key1.currency0); + bool zeroToOne = !(permissionsAdapter1Currency == key1.currency0); Currency inputCurrency; Currency outputCurrency; @@ -428,13 +438,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.NONE); uint256 amountIn = 100; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); vm.prank(alice); vm.expectRevert(); @@ -450,13 +462,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); uint256 amountIn = 100; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); vm.prank(alice); permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); @@ -471,13 +485,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); uint256 amountIn = 100; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); vm.prank(alice); vm.expectRevert(); @@ -493,13 +509,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); uint256 amountIn = 100; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); vm.prank(alice); permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); @@ -511,13 +529,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); uint256 amountIn = 100; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); // Test SWAP_ALLOWED + LIQUIDITY_ALLOWED (should work like ALL_ALLOWED) MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist( @@ -540,13 +560,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); uint256 amountIn = 100; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); vm.prank(alice); vm.expectRevert(); @@ -562,13 +584,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); uint256 amountIn = 100; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); // Should succeed initially vm.prank(alice); @@ -590,13 +614,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); uint256 amountIn = 100; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(wrappedKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); // Test with zero permissions (should be same as NONE) MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlag.wrap(0x0000)); @@ -615,10 +641,10 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { } function test_permission_flag_all_pool_types_swap() public { - MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(wrappedCurrency0))).setAllowlist( + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(permissionsAdapter0Currency))).setAllowlist( alice, PermissionFlags.SWAP_ALLOWED ); - MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(wrappedCurrency1))).setAllowlist( + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(permissionsAdapter1Currency))).setAllowlist( alice, PermissionFlags.SWAP_ALLOWED ); @@ -654,13 +680,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountOut = 50; uint256 amountInMaximum = 100; - PoolKey memory wrappedKey = PoolKey(wrappedCurrency1, wrappedCurrency0, 3000, 60, permissionedHooks); + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactOutputSingleParams memory params = - IV4Router.ExactOutputSingleParams(wrappedKey, true, uint128(amountOut), uint128(amountInMaximum), bytes("")); + IV4Router.ExactOutputSingleParams(adapterKey, true, uint128(amountOut), uint128(amountInMaximum), bytes("")); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); vm.prank(alice); permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); @@ -676,13 +704,13 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountIn = 100; Currency[] memory path = new Currency[](2); - path[0] = wrappedCurrency1; - path[1] = wrappedCurrency0; + path[0] = permissionsAdapter1Currency; + path[1] = permissionsAdapter0Currency; // Create path keys for the multi-hop swap PathKey[] memory pathKeys = new PathKey[](1); pathKeys[0] = PathKey({ - intermediateCurrency: wrappedCurrency0, + intermediateCurrency: permissionsAdapter0Currency, fee: 3000, tickSpacing: 60, hooks: permissionedHooks, @@ -690,10 +718,11 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { }); IV4Router.ExactInputParams memory params = - IV4Router.ExactInputParams(wrappedCurrency1, pathKeys, uint128(amountIn), 0); + IV4Router.ExactInputParams(permissionsAdapter1Currency, pathKeys, uint128(amountIn), 0); plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); - bytes memory data = plan.finalizeSwap(wrappedCurrency1, wrappedCurrency0, ActionConstants.MSG_SENDER); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); vm.prank(alice); permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); @@ -781,8 +810,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); // the router holds no funds before - assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter0.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = getInputAndOutputBalances(key0, true, address(manager)); @@ -856,8 +885,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountIn = 1000; uint256 expectedAmountOut = 949; - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); IV4Router.ExactInputParams memory params = _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks), expectedAmountOut); @@ -876,8 +905,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountIn = 1 ether; uint256 expectedAmountOut = 19992; - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); IV4Router.ExactInputParams memory params = _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks), expectedAmountOut); @@ -890,12 +919,12 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 userOutputBalanceBefore, uint256 userInputBalanceAfter, uint256 userOutputBalanceAfter - ) = _finalizeAndExecuteSwap(wrappedCurrency0, wrappedCurrency1, amountIn); + ) = _finalizeAndExecuteSwap(permissionsAdapter0Currency, permissionsAdapter1Currency, amountIn); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalancesPath(tokenPath, address(manager)); - assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter0.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); @@ -906,8 +935,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountIn = 1 ether; uint256 expectedAmountOut = 19992; - tokenPath.push(wrappedCurrency1); - tokenPath.push(wrappedCurrency0); + tokenPath.push(permissionsAdapter1Currency); + tokenPath.push(permissionsAdapter0Currency); IV4Router.ExactInputParams memory params = _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks), expectedAmountOut); @@ -921,12 +950,12 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 userOutputBalanceBefore, uint256 userInputBalanceAfter, uint256 userOutputBalanceAfter - ) = _finalizeAndExecuteSwap(wrappedCurrency1, wrappedCurrency0, amountIn); + ) = _finalizeAndExecuteSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, amountIn); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalancesPath(tokenPath, address(manager)); - assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter0.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); assertEq(inputBalanceAfter - inputBalanceBefore, amountIn); @@ -937,8 +966,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountIn = 1 ether; uint256 expectedAmountOut = 9982; - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); tokenPath.push(currency2); IV4Router.ExactInputParams memory params = @@ -955,14 +984,14 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 userOutputBalanceBefore, uint256 userInputBalanceAfter, uint256 userOutputBalanceAfter - ) = _finalizeAndExecuteSwap(wrappedCurrency0, currency2, amountIn); + ) = _finalizeAndExecuteSwap(permissionsAdapter0Currency, currency2, amountIn); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalancesPath(tokenPath, address(manager)); // check intermediate token balances assertEq(intermediateBalanceBefore, currency1.balanceOfSelf()); - assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter0.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); assertEq(currency2.balanceOf(address(permissionedRouter)), 0); assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); @@ -974,8 +1003,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountIn = 1 ether; uint256 expectedAmountOut = 6645; - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); tokenPath.push(currency2); tokenPath.push(currency3); @@ -991,13 +1020,13 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 userOutputBalanceBefore, uint256 userInputBalanceAfter, uint256 userOutputBalanceAfter - ) = _finalizeAndExecuteSwap(wrappedCurrency0, currency3, amountIn); + ) = _finalizeAndExecuteSwap(permissionsAdapter0Currency, currency3, amountIn); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalancesPath(tokenPath, address(manager)); // check intermediate tokens werent left in the router - assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter0.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); assertEq(currency2.balanceOf(address(permissionedRouter)), 0); assertEq(currency3.balanceOf(address(permissionedRouter)), 0); assertEq(userInputBalanceBefore - userInputBalanceAfter, amountIn); @@ -1136,8 +1165,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { // the initialized nativeKey is (native, currency0) tokenPath.push(CurrencyLibrary.ADDRESS_ZERO); - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); IV4Router.ExactInputParams memory params = _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks)); @@ -1152,7 +1181,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 userOutputBalanceBefore, uint256 userInputBalanceAfter, uint256 userOutputBalanceAfter - ) = _finalizeAndExecuteSwap(CurrencyLibrary.ADDRESS_ZERO, wrappedCurrency1, amountIn); + ) = _finalizeAndExecuteSwap(CurrencyLibrary.ADDRESS_ZERO, permissionsAdapter1Currency, amountIn); (, uint256 outputBalanceAfter, uint256 ethBalanceAfter) = getInputAndOutputBalancesPath(tokenPath, address(manager)); @@ -1162,7 +1191,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { assertEq(userOutputBalanceAfter - userOutputBalanceBefore, expectedAmountOut); assertEq(nativeKey.currency0.balanceOf(address(permissionedRouter)), 0); assertEq(nativeKey.currency1.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); assertEq(ethBalanceBefore - ethBalanceAfter, amountIn); assertEq(outputBalanceBefore - outputBalanceAfter, expectedAmountOut); } @@ -1172,8 +1201,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 expectedAmountOut = 9982; // the initialized nativeKey is (native, currency0) - tokenPath.push(wrappedCurrency1); - tokenPath.push(wrappedCurrency0); + tokenPath.push(permissionsAdapter1Currency); + tokenPath.push(permissionsAdapter0Currency); tokenPath.push(CurrencyLibrary.ADDRESS_ZERO); IV4Router.ExactInputParams memory params = @@ -1190,7 +1219,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 userOutputBalanceBefore, uint256 userInputBalanceAfter, uint256 userOutputBalanceAfter - ) = _finalizeAndExecuteSwap(wrappedCurrency1, CurrencyLibrary.ADDRESS_ZERO, amountIn); + ) = _finalizeAndExecuteSwap(permissionsAdapter1Currency, CurrencyLibrary.ADDRESS_ZERO, amountIn); (uint256 inputBalanceAfter,, uint256 ethBalanceAfter) = getInputAndOutputBalancesPath(tokenPath, address(manager)); @@ -1309,8 +1338,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalances(key0, zeroForOne, address(manager)); - assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter0.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); assertEq(outputBalanceBefore - outputBalanceAfter, amountOut); @@ -1321,8 +1350,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountOut = 1 ether; uint256 expectedAmountIn = 369070324262815812743748; - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); IV4Router.ExactOutputParams memory params = _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); @@ -1341,8 +1370,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountOut = 19992; uint256 expectedAmountIn = 434604409; - tokenPath.push(wrappedCurrency1); - tokenPath.push(wrappedCurrency0); + tokenPath.push(permissionsAdapter1Currency); + tokenPath.push(permissionsAdapter0Currency); IV4Router.ExactOutputParams memory params = _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); @@ -1359,8 +1388,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalancesPath(tokenPath, address(manager)); - assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter0.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); assertEq(outputBalanceBefore - outputBalanceAfter, amountOut); @@ -1371,8 +1400,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountOut = 19992; uint256 expectedAmountIn = 433302557; - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); IV4Router.ExactOutputParams memory params = _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); @@ -1385,12 +1414,12 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 userOutputBalanceBefore, uint256 userInputBalanceAfter, uint256 userOutputBalanceAfter - ) = _finalizeAndExecuteSwap(wrappedCurrency0, key0.currency0, expectedAmountIn); + ) = _finalizeAndExecuteSwap(permissionsAdapter0Currency, key0.currency0, expectedAmountIn); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalancesPath(tokenPath, address(manager)); - assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter0.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); assertEq(outputBalanceBefore - outputBalanceAfter, amountOut); @@ -1401,8 +1430,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountOut = 100; uint256 expectedAmountIn = 104; - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); tokenPath.push(currency2); IV4Router.ExactOutputParams memory params = @@ -1418,13 +1447,13 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 userOutputBalanceBefore, uint256 userInputBalanceAfter, uint256 userOutputBalanceAfter - ) = _finalizeAndExecuteSwap(wrappedCurrency0, currency2, expectedAmountIn); + ) = _finalizeAndExecuteSwap(permissionsAdapter0Currency, currency2, expectedAmountIn); (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalancesPath(tokenPath, address(manager)); assertEq(intermediateBalanceBefore, currency1.balanceOfSelf()); - assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter0.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); assertEq(currency2.balanceOf(address(permissionedRouter)), 0); assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); @@ -1436,8 +1465,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountOut = 100; uint256 expectedAmountIn = 106; - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); tokenPath.push(currency2); tokenPath.push(currency3); @@ -1457,8 +1486,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { (uint256 inputBalanceAfter, uint256 outputBalanceAfter,) = getInputAndOutputBalancesPath(tokenPath, address(manager)); - assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter0.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); assertEq(currency2.balanceOf(address(permissionedRouter)), 0); assertEq(currency3.balanceOf(address(permissionedRouter)), 0); assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); @@ -1472,8 +1501,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 expectedAmountIn = 106; tokenPath.push(currency4); - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); tokenPath.push(currency2); IV4Router.ExactOutputParams memory params = @@ -1493,8 +1522,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { getInputAndOutputBalancesPath(tokenPath, address(manager)); assertEq(currency4.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter0.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); assertEq(currency2.balanceOf(address(permissionedRouter)), 0); assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); @@ -1509,8 +1538,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { currency4.transfer(unauthorizedUser, 1000); tokenPath.push(currency4); - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); tokenPath.push(currency2); MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); @@ -1541,8 +1570,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 expectedAmountIn = 106; tokenPath.push(nativeKey.currency0); - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); tokenPath.push(currency2); IV4Router.ExactOutputParams memory params = @@ -1562,8 +1591,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { getInputAndOutputBalancesPath(tokenPath, address(manager)); assertEq(nativeKey.currency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency0.balanceOf(address(permissionedRouter)), 0); - assertEq(wrappedCurrency1.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter0.balanceOf(address(permissionedRouter)), 0); + assertEq(permissionsAdapter1.balanceOf(address(permissionedRouter)), 0); assertEq(currency2.balanceOf(address(permissionedRouter)), 0); assertEq(userInputBalanceBefore - userInputBalanceAfter, expectedAmountIn); assertEq(userOutputBalanceAfter - userOutputBalanceBefore, amountOut); @@ -1578,8 +1607,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 expectedAmountIn = 106; tokenPath.push(nativeKey.currency0); - tokenPath.push(wrappedCurrency0); - tokenPath.push(wrappedCurrency1); + tokenPath.push(permissionsAdapter0Currency); + tokenPath.push(permissionsAdapter1Currency); tokenPath.push(currency2); MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); @@ -1635,8 +1664,8 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 expectedAmountIn = 1057; uint256 expectedOutput = 1000; - tokenPath.push(wrappedCurrency1); - tokenPath.push(wrappedCurrency0); + tokenPath.push(permissionsAdapter1Currency); + tokenPath.push(permissionsAdapter0Currency); IV4Router.ExactOutputParams memory params = _getExactOutputParamsWithHook( tokenPath, ActionConstants.OPEN_DELTA, address(permissionedHooks), expectedAmountIn diff --git a/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol b/test/hooks/permissionedPools/PermissionsAdapter.t.sol similarity index 51% rename from test/hooks/permissionedPools/WrappedPermissionedToken.t.sol rename to test/hooks/permissionedPools/PermissionsAdapter.t.sol index 07e176e85..49d932143 100644 --- a/test/hooks/permissionedPools/WrappedPermissionedToken.t.sol +++ b/test/hooks/permissionedPools/PermissionsAdapter.t.sol @@ -2,16 +2,16 @@ pragma solidity ^0.8.20; import { - IWrappedPermissionedToken, + IPermissionsAdapter, IAllowlistChecker -} from "../../../src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol"; +} from "../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapter.sol"; import {IERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {PermissionedPoolsBase, MockAllowlistChecker} from "./PermissionedPoolsBase.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {PermissionFlags, PermissionFlag} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; -contract WrappedPermissionedTokenTest is PermissionedPoolsBase { - IWrappedPermissionedToken public wrappedPermissionedToken; +contract PermissionsAdapterTest is PermissionedPoolsBase { + IPermissionsAdapter public pemissionsAdapter; address public mockPoolManager; address public owner; @@ -20,136 +20,131 @@ contract WrappedPermissionedTokenTest is PermissionedPoolsBase { mockPoolManager = makeAddr("mockPoolManager"); owner = makeAddr("owner"); bytes memory args = abi.encode(permissionedToken, mockPoolManager, owner, allowlistChecker); - bytes memory initcode = - abi.encodePacked(vm.getCode("WrappedPermissionedToken.sol:WrappedPermissionedToken"), args); + bytes memory initcode = abi.encodePacked(vm.getCode("PermissionsAdapter.sol:PermissionsAdapter"), args); assembly { - sstore(wrappedPermissionedToken.slot, create(0, add(initcode, 0x20), mload(initcode))) + sstore(pemissionsAdapter.slot, create(0, add(initcode, 0x20), mload(initcode))) } - permissionedToken.setTokenAllowlist(address(wrappedPermissionedToken), true); + permissionedToken.setTokenAllowlist(address(pemissionsAdapter), true); vm.prank(owner); - wrappedPermissionedToken.updateAllowedWrapper(address(this), true); + pemissionsAdapter.updateAllowedWrapper(address(this), true); } function test_InitialState() public view { - assertEq(IERC20Metadata(address(wrappedPermissionedToken)).name(), "Uniswap v4 Wrapped MockToken"); - assertEq(IERC20Metadata(address(wrappedPermissionedToken)).symbol(), "uwMT"); - assertEq(IERC20Metadata(address(wrappedPermissionedToken)).decimals(), permissionedToken.decimals()); - assertEq(wrappedPermissionedToken.totalSupply(), 0); - assertEq(wrappedPermissionedToken.balanceOf(mockPoolManager), 0); - assertEq(address(wrappedPermissionedToken.allowListChecker()), address(allowlistChecker)); - assertEq(wrappedPermissionedToken.POOL_MANAGER(), mockPoolManager); - assertEq(address(wrappedPermissionedToken.PERMISSIONED_TOKEN()), address(permissionedToken)); + assertEq(IERC20Metadata(address(pemissionsAdapter)).name(), "Uniswap v4 MockToken"); + assertEq(IERC20Metadata(address(pemissionsAdapter)).symbol(), "v4MT"); + assertEq(IERC20Metadata(address(pemissionsAdapter)).decimals(), permissionedToken.decimals()); + assertEq(pemissionsAdapter.totalSupply(), 0); + assertEq(pemissionsAdapter.balanceOf(mockPoolManager), 0); + assertEq(address(pemissionsAdapter.allowListChecker()), address(allowlistChecker)); + assertEq(pemissionsAdapter.POOL_MANAGER(), mockPoolManager); + assertEq(address(pemissionsAdapter.PERMISSIONED_TOKEN()), address(permissionedToken)); } function testRevert_WhenNotOwner(address account) public { vm.assume(account != owner); vm.startPrank(account); vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, account)); - wrappedPermissionedToken.updateAllowedWrapper(account, true); + pemissionsAdapter.updateAllowedWrapper(account, true); vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, account)); - wrappedPermissionedToken.updateAllowListChecker(allowlistChecker); + pemissionsAdapter.updateAllowListChecker(allowlistChecker); vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, account)); - wrappedPermissionedToken.updateSwappingEnabled(true); + pemissionsAdapter.updateSwappingEnabled(true); vm.stopPrank(); } function testRevert_WhenNotAllowedWrapper(address wrapper) public { vm.assume(wrapper != address(this)); - assertFalse(wrappedPermissionedToken.allowedWrappers(wrapper)); + assertFalse(pemissionsAdapter.allowedWrappers(wrapper)); vm.prank(wrapper); - vm.expectRevert(abi.encodeWithSelector(IWrappedPermissionedToken.UnauthorizedWrapper.selector, wrapper)); - wrappedPermissionedToken.wrapToPoolManager(100); + vm.expectRevert(abi.encodeWithSelector(IPermissionsAdapter.UnauthorizedWrapper.selector, wrapper)); + pemissionsAdapter.wrapToPoolManager(100); } function testRevert_WhenInsufficientBalance(uint256 amount, uint256 transferAmount) public { vm.assume(amount != 0); transferAmount = bound(amount, 0, amount - 1); - permissionedToken.mint(address(wrappedPermissionedToken), transferAmount); + permissionedToken.mint(address(pemissionsAdapter), transferAmount); vm.expectRevert( - abi.encodeWithSelector(IWrappedPermissionedToken.InsufficientBalance.selector, amount, transferAmount) + abi.encodeWithSelector(IPermissionsAdapter.InsufficientBalance.selector, amount, transferAmount) ); - wrappedPermissionedToken.wrapToPoolManager(amount); + pemissionsAdapter.wrapToPoolManager(amount); } function test_WrapToPoolManager(uint256 amount, uint256 actualAmount) public { actualAmount = bound(amount, amount, type(uint256).max); - permissionedToken.mint(address(wrappedPermissionedToken), actualAmount); + permissionedToken.mint(address(pemissionsAdapter), actualAmount); vm.expectEmit(true, true, true, true); emit IERC20.Transfer(address(0), mockPoolManager, amount); - wrappedPermissionedToken.wrapToPoolManager(amount); - assertEq(wrappedPermissionedToken.balanceOf(mockPoolManager), amount); + pemissionsAdapter.wrapToPoolManager(amount); + assertEq(pemissionsAdapter.balanceOf(mockPoolManager), amount); assertEq(permissionedToken.balanceOf(mockPoolManager), actualAmount - amount); } function test_UpdateAllowedWrapper(address wrapper, bool allowed) public { vm.assume(wrapper != address(this)); - assertFalse(wrappedPermissionedToken.allowedWrappers(wrapper)); + assertFalse(pemissionsAdapter.allowedWrappers(wrapper)); vm.prank(owner); vm.expectEmit(true, true, true, true); - emit IWrappedPermissionedToken.AllowedWrapperUpdated(wrapper, allowed); - wrappedPermissionedToken.updateAllowedWrapper(wrapper, allowed); - assertEq(wrappedPermissionedToken.allowedWrappers(wrapper), allowed); + emit IPermissionsAdapter.AllowedWrapperUpdated(wrapper, allowed); + pemissionsAdapter.updateAllowedWrapper(wrapper, allowed); + assertEq(pemissionsAdapter.allowedWrappers(wrapper), allowed); } function testRevert_WhenNotSupportedInterfaceEOA(IAllowlistChecker newAllowListCheckerEOA) public { vm.assume(newAllowListCheckerEOA != allowlistChecker); vm.prank(owner); vm.expectRevert(); // expect revert without data - wrappedPermissionedToken.updateAllowListChecker(newAllowListCheckerEOA); + pemissionsAdapter.updateAllowListChecker(newAllowListCheckerEOA); } function testRevert_WhenNotSupportedInterfaceContract() public { IAllowlistChecker newAllowListCheckerContract = new ImproperAllowlistChecker(); vm.prank(owner); vm.expectRevert( - abi.encodeWithSelector( - IWrappedPermissionedToken.InvalidAllowListChecker.selector, newAllowListCheckerContract - ) + abi.encodeWithSelector(IPermissionsAdapter.InvalidAllowListChecker.selector, newAllowListCheckerContract) ); - wrappedPermissionedToken.updateAllowListChecker(newAllowListCheckerContract); + pemissionsAdapter.updateAllowListChecker(newAllowListCheckerContract); } function test_UpdateAllowListChecker() public { IAllowlistChecker newAllowListChecker = new MockAllowlistChecker(permissionedToken); vm.prank(owner); vm.expectEmit(true, true, true, true); - emit IWrappedPermissionedToken.AllowListCheckerUpdated(newAllowListChecker); - wrappedPermissionedToken.updateAllowListChecker(newAllowListChecker); - assertEq(address(wrappedPermissionedToken.allowListChecker()), address(newAllowListChecker)); + emit IPermissionsAdapter.AllowListCheckerUpdated(newAllowListChecker); + pemissionsAdapter.updateAllowListChecker(newAllowListChecker); + assertEq(address(pemissionsAdapter.allowListChecker()), address(newAllowListChecker)); } function test_UpdateSwappingEnabled(bool enabled) public { vm.prank(owner); vm.expectEmit(true, true, true, true); - emit IWrappedPermissionedToken.SwappingEnabledUpdated(enabled); - wrappedPermissionedToken.updateSwappingEnabled(enabled); - assertEq(wrappedPermissionedToken.swappingEnabled(), enabled); + emit IPermissionsAdapter.SwappingEnabledUpdated(enabled); + pemissionsAdapter.updateSwappingEnabled(enabled); + assertEq(pemissionsAdapter.swappingEnabled(), enabled); } function testRevert_WhenInvalidTransfer(address from, address to) public { vm.assume(from != address(0) && from != mockPoolManager); vm.assume(to != address(0) && to != mockPoolManager); vm.prank(from); - vm.expectRevert(abi.encodeWithSelector(IWrappedPermissionedToken.InvalidTransfer.selector, from, to)); - wrappedPermissionedToken.transfer(to, 0); + vm.expectRevert(abi.encodeWithSelector(IPermissionsAdapter.InvalidTransfer.selector, from, to)); + pemissionsAdapter.transfer(to, 0); } function test_UnwrapOnPoolManagerTransfer(uint256 mintAmount, uint256 transferAmount, address recipient) public { - vm.assume( - recipient != address(0) && recipient != mockPoolManager && recipient != address(wrappedPermissionedToken) - ); + vm.assume(recipient != address(0) && recipient != mockPoolManager && recipient != address(pemissionsAdapter)); assertEq(permissionedToken.balanceOf(recipient), 0); permissionedToken.setTokenAllowlist(recipient, true); transferAmount = bound(transferAmount, 0, mintAmount); - permissionedToken.mint(address(wrappedPermissionedToken), mintAmount); - wrappedPermissionedToken.wrapToPoolManager(mintAmount); + permissionedToken.mint(address(pemissionsAdapter), mintAmount); + pemissionsAdapter.wrapToPoolManager(mintAmount); vm.prank(mockPoolManager); vm.expectEmit(true, true, true, true); emit IERC20.Transfer(mockPoolManager, recipient, transferAmount); vm.expectEmit(true, true, true, true); emit IERC20.Transfer(recipient, address(0), transferAmount); - wrappedPermissionedToken.transfer(recipient, transferAmount); - assertEq(wrappedPermissionedToken.balanceOf(mockPoolManager), mintAmount - transferAmount); + pemissionsAdapter.transfer(recipient, transferAmount); + assertEq(pemissionsAdapter.balanceOf(mockPoolManager), mintAmount - transferAmount); assertEq(permissionedToken.balanceOf(recipient), transferAmount); } } diff --git a/test/hooks/permissionedPools/PermissionsAdapterFactory.t.sol b/test/hooks/permissionedPools/PermissionsAdapterFactory.t.sol new file mode 100644 index 000000000..798aec775 --- /dev/null +++ b/test/hooks/permissionedPools/PermissionsAdapterFactory.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import { + IPermissionsAdapter, + IAllowlistChecker +} from "../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapter.sol"; +import {IPermissionsAdapterFactory} from + "../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; +import {IERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {PermissionedPoolsBase, MockAllowlistChecker} from "./PermissionedPoolsBase.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {PermissionFlags} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; + +contract PermissionsAdapterFactoryTest is PermissionedPoolsBase { + IPermissionsAdapterFactory public permissionsAdapterFactory; + address public mockPoolManager; + + function setUp() public override { + super.setUp(); + mockPoolManager = makeAddr("mockPoolManager"); + bytes memory args = abi.encode(mockPoolManager); + bytes memory initcode = + abi.encodePacked(vm.getCode("PermissionsAdapterFactory.sol:PermissionsAdapterFactory"), args); + assembly { + sstore(permissionsAdapterFactory.slot, create(0, add(initcode, 0x20), mload(initcode))) + } + } + + function test_InitialState() public view { + assertEq(permissionsAdapterFactory.POOL_MANAGER(), mockPoolManager); + } + + function test_CreatePermissionsAdapter(address initialOwner) public { + vm.assume(initialOwner != address(0)); + address expectedPermissionsAdapter = vm.computeCreateAddress(address(permissionsAdapterFactory), 1); + vm.expectEmit(true, true, true, true); + emit IPermissionsAdapterFactory.PermissionsAdapterCreated( + expectedPermissionsAdapter, address(permissionedToken) + ); + address pemissionsAdapter = + permissionsAdapterFactory.createPermissionsAdapter(permissionedToken, initialOwner, allowlistChecker); + assertEq(pemissionsAdapter, expectedPermissionsAdapter); + assertEq(permissionsAdapterFactory.permissionsAdapterOf(pemissionsAdapter), address(permissionedToken)); + assertEq(permissionsAdapterFactory.verifiedPermissionsAdapterOf(pemissionsAdapter), address(0)); + } + + function testRevert_WhenPemissionsAdapterNotDeployed(address pemissionsAdapter) public { + vm.expectRevert( + abi.encodeWithSelector(IPermissionsAdapterFactory.PemissionsAdapterNotFound.selector, pemissionsAdapter) + ); + permissionsAdapterFactory.verifyPermissionsAdapter(pemissionsAdapter); + } + + function testRevert_WhenPemissionsAdapterNotVerified() public { + address pemissionsAdapter = permissionsAdapterFactory.createPermissionsAdapter( + permissionedToken, makeAddr("initialOwner"), allowlistChecker + ); + vm.expectRevert( + abi.encodeWithSelector(IPermissionsAdapterFactory.PemissionsAdapterNotVerified.selector, pemissionsAdapter) + ); + permissionsAdapterFactory.verifyPermissionsAdapter(pemissionsAdapter); + } + + function test_VerifyPemissionsAdapter() public { + address pemissionsAdapter = permissionsAdapterFactory.createPermissionsAdapter( + permissionedToken, makeAddr("initialOwner"), allowlistChecker + ); + permissionedToken.setAllowlist(pemissionsAdapter, PermissionFlags.ALL_ALLOWED); + permissionedToken.mint(pemissionsAdapter, 1); + vm.expectEmit(true, true, true, true); + emit IPermissionsAdapterFactory.PemissionsAdapterVerified(pemissionsAdapter, address(permissionedToken)); + permissionsAdapterFactory.verifyPermissionsAdapter(pemissionsAdapter); + assertEq(permissionsAdapterFactory.verifiedPermissionsAdapterOf(pemissionsAdapter), address(permissionedToken)); + } + + function testRevert_WhenPemissionsAdapterAlreadyVerified() public { + address pemissionsAdapter = permissionsAdapterFactory.createPermissionsAdapter( + permissionedToken, makeAddr("initialOwner"), allowlistChecker + ); + permissionedToken.setAllowlist(pemissionsAdapter, PermissionFlags.ALL_ALLOWED); + permissionedToken.mint(pemissionsAdapter, 1); + permissionsAdapterFactory.verifyPermissionsAdapter(pemissionsAdapter); + vm.expectRevert( + abi.encodeWithSelector( + IPermissionsAdapterFactory.PemissionsAdapterAlreadyVerified.selector, pemissionsAdapter + ) + ); + permissionsAdapterFactory.verifyPermissionsAdapter(pemissionsAdapter); + } +} diff --git a/test/hooks/permissionedPools/WrappedPermissionedTokenFactory.t.sol b/test/hooks/permissionedPools/WrappedPermissionedTokenFactory.t.sol deleted file mode 100644 index 4767251ab..000000000 --- a/test/hooks/permissionedPools/WrappedPermissionedTokenFactory.t.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.20; - -import { - IWrappedPermissionedToken, - IAllowlistChecker -} from "../../../src/hooks/permissionedPools/interfaces/IWrappedPermissionedToken.sol"; -import {IWrappedPermissionedTokenFactory} from - "../../../src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol"; -import {IERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {PermissionedPoolsBase, MockAllowlistChecker} from "./PermissionedPoolsBase.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {PermissionFlags} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; - -contract WrappedPermissionedTokenFactoryTest is PermissionedPoolsBase { - IWrappedPermissionedTokenFactory public wrappedPermissionedTokenFactory; - address public mockPoolManager; - - function setUp() public override { - super.setUp(); - mockPoolManager = makeAddr("mockPoolManager"); - bytes memory args = abi.encode(mockPoolManager); - bytes memory initcode = - abi.encodePacked(vm.getCode("WrappedPermissionedTokenFactory.sol:WrappedPermissionedTokenFactory"), args); - assembly { - sstore(wrappedPermissionedTokenFactory.slot, create(0, add(initcode, 0x20), mload(initcode))) - } - } - - function test_InitialState() public view { - assertEq(wrappedPermissionedTokenFactory.POOL_MANAGER(), mockPoolManager); - } - - function test_CreateWrappedPermissionedToken(address initialOwner) public { - vm.assume(initialOwner != address(0)); - address expectedWrappedPermissionedToken = vm.computeCreateAddress(address(wrappedPermissionedTokenFactory), 1); - vm.expectEmit(true, true, true, true); - emit IWrappedPermissionedTokenFactory.WrappedPermissionedTokenCreated( - expectedWrappedPermissionedToken, address(permissionedToken) - ); - address wrappedPermissionedToken = wrappedPermissionedTokenFactory.createWrappedPermissionedToken( - permissionedToken, initialOwner, allowlistChecker - ); - assertEq(wrappedPermissionedToken, expectedWrappedPermissionedToken); - assertEq( - wrappedPermissionedTokenFactory.permissionedTokenOf(wrappedPermissionedToken), address(permissionedToken) - ); - assertEq(wrappedPermissionedTokenFactory.verifiedPermissionedTokenOf(wrappedPermissionedToken), address(0)); - } - - function testRevert_WhenWrappedTokenNotDeployed(address wrappedToken) public { - vm.expectRevert( - abi.encodeWithSelector(IWrappedPermissionedTokenFactory.WrappedTokenNotFound.selector, wrappedToken) - ); - wrappedPermissionedTokenFactory.verifyWrappedToken(wrappedToken); - } - - function testRevert_WhenWrappedTokenNotVerified() public { - address wrappedPermissionedToken = wrappedPermissionedTokenFactory.createWrappedPermissionedToken( - permissionedToken, makeAddr("initialOwner"), allowlistChecker - ); - vm.expectRevert( - abi.encodeWithSelector( - IWrappedPermissionedTokenFactory.WrappedTokenNotVerified.selector, wrappedPermissionedToken - ) - ); - wrappedPermissionedTokenFactory.verifyWrappedToken(wrappedPermissionedToken); - } - - function test_VerifyWrappedToken() public { - address wrappedPermissionedToken = wrappedPermissionedTokenFactory.createWrappedPermissionedToken( - permissionedToken, makeAddr("initialOwner"), allowlistChecker - ); - permissionedToken.setAllowlist(wrappedPermissionedToken, PermissionFlags.ALL_ALLOWED); - permissionedToken.mint(wrappedPermissionedToken, 1); - vm.expectEmit(true, true, true, true); - emit IWrappedPermissionedTokenFactory.WrappedTokenVerified(wrappedPermissionedToken, address(permissionedToken)); - wrappedPermissionedTokenFactory.verifyWrappedToken(wrappedPermissionedToken); - assertEq( - wrappedPermissionedTokenFactory.verifiedPermissionedTokenOf(wrappedPermissionedToken), - address(permissionedToken) - ); - } - - function testRevert_WhenWrappedTokenAlreadyVerified() public { - address wrappedPermissionedToken = wrappedPermissionedTokenFactory.createWrappedPermissionedToken( - permissionedToken, makeAddr("initialOwner"), allowlistChecker - ); - permissionedToken.setAllowlist(wrappedPermissionedToken, PermissionFlags.ALL_ALLOWED); - permissionedToken.mint(wrappedPermissionedToken, 1); - wrappedPermissionedTokenFactory.verifyWrappedToken(wrappedPermissionedToken); - vm.expectRevert( - abi.encodeWithSelector( - IWrappedPermissionedTokenFactory.WrappedTokenAlreadyVerified.selector, wrappedPermissionedToken - ) - ); - wrappedPermissionedTokenFactory.verifyWrappedToken(wrappedPermissionedToken); - } -} diff --git a/test/hooks/permissionedPools/mocks/MockHooks.sol b/test/hooks/permissionedPools/mocks/MockHooks.sol index 0c8be65f3..b1d95222b 100644 --- a/test/hooks/permissionedPools/mocks/MockHooks.sol +++ b/test/hooks/permissionedPools/mocks/MockHooks.sol @@ -5,13 +5,13 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IWrappedPermissionedTokenFactory} from - "../../../../src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol"; +import {IPermissionsAdapterFactory} from + "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; import {PermissionedHooks} from "../../../../src/hooks/permissionedPools/PermissionedHooks.sol"; contract MockHooks is PermissionedHooks { - constructor(IPoolManager manager, IWrappedPermissionedTokenFactory wrappedTokenFactory) - PermissionedHooks(manager, wrappedTokenFactory) + constructor(IPoolManager manager, IPermissionsAdapterFactory permissionsAdapterFactory) + PermissionedHooks(manager, permissionsAdapterFactory) {} } diff --git a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol index 5e2a0186d..712803a5b 100644 --- a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol @@ -31,8 +31,8 @@ import {Plan} from "../../../shared/Planner.sol"; import {IV4Router} from "../../../../src/interfaces/IV4Router.sol"; import {Actions} from "../../../../src/libraries/Actions.sol"; import {ActionConstants} from "../../../../src/libraries/ActionConstants.sol"; -import {IWrappedPermissionedTokenFactory} from - "../../../../src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol"; +import {IPermissionsAdapterFactory} from + "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; import {PermissionedV4Router} from "../../../../src/hooks/permissionedPools/PermissionedV4Router.sol"; import {MockPermissionedToken} from "../PermissionedPoolsBase.sol"; import {MockV4Router} from "../../../mocks/MockV4Router.sol"; @@ -81,7 +81,7 @@ contract PermissionedDeployers is Test { ActionsRouter public actionsRouter; IHooks public permissionedHooks; IHooks public secondaryPermissionedHooks; - IWrappedPermissionedTokenFactory public wrappedTokenFactory; + IPermissionsAdapterFactory public permissionsAdapterFactory; PoolClaimsTest public claimsRouter; PoolNestedActionsTest public nestedActionRouter; @@ -125,7 +125,7 @@ contract PermissionedDeployers is Test { manager.setProtocolFeeController(feeController); } - function deployPermissionedHooks(address manager_, address wrappedTokenFactory_) + function deployPermissionedHooks(address manager_, address permissionsAdapterFactory_) internal returns (address deployedHooksAddr) { @@ -134,12 +134,12 @@ contract PermissionedDeployers is Test { address(this), flags, vm.getCode("MockHooks.sol:MockHooks"), - abi.encode(IPoolManager(manager_), IWrappedPermissionedTokenFactory(wrappedTokenFactory_)) + abi.encode(IPoolManager(manager_), IPermissionsAdapterFactory(permissionsAdapterFactory_)) ); address addr = Deploy.create2( abi.encodePacked( vm.getCode("MockHooks.sol:MockHooks"), - abi.encode(IPoolManager(manager_), IWrappedPermissionedTokenFactory(wrappedTokenFactory_)) + abi.encode(IPoolManager(manager_), IPermissionsAdapterFactory(permissionsAdapterFactory_)) ), salt ); @@ -147,7 +147,7 @@ contract PermissionedDeployers is Test { deployedHooksAddr = calculatedAddr; } - function deployPermissionedV4Router(address permit2_, address wrappedTokenFactory_, address weth9) + function deployPermissionedV4Router(address permit2_, address permissionsAdapterFactory_, address weth9) internal returns (address deployedAddr) { @@ -156,7 +156,7 @@ contract PermissionedDeployers is Test { abi.encode( manager, IAllowanceTransfer(permit2_), - IWrappedPermissionedTokenFactory(wrappedTokenFactory_), + IPermissionsAdapterFactory(permissionsAdapterFactory_), IWETH9(address(weth9)) ) ); @@ -167,18 +167,17 @@ contract PermissionedDeployers is Test { function deployFreshManagerAndRoutersPermissioned(address permit2_, address weth9) internal { deployFreshManager(); - address wrappedTokenFactoryAddress = Deploy.create2( + address permissionsAdapterFactoryAddress = Deploy.create2( abi.encodePacked( - vm.getCode("WrappedPermissionedTokenFactory.sol:WrappedPermissionedTokenFactory"), - abi.encode(address(manager)) + vm.getCode("PermissionsAdapterFactory.sol:PermissionsAdapterFactory"), abi.encode(address(manager)) ), - keccak256("wrappedTokenFactory") + keccak256("permissionsAdapterFactory") ); - wrappedTokenFactory = IWrappedPermissionedTokenFactory(wrappedTokenFactoryAddress); - permissionedHooks = IHooks(deployPermissionedHooks(address(manager), wrappedTokenFactoryAddress)); - secondaryPermissionedHooks = IHooks(deployPermissionedHooks(address(manager), wrappedTokenFactoryAddress)); - address deployedAddr = deployPermissionedV4Router(permit2_, wrappedTokenFactoryAddress, weth9); + permissionsAdapterFactory = IPermissionsAdapterFactory(permissionsAdapterFactoryAddress); + permissionedHooks = IHooks(deployPermissionedHooks(address(manager), permissionsAdapterFactoryAddress)); + secondaryPermissionedHooks = IHooks(deployPermissionedHooks(address(manager), permissionsAdapterFactoryAddress)); + address deployedAddr = deployPermissionedV4Router(permit2_, permissionsAdapterFactoryAddress, weth9); permissionedSwapRouter = PermissionedV4Router(payable(deployedAddr)); swapRouter = PoolSwapTest(deployedAddr); deployMiscRouters(); diff --git a/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol b/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol index 51998501e..7caa4592d 100644 --- a/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol +++ b/test/hooks/permissionedPools/shared/PermissionedPosmTestSetup.sol @@ -51,14 +51,16 @@ contract PermissionedPosmTestSetup is Test, PermissionedDeployers, DeployPermit2 PoolKey public wethKey; - mapping(Currency => Currency) public wrappedToPermissioned; + mapping(Currency => Currency) public adapterToPermissioned; - function deployAndApprovePosm(IPoolManager poolManager, address wrappedTokenFactory_, bytes32 salt) public { - deployPermissionedPosm(poolManager, wrappedTokenFactory_, salt); + function deployAndApprovePosm(IPoolManager poolManager, address permissionsAdapterFactory_, bytes32 salt) public { + deployPermissionedPosm(poolManager, permissionsAdapterFactory_, salt); approvePosm(); } - function deployPermissionedPosm(IPoolManager poolManager, address wrappedTokenFactory_, bytes32 salt) internal { + function deployPermissionedPosm(IPoolManager poolManager, address permissionsAdapterFactory_, bytes32 salt) + internal + { permit2 = IAllowanceTransfer(deployPermit2()); _WETH9 = deployWETH(); proxyAsImplementation = deployDescriptor(poolManager, "ETH"); @@ -68,12 +70,12 @@ contract PermissionedPosmTestSetup is Test, PermissionedDeployers, DeployPermit2 100_000, address(proxyAsImplementation), address(_WETH9), - wrappedTokenFactory_, + permissionsAdapterFactory_, abi.encode(salt) ); } - function deployAndApprovePosmOnly(IPoolManager poolManager, address wrappedTokenFactory_, bytes32 salt) + function deployAndApprovePosmOnly(IPoolManager poolManager, address permissionsAdapterFactory_, bytes32 salt) public returns (IPositionManager secondaryPosm) { @@ -83,7 +85,7 @@ contract PermissionedPosmTestSetup is Test, PermissionedDeployers, DeployPermit2 100_000, address(proxyAsImplementation), address(_WETH9), - wrappedTokenFactory_, + permissionsAdapterFactory_, abi.encode(salt) ); approvePosm(); @@ -144,7 +146,7 @@ contract PermissionedPosmTestSetup is Test, PermissionedDeployers, DeployPermit2 } function getPermissionedCurrency(Currency currency) internal view returns (Currency) { - Currency permissionedCurrency = wrappedToPermissioned[currency]; + Currency permissionedCurrency = adapterToPermissioned[currency]; if (permissionedCurrency == Currency.wrap(address(0))) { return currency; } diff --git a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol index 1907a5ac7..247f577e7 100644 --- a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol @@ -16,12 +16,12 @@ import {PermissionedV4Router} from "../../../../src/hooks/permissionedPools/Perm import {Plan, Planner} from "../../../shared/Planner.sol"; import {PathKey} from "../../../../src/libraries/PathKey.sol"; import {Actions} from "../../../../src/libraries/Actions.sol"; -import {WrappedPermissionedToken} from "../../../../src/hooks/permissionedPools/WrappedPermissionedToken.sol"; +import {PermissionsAdapter} from "../../../../src/hooks/permissionedPools/PermissionsAdapter.sol"; import {MockAllowlistChecker, MockPermissionedToken} from "../PermissionedPoolsBase.sol"; import {IAllowlistChecker} from "../../../../src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol"; import {IPositionManager} from "../../../../src/interfaces/IPositionManager.sol"; -import {IWrappedPermissionedTokenFactory} from - "../../../../src/hooks/permissionedPools/interfaces/IWrappedPermissionedTokenFactory.sol"; +import {IPermissionsAdapterFactory} from + "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; import {IWETH9} from "../../../../src/interfaces/external/IWETH9.sol"; import {IPositionDescriptor} from "../../../../src/interfaces/IPositionDescriptor.sol"; import {IV4Router} from "../../../../src/interfaces/IV4Router.sol"; @@ -53,22 +53,22 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 Currency public currency3; Currency public currency4; - WrappedPermissionedToken public wrappedToken0; - WrappedPermissionedToken public wrappedToken1; + PermissionsAdapter public permissionsAdapter0; + PermissionsAdapter public permissionsAdapter1; Currency[] public tokenPath; Plan public plan; address public positionManager; - mapping(Currency wrappedCurrency => Currency permissionedCurrency) public wrappedToPermissioned; + mapping(Currency permissionsAdapter => Currency permissionedCurrency) public adapterToPermissioned; function setupPermissionedRouterCurrenciesAndPoolsWithLiquidity(address spender) public { _deployFreshManager(); _deployWETH(); _deployPositionDescriptor(); _deployPermit2(); - _deployWrappedTokenFactory(); + _deployPemissionsAdapterFactory(); _deployPermissionedHooks(); _deployMockPermissionedRouter(); _deployPositionManager(); @@ -99,7 +99,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 } function getPermissionedCurrency(Currency currency) public view returns (Currency) { - Currency permissionedCurrency = wrappedToPermissioned[currency]; + Currency permissionedCurrency = adapterToPermissioned[currency]; if (permissionedCurrency == Currency.wrap(address(0))) { return currency; } @@ -110,52 +110,56 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 _setupMockAllowList(currency0, spender); _setupMockAllowList(currency1, spender); while (true) { - wrappedToken0 = WrappedPermissionedToken( - wrappedTokenFactory.createWrappedPermissionedToken( + permissionsAdapter0 = PermissionsAdapter( + permissionsAdapterFactory.createPermissionsAdapter( IERC20(Currency.unwrap(currency0)), address(this), mockAllowlistChecker ) ); - wrappedToken1 = WrappedPermissionedToken( - wrappedTokenFactory.createWrappedPermissionedToken( + permissionsAdapter1 = PermissionsAdapter( + permissionsAdapterFactory.createPermissionsAdapter( IERC20(Currency.unwrap(currency1)), address(this), mockAllowlistChecker ) ); - if (address(wrappedToken0) > address(wrappedToken1)) { + if (address(permissionsAdapter0) > address(permissionsAdapter1)) { break; } } MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist( - address(wrappedToken0), PermissionFlags.ALL_ALLOWED + address(permissionsAdapter0), PermissionFlags.ALL_ALLOWED ); MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist( - address(wrappedToken1), PermissionFlags.ALL_ALLOWED + address(permissionsAdapter1), PermissionFlags.ALL_ALLOWED ); - // Transfer some underlying tokens to the wrapped tokens for verification - IERC20(Currency.unwrap(currency0)).transfer(address(wrappedToken0), 1); - IERC20(Currency.unwrap(currency1)).transfer(address(wrappedToken1), 1); + // Transfer some underlying tokens to the permissions adapter for verification + IERC20(Currency.unwrap(currency0)).transfer(address(permissionsAdapter0), 1); + IERC20(Currency.unwrap(currency1)).transfer(address(permissionsAdapter1), 1); verifyTokensAndAddWrappers(); - wrappedToPermissioned[Currency.wrap(address(wrappedToken0))] = currency0; - wrappedToPermissioned[Currency.wrap(address(wrappedToken1))] = currency1; + adapterToPermissioned[Currency.wrap(address(permissionsAdapter0))] = currency0; + adapterToPermissioned[Currency.wrap(address(permissionsAdapter1))] = currency1; } function verifyTokensAndAddWrappers() private { - wrappedTokenFactory.verifyWrappedToken(address(wrappedToken0)); - wrappedTokenFactory.verifyWrappedToken(address(wrappedToken1)); - - wrappedToken0.updateAllowedWrapper(address(this), true); - wrappedToken1.updateAllowedWrapper(address(this), true); - wrappedToken0.updateAllowedWrapper(positionManager, true); - wrappedToken1.updateAllowedWrapper(positionManager, true); - wrappedToken0.updateAllowedWrapper(address(permissionedRouter), true); - wrappedToken1.updateAllowedWrapper(address(permissionedRouter), true); - wrappedToken0.updateAllowedWrapper(address(permissionedHooks), true); - wrappedToken1.updateAllowedWrapper(address(permissionedHooks), true); - - setAllowedHooks(IPositionManager(positionManager), Currency.wrap(address(wrappedToken0)), permissionedHooks); - setAllowedHooks(IPositionManager(positionManager), Currency.wrap(address(wrappedToken1)), permissionedHooks); + permissionsAdapterFactory.verifyPermissionsAdapter(address(permissionsAdapter0)); + permissionsAdapterFactory.verifyPermissionsAdapter(address(permissionsAdapter1)); + + permissionsAdapter0.updateAllowedWrapper(address(this), true); + permissionsAdapter1.updateAllowedWrapper(address(this), true); + permissionsAdapter0.updateAllowedWrapper(positionManager, true); + permissionsAdapter1.updateAllowedWrapper(positionManager, true); + permissionsAdapter0.updateAllowedWrapper(address(permissionedRouter), true); + permissionsAdapter1.updateAllowedWrapper(address(permissionedRouter), true); + permissionsAdapter0.updateAllowedWrapper(address(permissionedHooks), true); + permissionsAdapter1.updateAllowedWrapper(address(permissionedHooks), true); + + setAllowedHooks( + IPositionManager(positionManager), Currency.wrap(address(permissionsAdapter0)), permissionedHooks + ); + setAllowedHooks( + IPositionManager(positionManager), Currency.wrap(address(permissionsAdapter1)), permissionedHooks + ); } function setAllowedHooks(IPositionManager posm, Currency currency, IHooks permissionedHooks_) internal { @@ -366,24 +370,23 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 permit2 = IAllowanceTransfer(deployPermit2()); } - function _deployWrappedTokenFactory() private { - bytes memory wrappedTokenFactoryBytecode = abi.encodePacked( - vm.getCode("WrappedPermissionedTokenFactory.sol:WrappedPermissionedTokenFactory"), - abi.encode(address(manager)) + function _deployPemissionsAdapterFactory() private { + bytes memory permissionsAdapterFactoryBytecode = abi.encodePacked( + vm.getCode("PermissionsAdapterFactory.sol:PermissionsAdapterFactory"), abi.encode(address(manager)) ); - wrappedTokenFactory = IWrappedPermissionedTokenFactory( - Deploy.create2(wrappedTokenFactoryBytecode, keccak256("wrappedTokenFactory")) + permissionsAdapterFactory = IPermissionsAdapterFactory( + Deploy.create2(permissionsAdapterFactoryBytecode, keccak256("permissionsAdapterFactory")) ); } function _deployPermissionedHooks() private { - permissionedHooks = IHooks(deployPermissionedHooks(address(manager), address(wrappedTokenFactory))); + permissionedHooks = IHooks(deployPermissionedHooks(address(manager), address(permissionsAdapterFactory))); } function _deployMockPermissionedRouter() private { bytes memory routerBytecode = abi.encodePacked( vm.getCode("PermissionedV4Router.sol:PermissionedV4Router"), - abi.encode(manager, permit2, wrappedTokenFactory, weth9) + abi.encode(manager, permit2, permissionsAdapterFactory, weth9) ); permissionedRouter = PermissionedV4Router(payable(Deploy.create2(routerBytecode, keccak256("permissionedSwapRouter")))); @@ -392,7 +395,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 function _deployPositionManager() private { bytes memory posmBytecode = abi.encodePacked( vm.getCode("PermissionedPositionManager.sol:PermissionedPositionManager"), - abi.encode(manager, permit2, 1e18, address(tokenDescriptor), address(weth9), wrappedTokenFactory) + abi.encode(manager, permit2, 1e18, address(tokenDescriptor), address(weth9), permissionsAdapterFactory) ); positionManager = Deploy.create2(posmBytecode, keccak256("permissionedPosm")); } @@ -417,13 +420,18 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 } function _createPoolsWithLiquidity() private { - nativeKey = createNativePoolWithLiquidity(Currency.wrap(address(wrappedToken0)), address(permissionedHooks)); + nativeKey = + createNativePoolWithLiquidity(Currency.wrap(address(permissionsAdapter0)), address(permissionedHooks)); key0 = createPoolWithLiquidity( - Currency.wrap(address(wrappedToken0)), Currency.wrap(address(wrappedToken1)), address(permissionedHooks) + Currency.wrap(address(permissionsAdapter0)), + Currency.wrap(address(permissionsAdapter1)), + address(permissionedHooks) ); - key1 = createPoolWithLiquidity(Currency.wrap(address(wrappedToken1)), currency2, address(permissionedHooks)); + key1 = + createPoolWithLiquidity(Currency.wrap(address(permissionsAdapter1)), currency2, address(permissionedHooks)); key2 = createPoolWithLiquidity(currency2, currency3, address(permissionedHooks)); - key3 = createPoolWithLiquidity(Currency.wrap(address(wrappedToken0)), currency4, address(permissionedHooks)); + key3 = + createPoolWithLiquidity(Currency.wrap(address(permissionsAdapter0)), currency4, address(permissionedHooks)); } function _setupMockAllowList(Currency currency, address spender) private { @@ -433,7 +441,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 mockPermissionedToken.setAllowlist(address(permissionedRouter), PermissionFlags.ALL_ALLOWED); mockPermissionedToken.setAllowlist(address(permissionedHooks), PermissionFlags.ALL_ALLOWED); mockPermissionedToken.setAllowlist(address(positionManager), PermissionFlags.ALL_ALLOWED); - mockPermissionedToken.setAllowlist(address(wrappedTokenFactory), PermissionFlags.ALL_ALLOWED); + mockPermissionedToken.setAllowlist(address(permissionsAdapterFactory), PermissionFlags.ALL_ALLOWED); mockPermissionedToken.setAllowlist(address(manager), PermissionFlags.ALL_ALLOWED); mockPermissionedToken.setAllowlist(address(permit2), PermissionFlags.ALL_ALLOWED); mockPermissionedToken.setAllowlist(spender, PermissionFlags.ALL_ALLOWED); diff --git a/test/shared/Deploy.sol b/test/shared/Deploy.sol index 1a8d64962..ee90db98c 100644 --- a/test/shared/Deploy.sol +++ b/test/shared/Deploy.sol @@ -33,11 +33,11 @@ library Deploy { uint256 unsubscribeGasLimit, address positionDescriptor_, address wrappedNative, - address wrappedTokenFactory, + address permissionsAdapterFactory, bytes memory salt ) internal returns (IPositionManager manager) { bytes memory args = abi.encode( - poolManager, permit2, unsubscribeGasLimit, positionDescriptor_, wrappedNative, wrappedTokenFactory + poolManager, permit2, unsubscribeGasLimit, positionDescriptor_, wrappedNative, permissionsAdapterFactory ); bytes memory initcode = abi.encodePacked(vm.getCode("PermissionedPositionManager.sol:PermissionedPositionManager"), args); From 334a58ca5028ebab4050bb63e4b0ae2bec059c82 Mon Sep 17 00:00:00 2001 From: Daniel Gretzke Date: Mon, 22 Sep 2025 16:49:00 +0200 Subject: [PATCH 12/33] emit event when hooks are allowed/disallowed (#498) --- src/hooks/permissionedPools/PermissionedPositionManager.sol | 5 +++++ .../permissionedPools/PermissionedPositionManager.t.sol | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index 6973eec94..ce5dd62ad 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -20,6 +20,8 @@ contract PermissionedPositionManager is PositionManager { mapping(Currency currency => mapping(IHooks hooks => bool)) public isAllowedHooks; + event AllowedHooksUpdated(Currency currency, IHooks hooks, bool allowed); + error InvalidHook(); error SafeTransferDisabled(); error NotPermissionsAdapterAdmin(); @@ -45,7 +47,10 @@ contract PermissionedPositionManager is PositionManager { if (_getOwner(currency) != msg.sender) { revert NotPermissionsAdapterAdmin(); } + bool oldAllowed = isAllowedHooks[currency][hooks]; + if (oldAllowed == allowed) return; isAllowedHooks[currency][hooks] = allowed; + emit AllowedHooksUpdated(currency, hooks, allowed); } /// @inheritdoc PositionManager diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 0008641bf..dd5d0e8ce 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -226,7 +226,7 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq } function setAllowedHooks(IPositionManager posm, Currency currency, IHooks permissionedHooks_) internal { - // addPermissionedHooks selector + // setAllowedHook selector bytes4 selector = 0xb5cdc484; bytes memory data = abi.encodeWithSelector(selector, currency, permissionedHooks_, true); (bool success,) = address(posm).call(data); From fbf665bacc8aa0cf7002750598977b56902064f5 Mon Sep 17 00:00:00 2001 From: gretzke Date: Tue, 23 Sep 2025 23:23:46 +0200 Subject: [PATCH 13/33] spelling fix --- .../permissionedPools/PermissionedHooks.sol | 10 +-- .../PermissionedPositionManager.sol | 16 ++-- .../PermissionedV4Router.sol | 12 +-- .../PermissionsAdapterFactory.sol | 30 ++++---- .../interfaces/IPermissionsAdapterFactory.sol | 22 +++--- .../PermissionedPositionManager.t.sol | 30 ++++---- .../PermissionsAdapter.t.sol | 74 +++++++++---------- .../PermissionsAdapterFactory.t.sol | 44 +++++------ 8 files changed, 120 insertions(+), 118 deletions(-) diff --git a/src/hooks/permissionedPools/PermissionedHooks.sol b/src/hooks/permissionedPools/PermissionedHooks.sol index 06175abe0..0044de79b 100644 --- a/src/hooks/permissionedPools/PermissionedHooks.sol +++ b/src/hooks/permissionedPools/PermissionedHooks.sol @@ -60,21 +60,21 @@ contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { } /// @dev checks if the provided token is a permissioned token by checking if it has a verified permissions adapter, if yes, check the allowlist and check whether swapping is enabled - function _isAllowed(address pemissionsAdapter, address sender, address router, bytes4 selector) internal view { - address permissionedToken = PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(pemissionsAdapter); + function _isAllowed(address permissionsAdapter, address sender, address router, bytes4 selector) internal view { + address permissionedToken = PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(permissionsAdapter); if (permissionedToken == address(0)) return; PermissionFlag permission = PermissionFlags.NONE; if (selector == this.beforeSwap.selector) { permission = PermissionFlags.SWAP_ALLOWED; - if (!IPermissionsAdapter(pemissionsAdapter).swappingEnabled()) revert SwappingDisabled(); + if (!IPermissionsAdapter(permissionsAdapter).swappingEnabled()) revert SwappingDisabled(); } else if (selector == this.beforeAddLiquidity.selector) { permission = PermissionFlags.LIQUIDITY_ALLOWED; } if ( - !IPermissionsAdapter(pemissionsAdapter).isAllowed(sender, permission) - || !IPermissionsAdapter(pemissionsAdapter).allowedWrappers(router) + !IPermissionsAdapter(permissionsAdapter).isAllowed(sender, permission) + || !IPermissionsAdapter(permissionsAdapter).allowedWrappers(router) ) revert Unauthorized(); } } diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index ce5dd62ad..06ea15a88 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -110,18 +110,18 @@ contract PermissionedPositionManager is PositionManager { return; } // token is permissioned, wrap the token and transfer it to the pool manager - IPermissionsAdapter pemissionsAdapter = IPermissionsAdapter(Currency.unwrap(currency)); + IPermissionsAdapter permissionsAdapter = IPermissionsAdapter(Currency.unwrap(currency)); if (payer == address(this)) { // @audit is it necessary to check the allowlist here? - if (!pemissionsAdapter.isAllowed(msgSender(), PermissionFlags.LIQUIDITY_ALLOWED)) { + if (!permissionsAdapter.isAllowed(msgSender(), PermissionFlags.LIQUIDITY_ALLOWED)) { revert Unauthorized(); } - Currency.wrap(permissionedToken).transfer(address(pemissionsAdapter), amount); - pemissionsAdapter.wrapToPoolManager(amount); + Currency.wrap(permissionedToken).transfer(address(permissionsAdapter), amount); + permissionsAdapter.wrapToPoolManager(amount); } else { // token is a permissioned token, wrap the token - permit2.transferFrom(payer, address(pemissionsAdapter), uint160(amount), permissionedToken); - pemissionsAdapter.wrapToPoolManager(amount); + permit2.transferFrom(payer, address(permissionsAdapter), uint160(amount), permissionedToken); + permissionsAdapter.wrapToPoolManager(amount); } } @@ -130,9 +130,9 @@ contract PermissionedPositionManager is PositionManager { } function _getOwner(Currency currency) internal view returns (address) { - address pemissionsAdapter = Currency.unwrap(currency); + address permissionsAdapter = Currency.unwrap(currency); address permissionedToken = _verifiedPermissionedTokenOf(currency); if (permissionedToken == address(0)) return address(0); - return IPermissionsAdapter(pemissionsAdapter).owner(); + return IPermissionsAdapter(permissionsAdapter).owner(); } } diff --git a/src/hooks/permissionedPools/PermissionedV4Router.sol b/src/hooks/permissionedPools/PermissionedV4Router.sol index c755f06b7..fec08a4f8 100644 --- a/src/hooks/permissionedPools/PermissionedV4Router.sol +++ b/src/hooks/permissionedPools/PermissionedV4Router.sol @@ -137,18 +137,18 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { return; } // token is permissioned, wrap the token and transfer it to the pool manager - IPermissionsAdapter pemissionsAdapter = IPermissionsAdapter(Currency.unwrap(currency)); + IPermissionsAdapter permissionsAdapter = IPermissionsAdapter(Currency.unwrap(currency)); if (payer == address(this)) { // allowlist check necessary to ensure a disallowed user cannot sell a permissioned token - if (!pemissionsAdapter.isAllowed(msgSender(), PermissionFlags.SWAP_ALLOWED)) { + if (!permissionsAdapter.isAllowed(msgSender(), PermissionFlags.SWAP_ALLOWED)) { revert Unauthorized(); } - Currency.wrap(permissionedToken).transfer(address(pemissionsAdapter), amount); - pemissionsAdapter.wrapToPoolManager(amount); + Currency.wrap(permissionedToken).transfer(address(permissionsAdapter), amount); + permissionsAdapter.wrapToPoolManager(amount); } else { // token is a permissioned token, wrap the token - PERMIT2.transferFrom(payer, address(pemissionsAdapter), uint160(amount), permissionedToken); - pemissionsAdapter.wrapToPoolManager(amount); + PERMIT2.transferFrom(payer, address(permissionsAdapter), uint160(amount), permissionedToken); + permissionsAdapter.wrapToPoolManager(amount); } } diff --git a/src/hooks/permissionedPools/PermissionsAdapterFactory.sol b/src/hooks/permissionedPools/PermissionsAdapterFactory.sol index 83c0a3942..bbb9bfe67 100644 --- a/src/hooks/permissionedPools/PermissionsAdapterFactory.sol +++ b/src/hooks/permissionedPools/PermissionsAdapterFactory.sol @@ -10,9 +10,9 @@ contract PermissionsAdapterFactory is IPermissionsAdapterFactory { address public immutable POOL_MANAGER; /// @inheritdoc IPermissionsAdapterFactory - mapping(address pemissionsAdapter => address permissionedToken) public permissionsAdapterOf; + mapping(address permissionsAdapter => address permissionedToken) public permissionsAdapterOf; /// @inheritdoc IPermissionsAdapterFactory - mapping(address pemissionsAdapter => address permissionedToken) public verifiedPermissionsAdapterOf; + mapping(address permissionsAdapter => address permissionedToken) public verifiedPermissionsAdapterOf; constructor(address poolManager) { POOL_MANAGER = poolManager; @@ -23,23 +23,25 @@ contract PermissionsAdapterFactory is IPermissionsAdapterFactory { IERC20 permissionedToken, address initialOwner, IAllowlistChecker allowListChecker - ) external returns (address pemissionsAdapter) { - pemissionsAdapter = + ) external returns (address permissionsAdapter) { + permissionsAdapter = address(new PermissionsAdapter(permissionedToken, POOL_MANAGER, initialOwner, allowListChecker)); - permissionsAdapterOf[pemissionsAdapter] = address(permissionedToken); - emit PermissionsAdapterCreated(pemissionsAdapter, address(permissionedToken)); + permissionsAdapterOf[permissionsAdapter] = address(permissionedToken); + emit PermissionsAdapterCreated(permissionsAdapter, address(permissionedToken)); } /// @inheritdoc IPermissionsAdapterFactory - function verifyPermissionsAdapter(address pemissionsAdapter) external { - IERC20 permissionedToken = IERC20(permissionsAdapterOf[pemissionsAdapter]); - if (address(permissionedToken) == address(0)) revert PemissionsAdapterNotFound(pemissionsAdapter); - if (verifiedPermissionsAdapterOf[pemissionsAdapter] != address(0)) { - revert PemissionsAdapterAlreadyVerified(pemissionsAdapter); + function verifyPermissionsAdapter(address permissionsAdapter) external { + IERC20 permissionedToken = IERC20(permissionsAdapterOf[permissionsAdapter]); + if (address(permissionedToken) == address(0)) revert PemissionsAdapterNotFound(permissionsAdapter); + if (verifiedPermissionsAdapterOf[permissionsAdapter] != address(0)) { + revert PemissionsAdapterAlreadyVerified(permissionsAdapter); } // this requires that the verifier has some comntrol or ownership of the permissioned token - if (permissionedToken.balanceOf(pemissionsAdapter) == 0) revert PemissionsAdapterNotVerified(pemissionsAdapter); - verifiedPermissionsAdapterOf[pemissionsAdapter] = address(permissionedToken); - emit PemissionsAdapterVerified(pemissionsAdapter, address(permissionedToken)); + if (permissionedToken.balanceOf(permissionsAdapter) == 0) { + revert PemissionsAdapterNotVerified(permissionsAdapter); + } + verifiedPermissionsAdapterOf[permissionsAdapter] = address(permissionedToken); + emit PemissionsAdapterVerified(permissionsAdapter, address(permissionedToken)); } } diff --git a/src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol b/src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol index 26b6f30fb..4ced6cd74 100644 --- a/src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol +++ b/src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol @@ -6,19 +6,19 @@ import {IAllowlistChecker} from "./IAllowlistChecker.sol"; interface IPermissionsAdapterFactory { /// @notice Emitted when a permissions adapter is created - event PermissionsAdapterCreated(address indexed pemissionsAdapter, address indexed permissionedToken); + event PermissionsAdapterCreated(address indexed permissionsAdapter, address indexed permissionedToken); /// @notice Emitted when a permissions adapter is verified - event PemissionsAdapterVerified(address indexed pemissionsAdapter, address indexed permissionedToken); + event PemissionsAdapterVerified(address indexed permissionsAdapter, address indexed permissionedToken); /// @notice Thrown when the permissions adapter does not exist - error PemissionsAdapterNotFound(address pemissionsAdapter); + error PemissionsAdapterNotFound(address permissionsAdapter); /// @notice Thrown when the permissions adapter is already verified - error PemissionsAdapterAlreadyVerified(address pemissionsAdapter); + error PemissionsAdapterAlreadyVerified(address permissionsAdapter); /// @notice Thrown when the permissions adapter is not verified - error PemissionsAdapterNotVerified(address pemissionsAdapter); + error PemissionsAdapterNotVerified(address permissionsAdapter); /// @notice Creates a new permissions adapter /// @param permissionedToken The permissioned token to wrap @@ -31,20 +31,20 @@ interface IPermissionsAdapterFactory { ) external returns (address); /// @notice Verifies a permissions adapter - /// @param pemissionsAdapter The permissions adapter + /// @param permissionsAdapter The permissions adapter /// @dev This function verifies that the permissions adapter has a balance of the permissioned token. This means that the permissions adapter is on the allow list of the permissioned token and can be used to wrap and unwrap the permissioned token. - function verifyPermissionsAdapter(address pemissionsAdapter) external; + function verifyPermissionsAdapter(address permissionsAdapter) external; /// @notice Returns the permissioned token of a permissions adapter - /// @param pemissionsAdapter The permissions adapter + /// @param permissionsAdapter The permissions adapter /// @return permissionedToken The permissioned token - function permissionsAdapterOf(address pemissionsAdapter) external view returns (address permissionedToken); + function permissionsAdapterOf(address permissionsAdapter) external view returns (address permissionedToken); /// @notice Returns the verified permissioned token of a permissions adapter - /// @param pemissionsAdapter The permissions adapter + /// @param permissionsAdapter The permissions adapter /// @return permissionedToken The verified permissioned token /// @dev A reverse lookup of the permissioned token is required, otherwise anyone could create a permissions adapter for a non-permissioned token - function verifiedPermissionsAdapterOf(address pemissionsAdapter) + function verifiedPermissionsAdapterOf(address permissionsAdapter) external view returns (address permissionedToken); diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index dd5d0e8ce..39477dfb9 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -197,32 +197,32 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq ); } - function setUpPemissionsAdapter(PermissionsAdapter pemissionsAdapter, Currency currency) internal { - adapterToPermissioned[Currency.wrap(address(pemissionsAdapter))] = currency; + function setUpPemissionsAdapter(PermissionsAdapter permissionsAdapter, Currency currency) internal { + adapterToPermissioned[Currency.wrap(address(permissionsAdapter))] = currency; MockPermissionedToken(Currency.unwrap(currency)).mint(address(this), 1000 ether); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( - address(pemissionsAdapter), PermissionFlags.ALL_ALLOWED + address(permissionsAdapter), PermissionFlags.ALL_ALLOWED ); // permissions adapter contract must have a non-zero balance of the permissioned token - currency.transfer(address(pemissionsAdapter), 1); + currency.transfer(address(permissionsAdapter), 1); - pemissionsAdapter.updateAllowedWrapper(address(manager), true); - pemissionsAdapter.updateAllowedWrapper(address(lpm), true); - pemissionsAdapter.updateAllowedWrapper(address(secondaryPosm), true); - pemissionsAdapter.updateAllowedWrapper(address(permissionedSwapRouter), true); + permissionsAdapter.updateAllowedWrapper(address(manager), true); + permissionsAdapter.updateAllowedWrapper(address(lpm), true); + permissionsAdapter.updateAllowedWrapper(address(secondaryPosm), true); + permissionsAdapter.updateAllowedWrapper(address(permissionedSwapRouter), true); - permissionsAdapterFactory.verifyPermissionsAdapter(address(pemissionsAdapter)); + permissionsAdapterFactory.verifyPermissionsAdapter(address(permissionsAdapter)); - Currency permissionsAdapter = Currency.wrap(address(pemissionsAdapter)); + Currency permissionsAdapterCurrency = Currency.wrap(address(permissionsAdapter)); - setAllowedHooks(lpm, permissionsAdapter, permissionedHooks); - setAllowedHooks(tertiaryPosm, permissionsAdapter, permissionedHooks); + setAllowedHooks(lpm, permissionsAdapterCurrency, permissionedHooks); + setAllowedHooks(tertiaryPosm, permissionsAdapterCurrency, permissionedHooks); - setAllowedHooks(lpm, permissionsAdapter, secondaryPermissionedHooks); - setAllowedHooks(secondaryPosm, permissionsAdapter, secondaryPermissionedHooks); - setAllowedHooks(tertiaryPosm, permissionsAdapter, secondaryPermissionedHooks); + setAllowedHooks(lpm, permissionsAdapterCurrency, secondaryPermissionedHooks); + setAllowedHooks(secondaryPosm, permissionsAdapterCurrency, secondaryPermissionedHooks); + setAllowedHooks(tertiaryPosm, permissionsAdapterCurrency, secondaryPermissionedHooks); } function setAllowedHooks(IPositionManager posm, Currency currency, IHooks permissionedHooks_) internal { diff --git a/test/hooks/permissionedPools/PermissionsAdapter.t.sol b/test/hooks/permissionedPools/PermissionsAdapter.t.sol index 49d932143..96b72c0be 100644 --- a/test/hooks/permissionedPools/PermissionsAdapter.t.sol +++ b/test/hooks/permissionedPools/PermissionsAdapter.t.sol @@ -11,7 +11,7 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {PermissionFlags, PermissionFlag} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; contract PermissionsAdapterTest is PermissionedPoolsBase { - IPermissionsAdapter public pemissionsAdapter; + IPermissionsAdapter public permissionsAdapter; address public mockPoolManager; address public owner; @@ -22,79 +22,79 @@ contract PermissionsAdapterTest is PermissionedPoolsBase { bytes memory args = abi.encode(permissionedToken, mockPoolManager, owner, allowlistChecker); bytes memory initcode = abi.encodePacked(vm.getCode("PermissionsAdapter.sol:PermissionsAdapter"), args); assembly { - sstore(pemissionsAdapter.slot, create(0, add(initcode, 0x20), mload(initcode))) + sstore(permissionsAdapter.slot, create(0, add(initcode, 0x20), mload(initcode))) } - permissionedToken.setTokenAllowlist(address(pemissionsAdapter), true); + permissionedToken.setTokenAllowlist(address(permissionsAdapter), true); vm.prank(owner); - pemissionsAdapter.updateAllowedWrapper(address(this), true); + permissionsAdapter.updateAllowedWrapper(address(this), true); } function test_InitialState() public view { - assertEq(IERC20Metadata(address(pemissionsAdapter)).name(), "Uniswap v4 MockToken"); - assertEq(IERC20Metadata(address(pemissionsAdapter)).symbol(), "v4MT"); - assertEq(IERC20Metadata(address(pemissionsAdapter)).decimals(), permissionedToken.decimals()); - assertEq(pemissionsAdapter.totalSupply(), 0); - assertEq(pemissionsAdapter.balanceOf(mockPoolManager), 0); - assertEq(address(pemissionsAdapter.allowListChecker()), address(allowlistChecker)); - assertEq(pemissionsAdapter.POOL_MANAGER(), mockPoolManager); - assertEq(address(pemissionsAdapter.PERMISSIONED_TOKEN()), address(permissionedToken)); + assertEq(IERC20Metadata(address(permissionsAdapter)).name(), "Uniswap v4 MockToken"); + assertEq(IERC20Metadata(address(permissionsAdapter)).symbol(), "v4MT"); + assertEq(IERC20Metadata(address(permissionsAdapter)).decimals(), permissionedToken.decimals()); + assertEq(permissionsAdapter.totalSupply(), 0); + assertEq(permissionsAdapter.balanceOf(mockPoolManager), 0); + assertEq(address(permissionsAdapter.allowListChecker()), address(allowlistChecker)); + assertEq(permissionsAdapter.POOL_MANAGER(), mockPoolManager); + assertEq(address(permissionsAdapter.PERMISSIONED_TOKEN()), address(permissionedToken)); } function testRevert_WhenNotOwner(address account) public { vm.assume(account != owner); vm.startPrank(account); vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, account)); - pemissionsAdapter.updateAllowedWrapper(account, true); + permissionsAdapter.updateAllowedWrapper(account, true); vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, account)); - pemissionsAdapter.updateAllowListChecker(allowlistChecker); + permissionsAdapter.updateAllowListChecker(allowlistChecker); vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, account)); - pemissionsAdapter.updateSwappingEnabled(true); + permissionsAdapter.updateSwappingEnabled(true); vm.stopPrank(); } function testRevert_WhenNotAllowedWrapper(address wrapper) public { vm.assume(wrapper != address(this)); - assertFalse(pemissionsAdapter.allowedWrappers(wrapper)); + assertFalse(permissionsAdapter.allowedWrappers(wrapper)); vm.prank(wrapper); vm.expectRevert(abi.encodeWithSelector(IPermissionsAdapter.UnauthorizedWrapper.selector, wrapper)); - pemissionsAdapter.wrapToPoolManager(100); + permissionsAdapter.wrapToPoolManager(100); } function testRevert_WhenInsufficientBalance(uint256 amount, uint256 transferAmount) public { vm.assume(amount != 0); transferAmount = bound(amount, 0, amount - 1); - permissionedToken.mint(address(pemissionsAdapter), transferAmount); + permissionedToken.mint(address(permissionsAdapter), transferAmount); vm.expectRevert( abi.encodeWithSelector(IPermissionsAdapter.InsufficientBalance.selector, amount, transferAmount) ); - pemissionsAdapter.wrapToPoolManager(amount); + permissionsAdapter.wrapToPoolManager(amount); } function test_WrapToPoolManager(uint256 amount, uint256 actualAmount) public { actualAmount = bound(amount, amount, type(uint256).max); - permissionedToken.mint(address(pemissionsAdapter), actualAmount); + permissionedToken.mint(address(permissionsAdapter), actualAmount); vm.expectEmit(true, true, true, true); emit IERC20.Transfer(address(0), mockPoolManager, amount); - pemissionsAdapter.wrapToPoolManager(amount); - assertEq(pemissionsAdapter.balanceOf(mockPoolManager), amount); + permissionsAdapter.wrapToPoolManager(amount); + assertEq(permissionsAdapter.balanceOf(mockPoolManager), amount); assertEq(permissionedToken.balanceOf(mockPoolManager), actualAmount - amount); } function test_UpdateAllowedWrapper(address wrapper, bool allowed) public { vm.assume(wrapper != address(this)); - assertFalse(pemissionsAdapter.allowedWrappers(wrapper)); + assertFalse(permissionsAdapter.allowedWrappers(wrapper)); vm.prank(owner); vm.expectEmit(true, true, true, true); emit IPermissionsAdapter.AllowedWrapperUpdated(wrapper, allowed); - pemissionsAdapter.updateAllowedWrapper(wrapper, allowed); - assertEq(pemissionsAdapter.allowedWrappers(wrapper), allowed); + permissionsAdapter.updateAllowedWrapper(wrapper, allowed); + assertEq(permissionsAdapter.allowedWrappers(wrapper), allowed); } function testRevert_WhenNotSupportedInterfaceEOA(IAllowlistChecker newAllowListCheckerEOA) public { vm.assume(newAllowListCheckerEOA != allowlistChecker); vm.prank(owner); vm.expectRevert(); // expect revert without data - pemissionsAdapter.updateAllowListChecker(newAllowListCheckerEOA); + permissionsAdapter.updateAllowListChecker(newAllowListCheckerEOA); } function testRevert_WhenNotSupportedInterfaceContract() public { @@ -103,7 +103,7 @@ contract PermissionsAdapterTest is PermissionedPoolsBase { vm.expectRevert( abi.encodeWithSelector(IPermissionsAdapter.InvalidAllowListChecker.selector, newAllowListCheckerContract) ); - pemissionsAdapter.updateAllowListChecker(newAllowListCheckerContract); + permissionsAdapter.updateAllowListChecker(newAllowListCheckerContract); } function test_UpdateAllowListChecker() public { @@ -111,16 +111,16 @@ contract PermissionsAdapterTest is PermissionedPoolsBase { vm.prank(owner); vm.expectEmit(true, true, true, true); emit IPermissionsAdapter.AllowListCheckerUpdated(newAllowListChecker); - pemissionsAdapter.updateAllowListChecker(newAllowListChecker); - assertEq(address(pemissionsAdapter.allowListChecker()), address(newAllowListChecker)); + permissionsAdapter.updateAllowListChecker(newAllowListChecker); + assertEq(address(permissionsAdapter.allowListChecker()), address(newAllowListChecker)); } function test_UpdateSwappingEnabled(bool enabled) public { vm.prank(owner); vm.expectEmit(true, true, true, true); emit IPermissionsAdapter.SwappingEnabledUpdated(enabled); - pemissionsAdapter.updateSwappingEnabled(enabled); - assertEq(pemissionsAdapter.swappingEnabled(), enabled); + permissionsAdapter.updateSwappingEnabled(enabled); + assertEq(permissionsAdapter.swappingEnabled(), enabled); } function testRevert_WhenInvalidTransfer(address from, address to) public { @@ -128,23 +128,23 @@ contract PermissionsAdapterTest is PermissionedPoolsBase { vm.assume(to != address(0) && to != mockPoolManager); vm.prank(from); vm.expectRevert(abi.encodeWithSelector(IPermissionsAdapter.InvalidTransfer.selector, from, to)); - pemissionsAdapter.transfer(to, 0); + permissionsAdapter.transfer(to, 0); } function test_UnwrapOnPoolManagerTransfer(uint256 mintAmount, uint256 transferAmount, address recipient) public { - vm.assume(recipient != address(0) && recipient != mockPoolManager && recipient != address(pemissionsAdapter)); + vm.assume(recipient != address(0) && recipient != mockPoolManager && recipient != address(permissionsAdapter)); assertEq(permissionedToken.balanceOf(recipient), 0); permissionedToken.setTokenAllowlist(recipient, true); transferAmount = bound(transferAmount, 0, mintAmount); - permissionedToken.mint(address(pemissionsAdapter), mintAmount); - pemissionsAdapter.wrapToPoolManager(mintAmount); + permissionedToken.mint(address(permissionsAdapter), mintAmount); + permissionsAdapter.wrapToPoolManager(mintAmount); vm.prank(mockPoolManager); vm.expectEmit(true, true, true, true); emit IERC20.Transfer(mockPoolManager, recipient, transferAmount); vm.expectEmit(true, true, true, true); emit IERC20.Transfer(recipient, address(0), transferAmount); - pemissionsAdapter.transfer(recipient, transferAmount); - assertEq(pemissionsAdapter.balanceOf(mockPoolManager), mintAmount - transferAmount); + permissionsAdapter.transfer(recipient, transferAmount); + assertEq(permissionsAdapter.balanceOf(mockPoolManager), mintAmount - transferAmount); assertEq(permissionedToken.balanceOf(recipient), transferAmount); } } diff --git a/test/hooks/permissionedPools/PermissionsAdapterFactory.t.sol b/test/hooks/permissionedPools/PermissionsAdapterFactory.t.sol index 798aec775..fd94e1707 100644 --- a/test/hooks/permissionedPools/PermissionsAdapterFactory.t.sol +++ b/test/hooks/permissionedPools/PermissionsAdapterFactory.t.sol @@ -38,54 +38,54 @@ contract PermissionsAdapterFactoryTest is PermissionedPoolsBase { emit IPermissionsAdapterFactory.PermissionsAdapterCreated( expectedPermissionsAdapter, address(permissionedToken) ); - address pemissionsAdapter = + address permissionsAdapter = permissionsAdapterFactory.createPermissionsAdapter(permissionedToken, initialOwner, allowlistChecker); - assertEq(pemissionsAdapter, expectedPermissionsAdapter); - assertEq(permissionsAdapterFactory.permissionsAdapterOf(pemissionsAdapter), address(permissionedToken)); - assertEq(permissionsAdapterFactory.verifiedPermissionsAdapterOf(pemissionsAdapter), address(0)); + assertEq(permissionsAdapter, expectedPermissionsAdapter); + assertEq(permissionsAdapterFactory.permissionsAdapterOf(permissionsAdapter), address(permissionedToken)); + assertEq(permissionsAdapterFactory.verifiedPermissionsAdapterOf(permissionsAdapter), address(0)); } - function testRevert_WhenPemissionsAdapterNotDeployed(address pemissionsAdapter) public { + function testRevert_WhenPemissionsAdapterNotDeployed(address permissionsAdapter) public { vm.expectRevert( - abi.encodeWithSelector(IPermissionsAdapterFactory.PemissionsAdapterNotFound.selector, pemissionsAdapter) + abi.encodeWithSelector(IPermissionsAdapterFactory.PemissionsAdapterNotFound.selector, permissionsAdapter) ); - permissionsAdapterFactory.verifyPermissionsAdapter(pemissionsAdapter); + permissionsAdapterFactory.verifyPermissionsAdapter(permissionsAdapter); } function testRevert_WhenPemissionsAdapterNotVerified() public { - address pemissionsAdapter = permissionsAdapterFactory.createPermissionsAdapter( + address permissionsAdapter = permissionsAdapterFactory.createPermissionsAdapter( permissionedToken, makeAddr("initialOwner"), allowlistChecker ); vm.expectRevert( - abi.encodeWithSelector(IPermissionsAdapterFactory.PemissionsAdapterNotVerified.selector, pemissionsAdapter) + abi.encodeWithSelector(IPermissionsAdapterFactory.PemissionsAdapterNotVerified.selector, permissionsAdapter) ); - permissionsAdapterFactory.verifyPermissionsAdapter(pemissionsAdapter); + permissionsAdapterFactory.verifyPermissionsAdapter(permissionsAdapter); } function test_VerifyPemissionsAdapter() public { - address pemissionsAdapter = permissionsAdapterFactory.createPermissionsAdapter( + address permissionsAdapter = permissionsAdapterFactory.createPermissionsAdapter( permissionedToken, makeAddr("initialOwner"), allowlistChecker ); - permissionedToken.setAllowlist(pemissionsAdapter, PermissionFlags.ALL_ALLOWED); - permissionedToken.mint(pemissionsAdapter, 1); + permissionedToken.setAllowlist(permissionsAdapter, PermissionFlags.ALL_ALLOWED); + permissionedToken.mint(permissionsAdapter, 1); vm.expectEmit(true, true, true, true); - emit IPermissionsAdapterFactory.PemissionsAdapterVerified(pemissionsAdapter, address(permissionedToken)); - permissionsAdapterFactory.verifyPermissionsAdapter(pemissionsAdapter); - assertEq(permissionsAdapterFactory.verifiedPermissionsAdapterOf(pemissionsAdapter), address(permissionedToken)); + emit IPermissionsAdapterFactory.PemissionsAdapterVerified(permissionsAdapter, address(permissionedToken)); + permissionsAdapterFactory.verifyPermissionsAdapter(permissionsAdapter); + assertEq(permissionsAdapterFactory.verifiedPermissionsAdapterOf(permissionsAdapter), address(permissionedToken)); } function testRevert_WhenPemissionsAdapterAlreadyVerified() public { - address pemissionsAdapter = permissionsAdapterFactory.createPermissionsAdapter( + address permissionsAdapter = permissionsAdapterFactory.createPermissionsAdapter( permissionedToken, makeAddr("initialOwner"), allowlistChecker ); - permissionedToken.setAllowlist(pemissionsAdapter, PermissionFlags.ALL_ALLOWED); - permissionedToken.mint(pemissionsAdapter, 1); - permissionsAdapterFactory.verifyPermissionsAdapter(pemissionsAdapter); + permissionedToken.setAllowlist(permissionsAdapter, PermissionFlags.ALL_ALLOWED); + permissionedToken.mint(permissionsAdapter, 1); + permissionsAdapterFactory.verifyPermissionsAdapter(permissionsAdapter); vm.expectRevert( abi.encodeWithSelector( - IPermissionsAdapterFactory.PemissionsAdapterAlreadyVerified.selector, pemissionsAdapter + IPermissionsAdapterFactory.PemissionsAdapterAlreadyVerified.selector, permissionsAdapter ) ); - permissionsAdapterFactory.verifyPermissionsAdapter(pemissionsAdapter); + permissionsAdapterFactory.verifyPermissionsAdapter(permissionsAdapter); } } From 1154a7a8c4e0f015c752fd06bc6b10d339cbab8c Mon Sep 17 00:00:00 2001 From: gretzke Date: Mon, 3 Nov 2025 11:53:02 +0100 Subject: [PATCH 14/33] Fix typo --- .../permissionedPools/PermissionsAdapterFactory.sol | 9 +++++---- .../interfaces/IPermissionsAdapterFactory.sol | 7 ++----- .../permissionedPools/PermissionsAdapterFactory.t.sol | 7 ++++--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/hooks/permissionedPools/PermissionsAdapterFactory.sol b/src/hooks/permissionedPools/PermissionsAdapterFactory.sol index bbb9bfe67..d407d617d 100644 --- a/src/hooks/permissionedPools/PermissionsAdapterFactory.sol +++ b/src/hooks/permissionedPools/PermissionsAdapterFactory.sol @@ -24,8 +24,9 @@ contract PermissionsAdapterFactory is IPermissionsAdapterFactory { address initialOwner, IAllowlistChecker allowListChecker ) external returns (address permissionsAdapter) { - permissionsAdapter = - address(new PermissionsAdapter(permissionedToken, POOL_MANAGER, initialOwner, allowListChecker)); + permissionsAdapter = address( + new PermissionsAdapter(permissionedToken, POOL_MANAGER, initialOwner, allowListChecker) + ); permissionsAdapterOf[permissionsAdapter] = address(permissionedToken); emit PermissionsAdapterCreated(permissionsAdapter, address(permissionedToken)); } @@ -33,11 +34,11 @@ contract PermissionsAdapterFactory is IPermissionsAdapterFactory { /// @inheritdoc IPermissionsAdapterFactory function verifyPermissionsAdapter(address permissionsAdapter) external { IERC20 permissionedToken = IERC20(permissionsAdapterOf[permissionsAdapter]); - if (address(permissionedToken) == address(0)) revert PemissionsAdapterNotFound(permissionsAdapter); + if (address(permissionedToken) == address(0)) revert PermissionsAdapterNotFound(permissionsAdapter); if (verifiedPermissionsAdapterOf[permissionsAdapter] != address(0)) { revert PemissionsAdapterAlreadyVerified(permissionsAdapter); } - // this requires that the verifier has some comntrol or ownership of the permissioned token + // this requires that the verifier has some control or ownership of the permissioned token if (permissionedToken.balanceOf(permissionsAdapter) == 0) { revert PemissionsAdapterNotVerified(permissionsAdapter); } diff --git a/src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol b/src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol index 4ced6cd74..55a72ee32 100644 --- a/src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol +++ b/src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol @@ -12,7 +12,7 @@ interface IPermissionsAdapterFactory { event PemissionsAdapterVerified(address indexed permissionsAdapter, address indexed permissionedToken); /// @notice Thrown when the permissions adapter does not exist - error PemissionsAdapterNotFound(address permissionsAdapter); + error PermissionsAdapterNotFound(address permissionsAdapter); /// @notice Thrown when the permissions adapter is already verified error PemissionsAdapterAlreadyVerified(address permissionsAdapter); @@ -44,10 +44,7 @@ interface IPermissionsAdapterFactory { /// @param permissionsAdapter The permissions adapter /// @return permissionedToken The verified permissioned token /// @dev A reverse lookup of the permissioned token is required, otherwise anyone could create a permissions adapter for a non-permissioned token - function verifiedPermissionsAdapterOf(address permissionsAdapter) - external - view - returns (address permissionedToken); + function verifiedPermissionsAdapterOf(address permissionsAdapter) external view returns (address permissionedToken); /// @notice Returns the v4 pool manager /// @return poolManager The v4 pool manager diff --git a/test/hooks/permissionedPools/PermissionsAdapterFactory.t.sol b/test/hooks/permissionedPools/PermissionsAdapterFactory.t.sol index fd94e1707..378fb1075 100644 --- a/test/hooks/permissionedPools/PermissionsAdapterFactory.t.sol +++ b/test/hooks/permissionedPools/PermissionsAdapterFactory.t.sol @@ -5,8 +5,9 @@ import { IPermissionsAdapter, IAllowlistChecker } from "../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapter.sol"; -import {IPermissionsAdapterFactory} from - "../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; +import { + IPermissionsAdapterFactory +} from "../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; import {IERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {PermissionedPoolsBase, MockAllowlistChecker} from "./PermissionedPoolsBase.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; @@ -47,7 +48,7 @@ contract PermissionsAdapterFactoryTest is PermissionedPoolsBase { function testRevert_WhenPemissionsAdapterNotDeployed(address permissionsAdapter) public { vm.expectRevert( - abi.encodeWithSelector(IPermissionsAdapterFactory.PemissionsAdapterNotFound.selector, permissionsAdapter) + abi.encodeWithSelector(IPermissionsAdapterFactory.PermissionsAdapterNotFound.selector, permissionsAdapter) ); permissionsAdapterFactory.verifyPermissionsAdapter(permissionsAdapter); } From 1210b70942ff33149149a40681165a0d5ed8c1e9 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:02:06 -0400 Subject: [PATCH 15/33] fix(permissioned-hooks): enforce permission-flags in both payer modes of PermissionedV4Router + PermissionedPositionManager --- src/hooks/permissionedPools/PermissionedPositionManager.sol | 4 ++++ src/hooks/permissionedPools/PermissionedV4Router.sol | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index 06ea15a88..bef51d490 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -119,6 +119,10 @@ contract PermissionedPositionManager is PositionManager { Currency.wrap(permissionedToken).transfer(address(permissionsAdapter), amount); permissionsAdapter.wrapToPoolManager(amount); } else { + // Check liquidity permission for the actual user + if (!permissionsAdapter.isAllowed(msgSender(), PermissionFlags.LIQUIDITY_ALLOWED)) { + revert Unauthorized(); + } // token is a permissioned token, wrap the token permit2.transferFrom(payer, address(permissionsAdapter), uint160(amount), permissionedToken); permissionsAdapter.wrapToPoolManager(amount); diff --git a/src/hooks/permissionedPools/PermissionedV4Router.sol b/src/hooks/permissionedPools/PermissionedV4Router.sol index fec08a4f8..0427264a0 100644 --- a/src/hooks/permissionedPools/PermissionedV4Router.sol +++ b/src/hooks/permissionedPools/PermissionedV4Router.sol @@ -146,6 +146,10 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { Currency.wrap(permissionedToken).transfer(address(permissionsAdapter), amount); permissionsAdapter.wrapToPoolManager(amount); } else { + // Check swap permission for the actual user + if (!permissionsAdapter.isAllowed(msgSender(), PermissionFlags.SWAP_ALLOWED)) { + revert Unauthorized(); + } // token is a permissioned token, wrap the token PERMIT2.transferFrom(payer, address(permissionsAdapter), uint160(amount), permissionedToken); permissionsAdapter.wrapToPoolManager(amount); From 13a4e4f650245bc9623952827e11217e5547a021 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:11:00 -0400 Subject: [PATCH 16/33] tests(permissioned-hooks): adding unit tests to validate enforcement of permission-flags in both payer modes of PermissionedV4Router + PermissionedPositionManager --- snapshots/BaseActionsRouterTest.json | 2 +- snapshots/PaymentsTests.json | 8 +- snapshots/PermissionedV4RouterTest.json | 2 +- snapshots/PosMGasTest.json | 78 ++++++++--------- snapshots/PositionDescriptorTest.json | 2 +- snapshots/QuoterTest.json | 26 +++--- snapshots/StateViewTest.json | 20 ++--- snapshots/V4RouterTest.json | 50 +++++------ .../PermissionedV4Router.sol | 22 +++-- .../PermissionedPositionManager.t.sol | 83 ++++++++++-------- .../PermissionedV4Router.t.sol | 87 +++++++++++-------- .../permissionedPools/mocks/MockHooks.sol | 10 ++- .../shared/PermissionedDeployers.sol | 19 +++- .../shared/PermissionedRoutingTestHelpers.sol | 44 ++++++---- 14 files changed, 253 insertions(+), 200 deletions(-) diff --git a/snapshots/BaseActionsRouterTest.json b/snapshots/BaseActionsRouterTest.json index d7241dbe2..b40a29c24 100644 --- a/snapshots/BaseActionsRouterTest.json +++ b/snapshots/BaseActionsRouterTest.json @@ -1,3 +1,3 @@ { - "BaseActionsRouter_mock10commands": "63076" + "BaseActionsRouter_mock10commands": "39607" } \ No newline at end of file diff --git a/snapshots/PaymentsTests.json b/snapshots/PaymentsTests.json index c3e810d19..d005ec134 100644 --- a/snapshots/PaymentsTests.json +++ b/snapshots/PaymentsTests.json @@ -1,6 +1,6 @@ { - "Payments_swap_settleFromCaller_takeAllToMsgSender": "133006", - "Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "134475", - "Payments_swap_settleWithBalance_takeAllToMsgSender": "126957", - "Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "127095" + "Payments_swap_settleFromCaller_takeAllToMsgSender": "124759", + "Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "126152", + "Payments_swap_settleWithBalance_takeAllToMsgSender": "116711", + "Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "116534" } \ No newline at end of file diff --git a/snapshots/PermissionedV4RouterTest.json b/snapshots/PermissionedV4RouterTest.json index 9bcfabe17..2a01b4ccc 100644 --- a/snapshots/PermissionedV4RouterTest.json +++ b/snapshots/PermissionedV4RouterTest.json @@ -1,3 +1,3 @@ { - "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "227931" + "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "259259" } \ No newline at end of file diff --git a/snapshots/PosMGasTest.json b/snapshots/PosMGasTest.json index 579fa6642..d55abd08c 100644 --- a/snapshots/PosMGasTest.json +++ b/snapshots/PosMGasTest.json @@ -1,43 +1,43 @@ { - "PositionManager_burn_empty": "51102", - "PositionManager_burn_empty_native": "51102", - "PositionManager_burn_nonEmpty_native_withClose": "127695", - "PositionManager_burn_nonEmpty_native_withTakePair": "127137", - "PositionManager_burn_nonEmpty_withClose": "134600", - "PositionManager_burn_nonEmpty_withTakePair": "134043", - "PositionManager_collect_native": "147732", - "PositionManager_collect_sameRange": "156364", - "PositionManager_collect_withClose": "156364", - "PositionManager_collect_withTakePair": "155679", - "PositionManager_decreaseLiquidity_native": "113681", - "PositionManager_decreaseLiquidity_withClose": "121933", - "PositionManager_decreaseLiquidity_withTakePair": "121248", - "PositionManager_decrease_burnEmpty": "137500", - "PositionManager_decrease_burnEmpty_native": "130595", - "PositionManager_decrease_sameRange_allLiquidity": "134649", - "PositionManager_decrease_take_take": "122490", - "PositionManager_increaseLiquidity_erc20_withClose": "160652", - "PositionManager_increaseLiquidity_erc20_withSettlePair": "159563", - "PositionManager_increaseLiquidity_native": "143532", - "PositionManager_increase_autocompoundExactUnclaimedFees": "138231", - "PositionManager_increase_autocompoundExcessFeesCredit": "179593", - "PositionManager_increase_autocompound_clearExcess": "149511", - "PositionManager_mint_native": "367550", - "PositionManager_mint_nativeWithSweep_withClose": "376153", - "PositionManager_mint_nativeWithSweep_withSettlePair": "375348", - "PositionManager_mint_onSameTickLower": "319298", - "PositionManager_mint_onSameTickUpper": "319940", - "PositionManager_mint_sameRange": "245522", - "PositionManager_mint_settleWithBalance_sweep": "420760", - "PositionManager_mint_warmedPool_differentRange": "325316", - "PositionManager_mint_withClose": "421910", - "PositionManager_mint_withSettlePair": "420939", - "PositionManager_multicall_initialize_mint": "458237", - "PositionManager_permit": "79163", - "PositionManager_permit_secondPosition": "62075", - "PositionManager_permit_twice": "44975", - "PositionManager_subscribe": "87968", - "PositionManager_unsubscribe": "62697", + "PositionManager_burn_empty": "16871", + "PositionManager_burn_empty_native": "16871", + "PositionManager_burn_nonEmpty_native_withClose": "63380", + "PositionManager_burn_nonEmpty_native_withTakePair": "62631", + "PositionManager_burn_nonEmpty_withClose": "60378", + "PositionManager_burn_nonEmpty_withTakePair": "59629", + "PositionManager_collect_native": "85852", + "PositionManager_collect_sameRange": "82850", + "PositionManager_collect_withClose": "82850", + "PositionManager_collect_withTakePair": "82101", + "PositionManager_decreaseLiquidity_native": "55859", + "PositionManager_decreaseLiquidity_withClose": "52857", + "PositionManager_decreaseLiquidity_withTakePair": "52108", + "PositionManager_decrease_burnEmpty": "62811", + "PositionManager_decrease_burnEmpty_native": "65813", + "PositionManager_decrease_sameRange_allLiquidity": "50143", + "PositionManager_decrease_take_take": "52841", + "PositionManager_increaseLiquidity_erc20_withClose": "63940", + "PositionManager_increaseLiquidity_erc20_withSettlePair": "63155", + "PositionManager_increaseLiquidity_native": "60735", + "PositionManager_increase_autocompoundExactUnclaimedFees": "70594", + "PositionManager_increase_autocompoundExcessFeesCredit": "89962", + "PositionManager_increase_autocompound_clearExcess": "82118", + "PositionManager_mint_native": "351583", + "PositionManager_mint_nativeWithSweep_withClose": "359614", + "PositionManager_mint_nativeWithSweep_withSettlePair": "358657", + "PositionManager_mint_onSameTickLower": "271298", + "PositionManager_mint_onSameTickUpper": "275950", + "PositionManager_mint_sameRange": "176554", + "PositionManager_mint_settleWithBalance_sweep": "401096", + "PositionManager_mint_warmedPool_differentRange": "276594", + "PositionManager_mint_withClose": "407888", + "PositionManager_mint_withSettlePair": "407017", + "PositionManager_multicall_initialize_mint": "445356", + "PositionManager_permit": "51379", + "PositionManager_permit_secondPosition": "29479", + "PositionManager_permit_twice": "7579", + "PositionManager_subscribe": "57900", + "PositionManager_unsubscribe": "28116", "position manager initcode hash (without constructor params, as uint256)": "81827502601055975118808937107769364319765058198432540518516360048852193272922", "positionManager bytecode size": "23877" } \ No newline at end of file diff --git a/snapshots/PositionDescriptorTest.json b/snapshots/PositionDescriptorTest.json index a9f6400b3..3ae12d160 100644 --- a/snapshots/PositionDescriptorTest.json +++ b/snapshots/PositionDescriptorTest.json @@ -1,5 +1,5 @@ { "position descriptor initcode hash (without constructor params, as uint256)": "62242073579002066175147720437850721791653757771535856244007756965162378120375", "positionDescriptor bytecode size": "24069", - "proxy bytecode size": "1488" + "proxy bytecode size": "1968" } \ No newline at end of file diff --git a/snapshots/QuoterTest.json b/snapshots/QuoterTest.json index 3ecbe3350..19648b93f 100644 --- a/snapshots/QuoterTest.json +++ b/snapshots/QuoterTest.json @@ -1,15 +1,15 @@ { - "Quoter_exactInputSingle_oneForZero_multiplePositions": "146134", - "Quoter_exactInputSingle_zeroForOne_multiplePositions": "152349", - "Quoter_exactOutputSingle_oneForZero": "79477", - "Quoter_exactOutputSingle_zeroForOne": "84722", - "Quoter_quoteExactInput_oneHop_1TickLoaded": "122938", - "Quoter_quoteExactInput_oneHop_initializedAfter": "147573", - "Quoter_quoteExactInput_oneHop_startingInitialized": "80848", - "Quoter_quoteExactInput_twoHops": "205152", - "Quoter_quoteExactOutput_oneHop_1TickLoaded": "122434", - "Quoter_quoteExactOutput_oneHop_2TicksLoaded": "153089", - "Quoter_quoteExactOutput_oneHop_initializedAfter": "122461", - "Quoter_quoteExactOutput_oneHop_startingInitialized": "98755", - "Quoter_quoteExactOutput_twoHops": "204880" + "Quoter_exactInputSingle_oneForZero_multiplePositions": "139681", + "Quoter_exactInputSingle_zeroForOne_multiplePositions": "146706", + "Quoter_exactOutputSingle_oneForZero": "70940", + "Quoter_exactOutputSingle_zeroForOne": "77112", + "Quoter_quoteExactInput_oneHop_1TickLoaded": "116810", + "Quoter_quoteExactInput_oneHop_initializedAfter": "141778", + "Quoter_quoteExactInput_oneHop_startingInitialized": "62264", + "Quoter_quoteExactInput_twoHops": "210437", + "Quoter_quoteExactOutput_oneHop_1TickLoaded": "116866", + "Quoter_quoteExactOutput_oneHop_2TicksLoaded": "148769", + "Quoter_quoteExactOutput_oneHop_initializedAfter": "116926", + "Quoter_quoteExactOutput_oneHop_startingInitialized": "70805", + "Quoter_quoteExactOutput_twoHops": "211021" } \ No newline at end of file diff --git a/snapshots/StateViewTest.json b/snapshots/StateViewTest.json index ab141d8b1..e42038822 100644 --- a/snapshots/StateViewTest.json +++ b/snapshots/StateViewTest.json @@ -1,12 +1,12 @@ { - "StateView_extsload_getFeeGrowthGlobals": "8703", - "StateView_extsload_getFeeGrowthInside": "24375", - "StateView_extsload_getLiquidity": "5939", - "StateView_extsload_getPositionInfo": "11261", - "StateView_extsload_getPositionLiquidity": "6191", - "StateView_extsload_getSlot0": "5986", - "StateView_extsload_getTickBitmap": "5932", - "StateView_extsload_getTickFeeGrowthOutside": "8990", - "StateView_extsload_getTickInfo": "11193", - "StateView_extsload_getTickLiquidity": "6186" + "StateView_extsload_getFeeGrowthGlobals": "4529", + "StateView_extsload_getFeeGrowthInside": "15757", + "StateView_extsload_getLiquidity": "2737", + "StateView_extsload_getPositionInfo": "5752", + "StateView_extsload_getPositionLiquidity": "3408", + "StateView_extsload_getSlot0": "2857", + "StateView_extsload_getTickBitmap": "3283", + "StateView_extsload_getTickFeeGrowthOutside": "5392", + "StateView_extsload_getTickInfo": "5873", + "StateView_extsload_getTickLiquidity": "3508" } \ No newline at end of file diff --git a/snapshots/V4RouterTest.json b/snapshots/V4RouterTest.json index 952f619d9..5f8709ddb 100644 --- a/snapshots/V4RouterTest.json +++ b/snapshots/V4RouterTest.json @@ -1,27 +1,27 @@ { - "V4Router_Bytecode": "8975", - "V4Router_ExactIn1Hop_nativeIn": "121653", - "V4Router_ExactIn1Hop_nativeOut": "120119", - "V4Router_ExactIn1Hop_oneForZero": "128991", - "V4Router_ExactIn1Hop_zeroForOne": "135517", - "V4Router_ExactIn2Hops": "192843", - "V4Router_ExactIn2Hops_nativeIn": "178979", - "V4Router_ExactIn3Hops": "250218", - "V4Router_ExactIn3Hops_nativeIn": "236354", - "V4Router_ExactInputSingle": "134001", - "V4Router_ExactInputSingle_nativeIn": "120137", - "V4Router_ExactInputSingle_nativeOut": "118581", - "V4Router_ExactOut1Hop_nativeIn_sweepETH": "127940", - "V4Router_ExactOut1Hop_nativeOut": "121329", - "V4Router_ExactOut1Hop_oneForZero": "130201", - "V4Router_ExactOut1Hop_zeroForOne": "134858", - "V4Router_ExactOut2Hops": "191291", - "V4Router_ExactOut2Hops_nativeIn": "184373", - "V4Router_ExactOut3Hops": "247788", - "V4Router_ExactOut3Hops_nativeIn": "240870", - "V4Router_ExactOut3Hops_nativeOut": "224943", - "V4Router_ExactOutputSingle": "133337", - "V4Router_ExactOutputSingle_nativeIn_sweepETH": "126419", - "V4Router_ExactOutputSingle_nativeOut": "119821", - "router initcode hash (without constructor params, as uint256)": "27545762869727400677117557485685740862616789454191614676777323590122002226479" + "V4Router_Bytecode": "11442", + "V4Router_ExactIn1Hop_nativeIn": "111493", + "V4Router_ExactIn1Hop_nativeOut": "110918", + "V4Router_ExactIn1Hop_oneForZero": "120016", + "V4Router_ExactIn1Hop_zeroForOne": "127319", + "V4Router_ExactIn2Hops": "185153", + "V4Router_ExactIn2Hops_nativeIn": "176610", + "V4Router_ExactIn3Hops": "242988", + "V4Router_ExactIn3Hops_nativeIn": "234445", + "V4Router_ExactInputSingle": "126106", + "V4Router_ExactInputSingle_nativeIn": "110280", + "V4Router_ExactInputSingle_nativeOut": "109695", + "V4Router_ExactOut1Hop_nativeIn_sweepETH": "117775", + "V4Router_ExactOut1Hop_nativeOut": "111997", + "V4Router_ExactOut1Hop_oneForZero": "121095", + "V4Router_ExactOut1Hop_zeroForOne": "126565", + "V4Router_ExactOut2Hops": "185415", + "V4Router_ExactOut2Hops_nativeIn": "182095", + "V4Router_ExactOut3Hops": "244281", + "V4Router_ExactOut3Hops_nativeIn": "240961", + "V4Router_ExactOut3Hops_nativeOut": "235183", + "V4Router_ExactOutputSingle": "125324", + "V4Router_ExactOutputSingle_nativeIn_sweepETH": "116534", + "V4Router_ExactOutputSingle_nativeOut": "110849", + "router initcode hash (without constructor params, as uint256)": "16473836486703886561435051652926532733159549293834562681012203074313944726145" } \ No newline at end of file diff --git a/src/hooks/permissionedPools/PermissionedV4Router.sol b/src/hooks/permissionedPools/PermissionedV4Router.sol index 0427264a0..2fa02d2b0 100644 --- a/src/hooks/permissionedPools/PermissionedV4Router.sol +++ b/src/hooks/permissionedPools/PermissionedV4Router.sol @@ -95,10 +95,7 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { } /// @notice Decodes and executes the given command with the given inputs - function _dispatch(bytes1 commandType, bytes calldata inputs) - internal - returns (bool success, bytes memory output) - { + function _dispatch(bytes1 commandType, bytes calldata inputs) internal returns (bool success, bytes memory output) { uint256 command = uint8(commandType & COMMAND_TYPE_MASK); success = true; if (command == COMMAND_PERMIT2_PERMIT) { @@ -109,14 +106,15 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { } bytes calldata data = _toBytes(inputs, 6); // PermitSingle takes first 6 slots (0..5) - (success, output) = address(PERMIT2).call( - abi.encodeWithSignature( - "permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)", - msgSender(), - permitSingle, - data - ) - ); + (success, output) = address(PERMIT2) + .call( + abi.encodeWithSignature( + "permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)", + msgSender(), + permitSingle, + data + ) + ); } else if (command == COMMAND_V4_SWAP) { _executeActions(inputs); } else { diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 39477dfb9..6608b5d60 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -67,6 +67,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq PoolKey public keyFake1; PoolKey public keyFake2; + PoolKey public insecureKey; + // Test Users address public alice = makeAddr("ALICE"); address public unauthorizedUser = makeAddr("UNAUTHORIZED"); @@ -134,6 +136,13 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq 3000, SQRT_PRICE_1_1 ); + (insecureKey, poolId) = initPool( + Currency.wrap(address(permissionsAdapter0)), + Currency.wrap(address(permissionsAdapter2)), + insecureHooks, + 3000, + SQRT_PRICE_1_1 + ); } function setupPermissionedComponents() internal { @@ -181,29 +190,24 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(this), PermissionFlags.ALL_ALLOWED); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( - address(secondaryPosm), PermissionFlags.ALL_ALLOWED - ); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( - address(permissionsAdapterFactory), PermissionFlags.ALL_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(currency)) + .setAllowlist(address(secondaryPosm), PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency)) + .setAllowlist(address(permissionsAdapterFactory), PermissionFlags.ALL_ALLOWED); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), PermissionFlags.ALL_ALLOWED); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(manager), PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( - address(permissionedSwapRouter), PermissionFlags.ALL_ALLOWED - ); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist( - address(permissionedHooks), PermissionFlags.ALL_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(currency)) + .setAllowlist(address(permissionedSwapRouter), PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency2)) + .setAllowlist(address(permissionedHooks), PermissionFlags.ALL_ALLOWED); } function setUpPemissionsAdapter(PermissionsAdapter permissionsAdapter, Currency currency) internal { adapterToPermissioned[Currency.wrap(address(permissionsAdapter))] = currency; MockPermissionedToken(Currency.unwrap(currency)).mint(address(this), 1000 ether); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( - address(permissionsAdapter), PermissionFlags.ALL_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(currency)) + .setAllowlist(address(permissionsAdapter), PermissionFlags.ALL_ALLOWED); // permissions adapter contract must have a non-zero balance of the permissioned token currency.transfer(address(permissionsAdapter), 1); @@ -223,6 +227,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq setAllowedHooks(lpm, permissionsAdapterCurrency, secondaryPermissionedHooks); setAllowedHooks(secondaryPosm, permissionsAdapterCurrency, secondaryPermissionedHooks); setAllowedHooks(tertiaryPosm, permissionsAdapterCurrency, secondaryPermissionedHooks); + + setAllowedHooks(lpm, permissionsAdapterCurrency, insecureHooks); } function setAllowedHooks(IPositionManager posm, Currency currency, IHooks permissionedHooks_) internal { @@ -884,13 +890,11 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq } // Add some tokens to unauthorized user - MockPermissionedToken(Currency.unwrap(currencyPermissioned)).setAllowlist( - unauthorizedUser, PermissionFlags.ALL_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(currencyPermissioned)) + .setAllowlist(unauthorizedUser, PermissionFlags.ALL_ALLOWED); MockPermissionedToken(Currency.unwrap(currencyPermissioned)).mint(unauthorizedUser, 1000e18); - MockPermissionedToken(Currency.unwrap(currencyPermissioned)).setAllowlist( - unauthorizedUser, PermissionFlags.NONE - ); + MockPermissionedToken(Currency.unwrap(currencyPermissioned)) + .setAllowlist(unauthorizedUser, PermissionFlags.NONE); vm.startPrank(unauthorizedUser); @@ -997,8 +1001,9 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq function _test_initialize(Currency currency0_, Currency currency1_) internal { // initialize a new pool - PoolKey memory key = - PoolKey({currency0: currency0_, currency1: currency1_, fee: 0, tickSpacing: 100, hooks: IHooks(address(0))}); + PoolKey memory key = PoolKey({ + currency0: currency0_, currency1: currency1_, fee: 0, tickSpacing: 100, hooks: IHooks(address(0)) + }); lpm.initializePool(key, SQRT_PRICE_1_1); @@ -1024,11 +1029,7 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq uint160(bound(sqrtPrice, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE_MINUS_MIN_SQRT_PRICE_MINUS_ONE)); fee = uint24(bound(fee, 0, LPFeeLibrary.MAX_LP_FEE)); PoolKey memory key = PoolKey({ - currency0: currency0_, - currency1: currency1_, - fee: fee, - tickSpacing: 10, - hooks: IHooks(address(0)) + currency0: currency0_, currency1: currency1_, fee: fee, tickSpacing: 10, hooks: IHooks(address(0)) }); lpm.initializePool(key, sqrtPrice); @@ -1308,12 +1309,10 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq uint256 liquidity = 1e18; // Test SWAP_ALLOWED + LIQUIDITY_ALLOWED (should work like ALL_ALLOWED) - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist( - alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED) - ); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist( - alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED) - ); + MockPermissionedToken(Currency.unwrap(currency0)) + .setAllowlist(alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED)); + MockPermissionedToken(Currency.unwrap(currency2)) + .setAllowlist(alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED)); uint256 tokenId = lpm.nextTokenId(); vm.prank(alice); @@ -1409,4 +1408,20 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq mint(config2, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); assertEq(IERC721(address(lpm)).ownerOf(tokenId2), alice); } + + function test_permission_flag_swap_allowed_unauthorized_reverts() public { + // Test that SWAP_ALLOWED does not allow liquidity operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + + // Should revert when trying to mint with SWAP_ALLOWED + PositionConfig memory config = PositionConfig({poolKey: insecureKey, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + uint256 tokenId = lpm.nextTokenId(); + + vm.startPrank(alice); + vm.expectRevert(Unauthorized.selector); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + vm.stopPrank(); + } } diff --git a/test/hooks/permissionedPools/PermissionedV4Router.t.sol b/test/hooks/permissionedPools/PermissionedV4Router.t.sol index 59feea5f3..473cdd842 100644 --- a/test/hooks/permissionedPools/PermissionedV4Router.t.sol +++ b/test/hooks/permissionedPools/PermissionedV4Router.t.sol @@ -453,17 +453,17 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } - function test_permission_flag_swap_allowed_succeeds() public { - // Test that SWAP_ALLOWED allows swap operations - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + function test_router_user_payment_permissioned_token_unauthorized_reverts() public { + // Test that LIQUIDITY_ALLOWED does not allow swap operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); uint256 amountIn = 100; PoolKey memory adapterKey = - PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, insecureHooks); IV4Router.ExactInputSingleParams memory params = IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); @@ -472,8 +472,10 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); - vm.prank(alice); + vm.startPrank(alice); + vm.expectRevert(Unauthorized.selector); permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); + vm.stopPrank(); } function test_permission_flag_liquidity_allowed_swap_reverts() public { @@ -540,12 +542,10 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); // Test SWAP_ALLOWED + LIQUIDITY_ALLOWED (should work like ALL_ALLOWED) - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist( - alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED) - ); - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist( - alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED) - ); + MockPermissionedToken(Currency.unwrap(currency0)) + .setAllowlist(alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED)); + MockPermissionedToken(Currency.unwrap(currency1)) + .setAllowlist(alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED)); vm.prank(alice); permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); @@ -641,12 +641,10 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { } function test_permission_flag_all_pool_types_swap() public { - MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(permissionsAdapter0Currency))).setAllowlist( - alice, PermissionFlags.SWAP_ALLOWED - ); - MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(permissionsAdapter1Currency))).setAllowlist( - alice, PermissionFlags.SWAP_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(permissionsAdapter0Currency))) + .setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(permissionsAdapter1Currency))) + .setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); _test_permission_flag_all_pool_types_swap(key0); _test_permission_flag_all_pool_types_swap(key1); @@ -683,8 +681,9 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey memory adapterKey = PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); - IV4Router.ExactOutputSingleParams memory params = - IV4Router.ExactOutputSingleParams(adapterKey, true, uint128(amountOut), uint128(amountInMaximum), bytes("")); + IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( + adapterKey, true, uint128(amountOut), uint128(amountInMaximum), bytes("") + ); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); bytes memory data = @@ -728,6 +727,29 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } + function test_router_user_payment_permissioned_token() public { + // Test that SWAP_ALLOWED allows swap operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); + + vm.prank(alice); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); + } + /*////////////////////////////////////////////////////////////// ERC20 -> ERC20 EXACT INPUT //////////////////////////////////////////////////////////////*/ @@ -839,8 +861,9 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, expectedAmountOut * 12 / 10)); // take the entire open delta to the router's address - plan = - plan.add(Actions.TAKE, abi.encode(key0.currency1, ActionConstants.ADDRESS_THIS, ActionConstants.OPEN_DELTA)); + plan = plan.add( + Actions.TAKE, abi.encode(key0.currency1, ActionConstants.ADDRESS_THIS, ActionConstants.OPEN_DELTA) + ); bytes memory data = plan.encode(); // the router holds no funds before assertEq(inputCurrency.balanceOf(address(permissionedRouter)), 0); @@ -1816,17 +1839,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { function test_router_self_payment_permissioned_token_unauthorized_reverts() public { uint256 amountIn = 100000; - MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(key0.currency0))).setAllowlist( - unauthorizedUser, PermissionFlags.ALL_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(key0.currency0))) + .setAllowlist(unauthorizedUser, PermissionFlags.ALL_ALLOWED); IERC20(Currency.unwrap(getPermissionedCurrency(key0.currency0))).transfer(address(unauthorizedUser), amountIn); vm.prank(unauthorizedUser); IERC20(Currency.unwrap(getPermissionedCurrency(key0.currency0))).transfer(address(permissionedRouter), amountIn); - MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(key0.currency0))).setAllowlist( - unauthorizedUser, PermissionFlags.NONE - ); + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(key0.currency0))) + .setAllowlist(unauthorizedUser, PermissionFlags.NONE); IV4Router.ExactInputSingleParams memory params = IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); @@ -1845,10 +1866,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { function test_command_permit2_permit() public { IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({ details: IAllowanceTransfer.PermitDetails({ - token: Currency.unwrap(currency0), - amount: 1e18, - expiration: uint48(block.timestamp + 3600), - nonce: 0 + token: Currency.unwrap(currency0), amount: 1e18, expiration: uint48(block.timestamp + 3600), nonce: 0 }), spender: address(permissionedRouter), sigDeadline: block.timestamp + 3600 @@ -1875,10 +1893,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { function test_failed_call_reverts() public { IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({ details: IAllowanceTransfer.PermitDetails({ - token: Currency.unwrap(currency0), - amount: 1e18, - expiration: uint48(block.timestamp + 3600), - nonce: 0 + token: Currency.unwrap(currency0), amount: 1e18, expiration: uint48(block.timestamp + 3600), nonce: 0 }), spender: address(permissionedRouter), sigDeadline: block.timestamp + 3600 diff --git a/test/hooks/permissionedPools/mocks/MockHooks.sol b/test/hooks/permissionedPools/mocks/MockHooks.sol index b1d95222b..1331fd9cf 100644 --- a/test/hooks/permissionedPools/mocks/MockHooks.sol +++ b/test/hooks/permissionedPools/mocks/MockHooks.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.0; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IPermissionsAdapterFactory} from - "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; +import { + IPermissionsAdapterFactory +} from "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; import {PermissionedHooks} from "../../../../src/hooks/permissionedPools/PermissionedHooks.sol"; contract MockHooks is PermissionedHooks { @@ -28,9 +30,9 @@ contract MockInsecureHooks { function beforeSwap(address, PoolKey calldata, SwapParams calldata, bytes calldata) external pure - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { - return IHooks.beforeSwap.selector; + return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } receive() external payable {} diff --git a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol index 712803a5b..66b00541e 100644 --- a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol @@ -31,8 +31,9 @@ import {Plan} from "../../../shared/Planner.sol"; import {IV4Router} from "../../../../src/interfaces/IV4Router.sol"; import {Actions} from "../../../../src/libraries/Actions.sol"; import {ActionConstants} from "../../../../src/libraries/ActionConstants.sol"; -import {IPermissionsAdapterFactory} from - "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; +import { + IPermissionsAdapterFactory +} from "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; import {PermissionedV4Router} from "../../../../src/hooks/permissionedPools/PermissionedV4Router.sol"; import {MockPermissionedToken} from "../PermissionedPoolsBase.sol"; import {MockV4Router} from "../../../mocks/MockV4Router.sol"; @@ -81,6 +82,7 @@ contract PermissionedDeployers is Test { ActionsRouter public actionsRouter; IHooks public permissionedHooks; IHooks public secondaryPermissionedHooks; + IHooks public insecureHooks; IPermissionsAdapterFactory public permissionsAdapterFactory; PoolClaimsTest public claimsRouter; @@ -147,6 +149,18 @@ contract PermissionedDeployers is Test { deployedHooksAddr = calculatedAddr; } + function deployInsecureHooks(address manager_) internal returns (address deployedHooksAddr) { + uint160 flags = (1 << 11) | (1 << 7); + (address calculatedAddr, bytes32 salt) = HookMiner.find( + address(this), flags, vm.getCode("MockHooks.sol:MockInsecureHooks"), abi.encode(IPoolManager(manager_)) + ); + address addr = Deploy.create2( + abi.encodePacked(vm.getCode("MockHooks.sol:MockInsecureHooks"), abi.encode(IPoolManager(manager_))), salt + ); + assertEq(addr, calculatedAddr); + deployedHooksAddr = calculatedAddr; + } + function deployPermissionedV4Router(address permit2_, address permissionsAdapterFactory_, address weth9) internal returns (address deployedAddr) @@ -177,6 +191,7 @@ contract PermissionedDeployers is Test { permissionsAdapterFactory = IPermissionsAdapterFactory(permissionsAdapterFactoryAddress); permissionedHooks = IHooks(deployPermissionedHooks(address(manager), permissionsAdapterFactoryAddress)); secondaryPermissionedHooks = IHooks(deployPermissionedHooks(address(manager), permissionsAdapterFactoryAddress)); + insecureHooks = IHooks(deployInsecureHooks(address(manager))); address deployedAddr = deployPermissionedV4Router(permit2_, permissionsAdapterFactoryAddress, weth9); permissionedSwapRouter = PermissionedV4Router(payable(deployedAddr)); swapRouter = PoolSwapTest(deployedAddr); diff --git a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol index 247f577e7..653e838b0 100644 --- a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol @@ -20,8 +20,9 @@ import {PermissionsAdapter} from "../../../../src/hooks/permissionedPools/Permis import {MockAllowlistChecker, MockPermissionedToken} from "../PermissionedPoolsBase.sol"; import {IAllowlistChecker} from "../../../../src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol"; import {IPositionManager} from "../../../../src/interfaces/IPositionManager.sol"; -import {IPermissionsAdapterFactory} from - "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; +import { + IPermissionsAdapterFactory +} from "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; import {IWETH9} from "../../../../src/interfaces/external/IWETH9.sol"; import {IPositionDescriptor} from "../../../../src/interfaces/IPositionDescriptor.sol"; import {IV4Router} from "../../../../src/interfaces/IV4Router.sol"; @@ -47,6 +48,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 PoolKey public key1; PoolKey public key2; PoolKey public key3; + PoolKey public insecureKey; // currency0 and currency1 are defined in Deployers.sol Currency public currency2; @@ -70,6 +72,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 _deployPermit2(); _deployPemissionsAdapterFactory(); _deployPermissionedHooks(); + _deployInsecureHook(); _deployMockPermissionedRouter(); _deployPositionManager(); MockERC20[] memory tokens = _deployTokensMintAndApprove(5, 2); @@ -124,12 +127,10 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 break; } } - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist( - address(permissionsAdapter0), PermissionFlags.ALL_ALLOWED - ); - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist( - address(permissionsAdapter1), PermissionFlags.ALL_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(currency0)) + .setAllowlist(address(permissionsAdapter0), PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)) + .setAllowlist(address(permissionsAdapter1), PermissionFlags.ALL_ALLOWED); // Transfer some underlying tokens to the permissions adapter for verification IERC20(Currency.unwrap(currency0)).transfer(address(permissionsAdapter0), 1); @@ -160,6 +161,9 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 setAllowedHooks( IPositionManager(positionManager), Currency.wrap(address(permissionsAdapter1)), permissionedHooks ); + + setAllowedHooks(IPositionManager(positionManager), Currency.wrap(address(permissionsAdapter0)), insecureHooks); + setAllowedHooks(IPositionManager(positionManager), Currency.wrap(address(permissionsAdapter1)), insecureHooks); } function setAllowedHooks(IPositionManager posm, Currency currency, IHooks permissionedHooks_) internal { @@ -174,7 +178,9 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 internal returns (PoolKey memory _key) { - if (Currency.unwrap(currencyA) > Currency.unwrap(currencyB)) (currencyA, currencyB) = (currencyB, currencyA); + if (Currency.unwrap(currencyA) > Currency.unwrap(currencyB)) { + (currencyA, currencyB) = (currencyB, currencyA); + } _key = PoolKey(currencyA, currencyB, 3000, 60, IHooks(hookAddr)); manager.initialize(_key, SQRT_PRICE_1_1); MockERC20(Currency.unwrap(currencyA)).approve(positionManager, type(uint256).max); @@ -182,21 +188,14 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 modifyLiquidity(_key, ModifyLiquidityParams(-887220, 887220, 200 ether, 0), "0x", 0); } - function createNativePoolWithLiquidity(Currency currency, address hookAddr) - internal - returns (PoolKey memory _key) - { + function createNativePoolWithLiquidity(Currency currency, address hookAddr) internal returns (PoolKey memory _key) { _key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, currency, 3000, 60, IHooks(hookAddr)); manager.initialize(_key, SQRT_PRICE_1_1); MockERC20(Currency.unwrap(currency)).approve(positionManager, type(uint256).max); modifyLiquidity(_key, ModifyLiquidityParams(-887220, 887220, 200 ether, 0), "0x", 200 ether); } - function generatePermitDigest(IAllowanceTransfer.PermitSingle memory permitSingle) - internal - view - returns (bytes32) - { + function generatePermitDigest(IAllowanceTransfer.PermitSingle memory permitSingle) internal view returns (bytes32) { bytes32 domainSeparator = permit2.DOMAIN_SEPARATOR(); bytes32 PERMIT_SINGLE_TYPEHASH = keccak256( @@ -383,6 +382,10 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 permissionedHooks = IHooks(deployPermissionedHooks(address(manager), address(permissionsAdapterFactory))); } + function _deployInsecureHook() private { + insecureHooks = IHooks(deployInsecureHooks(address(manager))); + } + function _deployMockPermissionedRouter() private { bytes memory routerBytecode = abi.encodePacked( vm.getCode("PermissionedV4Router.sol:PermissionedV4Router"), @@ -432,6 +435,11 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 key2 = createPoolWithLiquidity(currency2, currency3, address(permissionedHooks)); key3 = createPoolWithLiquidity(Currency.wrap(address(permissionsAdapter0)), currency4, address(permissionedHooks)); + insecureKey = createPoolWithLiquidity( + Currency.wrap(address(permissionsAdapter0)), + Currency.wrap(address(permissionsAdapter1)), + address(insecureHooks) + ); } function _setupMockAllowList(Currency currency, address spender) private { From 9c489686a50086e05b2dbf74bfbe1665ac9d832f Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:40:51 -0400 Subject: [PATCH 17/33] update gas snapshot for PermissionedV4Router --- snapshots/PermissionedV4RouterTest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snapshots/PermissionedV4RouterTest.json b/snapshots/PermissionedV4RouterTest.json index 2a01b4ccc..818d71eaa 100644 --- a/snapshots/PermissionedV4RouterTest.json +++ b/snapshots/PermissionedV4RouterTest.json @@ -1,3 +1,3 @@ { - "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "259259" + "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "223296" } \ No newline at end of file From 08cdb30529e596e42557c1892fa1283fb28a6ca5 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:51:56 -0400 Subject: [PATCH 18/33] fixing snapshots and unused variable nit --- snapshots/PermissionedV4RouterTest.json | 2 +- test/hooks/permissionedPools/PermissionedPositionManager.t.sol | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/snapshots/PermissionedV4RouterTest.json b/snapshots/PermissionedV4RouterTest.json index 818d71eaa..79d2aa606 100644 --- a/snapshots/PermissionedV4RouterTest.json +++ b/snapshots/PermissionedV4RouterTest.json @@ -1,3 +1,3 @@ { - "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "223296" + "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "231176" } \ No newline at end of file diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 6608b5d60..5b3865e0b 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -1417,7 +1417,6 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq // Should revert when trying to mint with SWAP_ALLOWED PositionConfig memory config = PositionConfig({poolKey: insecureKey, tickLower: -120, tickUpper: 120}); uint256 liquidity = 1e18; - uint256 tokenId = lpm.nextTokenId(); vm.startPrank(alice); vm.expectRevert(Unauthorized.selector); From 688699123cc07d7d5868f9424e6ae591e75f3726 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:55:10 -0400 Subject: [PATCH 19/33] fixing snapshots one more time --- snapshots/BaseActionsRouterTest.json | 2 +- snapshots/PaymentsTests.json | 8 +-- snapshots/PosMGasTest.json | 78 +++++++++++++-------------- snapshots/PositionDescriptorTest.json | 2 +- snapshots/QuoterTest.json | 26 ++++----- snapshots/StateViewTest.json | 20 +++---- snapshots/V4RouterTest.json | 50 ++++++++--------- 7 files changed, 93 insertions(+), 93 deletions(-) diff --git a/snapshots/BaseActionsRouterTest.json b/snapshots/BaseActionsRouterTest.json index b40a29c24..d7241dbe2 100644 --- a/snapshots/BaseActionsRouterTest.json +++ b/snapshots/BaseActionsRouterTest.json @@ -1,3 +1,3 @@ { - "BaseActionsRouter_mock10commands": "39607" + "BaseActionsRouter_mock10commands": "63076" } \ No newline at end of file diff --git a/snapshots/PaymentsTests.json b/snapshots/PaymentsTests.json index d005ec134..c3e810d19 100644 --- a/snapshots/PaymentsTests.json +++ b/snapshots/PaymentsTests.json @@ -1,6 +1,6 @@ { - "Payments_swap_settleFromCaller_takeAllToMsgSender": "124759", - "Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "126152", - "Payments_swap_settleWithBalance_takeAllToMsgSender": "116711", - "Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "116534" + "Payments_swap_settleFromCaller_takeAllToMsgSender": "133006", + "Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "134475", + "Payments_swap_settleWithBalance_takeAllToMsgSender": "126957", + "Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "127095" } \ No newline at end of file diff --git a/snapshots/PosMGasTest.json b/snapshots/PosMGasTest.json index d55abd08c..579fa6642 100644 --- a/snapshots/PosMGasTest.json +++ b/snapshots/PosMGasTest.json @@ -1,43 +1,43 @@ { - "PositionManager_burn_empty": "16871", - "PositionManager_burn_empty_native": "16871", - "PositionManager_burn_nonEmpty_native_withClose": "63380", - "PositionManager_burn_nonEmpty_native_withTakePair": "62631", - "PositionManager_burn_nonEmpty_withClose": "60378", - "PositionManager_burn_nonEmpty_withTakePair": "59629", - "PositionManager_collect_native": "85852", - "PositionManager_collect_sameRange": "82850", - "PositionManager_collect_withClose": "82850", - "PositionManager_collect_withTakePair": "82101", - "PositionManager_decreaseLiquidity_native": "55859", - "PositionManager_decreaseLiquidity_withClose": "52857", - "PositionManager_decreaseLiquidity_withTakePair": "52108", - "PositionManager_decrease_burnEmpty": "62811", - "PositionManager_decrease_burnEmpty_native": "65813", - "PositionManager_decrease_sameRange_allLiquidity": "50143", - "PositionManager_decrease_take_take": "52841", - "PositionManager_increaseLiquidity_erc20_withClose": "63940", - "PositionManager_increaseLiquidity_erc20_withSettlePair": "63155", - "PositionManager_increaseLiquidity_native": "60735", - "PositionManager_increase_autocompoundExactUnclaimedFees": "70594", - "PositionManager_increase_autocompoundExcessFeesCredit": "89962", - "PositionManager_increase_autocompound_clearExcess": "82118", - "PositionManager_mint_native": "351583", - "PositionManager_mint_nativeWithSweep_withClose": "359614", - "PositionManager_mint_nativeWithSweep_withSettlePair": "358657", - "PositionManager_mint_onSameTickLower": "271298", - "PositionManager_mint_onSameTickUpper": "275950", - "PositionManager_mint_sameRange": "176554", - "PositionManager_mint_settleWithBalance_sweep": "401096", - "PositionManager_mint_warmedPool_differentRange": "276594", - "PositionManager_mint_withClose": "407888", - "PositionManager_mint_withSettlePair": "407017", - "PositionManager_multicall_initialize_mint": "445356", - "PositionManager_permit": "51379", - "PositionManager_permit_secondPosition": "29479", - "PositionManager_permit_twice": "7579", - "PositionManager_subscribe": "57900", - "PositionManager_unsubscribe": "28116", + "PositionManager_burn_empty": "51102", + "PositionManager_burn_empty_native": "51102", + "PositionManager_burn_nonEmpty_native_withClose": "127695", + "PositionManager_burn_nonEmpty_native_withTakePair": "127137", + "PositionManager_burn_nonEmpty_withClose": "134600", + "PositionManager_burn_nonEmpty_withTakePair": "134043", + "PositionManager_collect_native": "147732", + "PositionManager_collect_sameRange": "156364", + "PositionManager_collect_withClose": "156364", + "PositionManager_collect_withTakePair": "155679", + "PositionManager_decreaseLiquidity_native": "113681", + "PositionManager_decreaseLiquidity_withClose": "121933", + "PositionManager_decreaseLiquidity_withTakePair": "121248", + "PositionManager_decrease_burnEmpty": "137500", + "PositionManager_decrease_burnEmpty_native": "130595", + "PositionManager_decrease_sameRange_allLiquidity": "134649", + "PositionManager_decrease_take_take": "122490", + "PositionManager_increaseLiquidity_erc20_withClose": "160652", + "PositionManager_increaseLiquidity_erc20_withSettlePair": "159563", + "PositionManager_increaseLiquidity_native": "143532", + "PositionManager_increase_autocompoundExactUnclaimedFees": "138231", + "PositionManager_increase_autocompoundExcessFeesCredit": "179593", + "PositionManager_increase_autocompound_clearExcess": "149511", + "PositionManager_mint_native": "367550", + "PositionManager_mint_nativeWithSweep_withClose": "376153", + "PositionManager_mint_nativeWithSweep_withSettlePair": "375348", + "PositionManager_mint_onSameTickLower": "319298", + "PositionManager_mint_onSameTickUpper": "319940", + "PositionManager_mint_sameRange": "245522", + "PositionManager_mint_settleWithBalance_sweep": "420760", + "PositionManager_mint_warmedPool_differentRange": "325316", + "PositionManager_mint_withClose": "421910", + "PositionManager_mint_withSettlePair": "420939", + "PositionManager_multicall_initialize_mint": "458237", + "PositionManager_permit": "79163", + "PositionManager_permit_secondPosition": "62075", + "PositionManager_permit_twice": "44975", + "PositionManager_subscribe": "87968", + "PositionManager_unsubscribe": "62697", "position manager initcode hash (without constructor params, as uint256)": "81827502601055975118808937107769364319765058198432540518516360048852193272922", "positionManager bytecode size": "23877" } \ No newline at end of file diff --git a/snapshots/PositionDescriptorTest.json b/snapshots/PositionDescriptorTest.json index 3ae12d160..a9f6400b3 100644 --- a/snapshots/PositionDescriptorTest.json +++ b/snapshots/PositionDescriptorTest.json @@ -1,5 +1,5 @@ { "position descriptor initcode hash (without constructor params, as uint256)": "62242073579002066175147720437850721791653757771535856244007756965162378120375", "positionDescriptor bytecode size": "24069", - "proxy bytecode size": "1968" + "proxy bytecode size": "1488" } \ No newline at end of file diff --git a/snapshots/QuoterTest.json b/snapshots/QuoterTest.json index 19648b93f..3ecbe3350 100644 --- a/snapshots/QuoterTest.json +++ b/snapshots/QuoterTest.json @@ -1,15 +1,15 @@ { - "Quoter_exactInputSingle_oneForZero_multiplePositions": "139681", - "Quoter_exactInputSingle_zeroForOne_multiplePositions": "146706", - "Quoter_exactOutputSingle_oneForZero": "70940", - "Quoter_exactOutputSingle_zeroForOne": "77112", - "Quoter_quoteExactInput_oneHop_1TickLoaded": "116810", - "Quoter_quoteExactInput_oneHop_initializedAfter": "141778", - "Quoter_quoteExactInput_oneHop_startingInitialized": "62264", - "Quoter_quoteExactInput_twoHops": "210437", - "Quoter_quoteExactOutput_oneHop_1TickLoaded": "116866", - "Quoter_quoteExactOutput_oneHop_2TicksLoaded": "148769", - "Quoter_quoteExactOutput_oneHop_initializedAfter": "116926", - "Quoter_quoteExactOutput_oneHop_startingInitialized": "70805", - "Quoter_quoteExactOutput_twoHops": "211021" + "Quoter_exactInputSingle_oneForZero_multiplePositions": "146134", + "Quoter_exactInputSingle_zeroForOne_multiplePositions": "152349", + "Quoter_exactOutputSingle_oneForZero": "79477", + "Quoter_exactOutputSingle_zeroForOne": "84722", + "Quoter_quoteExactInput_oneHop_1TickLoaded": "122938", + "Quoter_quoteExactInput_oneHop_initializedAfter": "147573", + "Quoter_quoteExactInput_oneHop_startingInitialized": "80848", + "Quoter_quoteExactInput_twoHops": "205152", + "Quoter_quoteExactOutput_oneHop_1TickLoaded": "122434", + "Quoter_quoteExactOutput_oneHop_2TicksLoaded": "153089", + "Quoter_quoteExactOutput_oneHop_initializedAfter": "122461", + "Quoter_quoteExactOutput_oneHop_startingInitialized": "98755", + "Quoter_quoteExactOutput_twoHops": "204880" } \ No newline at end of file diff --git a/snapshots/StateViewTest.json b/snapshots/StateViewTest.json index e42038822..ab141d8b1 100644 --- a/snapshots/StateViewTest.json +++ b/snapshots/StateViewTest.json @@ -1,12 +1,12 @@ { - "StateView_extsload_getFeeGrowthGlobals": "4529", - "StateView_extsload_getFeeGrowthInside": "15757", - "StateView_extsload_getLiquidity": "2737", - "StateView_extsload_getPositionInfo": "5752", - "StateView_extsload_getPositionLiquidity": "3408", - "StateView_extsload_getSlot0": "2857", - "StateView_extsload_getTickBitmap": "3283", - "StateView_extsload_getTickFeeGrowthOutside": "5392", - "StateView_extsload_getTickInfo": "5873", - "StateView_extsload_getTickLiquidity": "3508" + "StateView_extsload_getFeeGrowthGlobals": "8703", + "StateView_extsload_getFeeGrowthInside": "24375", + "StateView_extsload_getLiquidity": "5939", + "StateView_extsload_getPositionInfo": "11261", + "StateView_extsload_getPositionLiquidity": "6191", + "StateView_extsload_getSlot0": "5986", + "StateView_extsload_getTickBitmap": "5932", + "StateView_extsload_getTickFeeGrowthOutside": "8990", + "StateView_extsload_getTickInfo": "11193", + "StateView_extsload_getTickLiquidity": "6186" } \ No newline at end of file diff --git a/snapshots/V4RouterTest.json b/snapshots/V4RouterTest.json index 5f8709ddb..952f619d9 100644 --- a/snapshots/V4RouterTest.json +++ b/snapshots/V4RouterTest.json @@ -1,27 +1,27 @@ { - "V4Router_Bytecode": "11442", - "V4Router_ExactIn1Hop_nativeIn": "111493", - "V4Router_ExactIn1Hop_nativeOut": "110918", - "V4Router_ExactIn1Hop_oneForZero": "120016", - "V4Router_ExactIn1Hop_zeroForOne": "127319", - "V4Router_ExactIn2Hops": "185153", - "V4Router_ExactIn2Hops_nativeIn": "176610", - "V4Router_ExactIn3Hops": "242988", - "V4Router_ExactIn3Hops_nativeIn": "234445", - "V4Router_ExactInputSingle": "126106", - "V4Router_ExactInputSingle_nativeIn": "110280", - "V4Router_ExactInputSingle_nativeOut": "109695", - "V4Router_ExactOut1Hop_nativeIn_sweepETH": "117775", - "V4Router_ExactOut1Hop_nativeOut": "111997", - "V4Router_ExactOut1Hop_oneForZero": "121095", - "V4Router_ExactOut1Hop_zeroForOne": "126565", - "V4Router_ExactOut2Hops": "185415", - "V4Router_ExactOut2Hops_nativeIn": "182095", - "V4Router_ExactOut3Hops": "244281", - "V4Router_ExactOut3Hops_nativeIn": "240961", - "V4Router_ExactOut3Hops_nativeOut": "235183", - "V4Router_ExactOutputSingle": "125324", - "V4Router_ExactOutputSingle_nativeIn_sweepETH": "116534", - "V4Router_ExactOutputSingle_nativeOut": "110849", - "router initcode hash (without constructor params, as uint256)": "16473836486703886561435051652926532733159549293834562681012203074313944726145" + "V4Router_Bytecode": "8975", + "V4Router_ExactIn1Hop_nativeIn": "121653", + "V4Router_ExactIn1Hop_nativeOut": "120119", + "V4Router_ExactIn1Hop_oneForZero": "128991", + "V4Router_ExactIn1Hop_zeroForOne": "135517", + "V4Router_ExactIn2Hops": "192843", + "V4Router_ExactIn2Hops_nativeIn": "178979", + "V4Router_ExactIn3Hops": "250218", + "V4Router_ExactIn3Hops_nativeIn": "236354", + "V4Router_ExactInputSingle": "134001", + "V4Router_ExactInputSingle_nativeIn": "120137", + "V4Router_ExactInputSingle_nativeOut": "118581", + "V4Router_ExactOut1Hop_nativeIn_sweepETH": "127940", + "V4Router_ExactOut1Hop_nativeOut": "121329", + "V4Router_ExactOut1Hop_oneForZero": "130201", + "V4Router_ExactOut1Hop_zeroForOne": "134858", + "V4Router_ExactOut2Hops": "191291", + "V4Router_ExactOut2Hops_nativeIn": "184373", + "V4Router_ExactOut3Hops": "247788", + "V4Router_ExactOut3Hops_nativeIn": "240870", + "V4Router_ExactOut3Hops_nativeOut": "224943", + "V4Router_ExactOutputSingle": "133337", + "V4Router_ExactOutputSingle_nativeIn_sweepETH": "126419", + "V4Router_ExactOutputSingle_nativeOut": "119821", + "router initcode hash (without constructor params, as uint256)": "27545762869727400677117557485685740862616789454191614676777323590122002226479" } \ No newline at end of file From 5cc4b17377d0e15f929bb28de7cb9c0b7f0169b9 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:11:22 -0400 Subject: [PATCH 20/33] fix: validate mint recipient has LIQUIDITY_ALLOWED in PermissionedPositionManager --- .../PermissionedPositionManager.sol | 10 +++ .../PermissionedPositionManager.t.sol | 70 ++++++++----------- .../shared/PermissionedRoutingTestHelpers.sol | 1 + 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index bef51d490..df5dcb90a 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -87,9 +87,19 @@ contract PermissionedPositionManager is PositionManager { ) internal override { // allowlist is verified in the hook call if (!_checkAllowedHooks(poolKey)) revert InvalidHook(); + _checkRecipientAllowed(poolKey.currency0, owner); + _checkRecipientAllowed(poolKey.currency1, owner); super._mint(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData); } + function _checkRecipientAllowed(Currency currency, address recipient) internal view { + address permissionedToken = _verifiedPermissionedTokenOf(currency); + if (permissionedToken == address(0)) return; + if (!IPermissionsAdapter(Currency.unwrap(currency)).isAllowed(recipient, PermissionFlags.LIQUIDITY_ALLOWED)) { + revert Unauthorized(); + } + } + function _checkAllowedHooks(PoolKey calldata poolKey) internal view returns (bool) { return _checkAllowedHook(poolKey.currency0, poolKey.hooks) && _checkAllowedHook(poolKey.currency1, poolKey.hooks); diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 5b3865e0b..780506fae 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -710,16 +710,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq permit2.approve(address(originalToken0), address(lpm), type(uint160).max, type(uint48).max); permit2.approve(Currency.unwrap(currency1), address(lpm), type(uint160).max, type(uint48).max); - // This should revert because the user is not in the allowlist - vm.expectRevert( - abi.encodeWithSelector( - CustomRevert.WrappedError.selector, - address(permissionedHooks), - IHooks.beforeAddLiquidity.selector, - abi.encodeWithSelector(Unauthorized.selector), - abi.encodeWithSelector(HookCallFailed.selector) - ) - ); + // This should revert because the recipient is not in the allowlist + vm.expectRevert(Unauthorized.selector); mint(config, liquidity, unauthorizedUser, ZERO_BYTES); vm.stopPrank(); } @@ -776,16 +768,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq tokenId = lpm.nextTokenId(); - // Increasing liquidity should revert because the user is no longer in the allowlist - vm.expectRevert( - abi.encodeWithSelector( - CustomRevert.WrappedError.selector, - address(permissionedHooks), - IHooks.beforeAddLiquidity.selector, - abi.encodeWithSelector(Unauthorized.selector), - abi.encodeWithSelector(HookCallFailed.selector) - ) - ); + // Increasing liquidity should revert because the recipient is no longer in the allowlist + vm.expectRevert(Unauthorized.selector); mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); vm.stopPrank(); @@ -822,16 +806,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq MockERC20(Currency.unwrap(currencyUnpermissioned)).approve(address(permit2), type(uint256).max); permit2.approve(Currency.unwrap(currencyUnpermissioned), address(lpm), type(uint160).max, type(uint48).max); - // This should revert because the user is not in the allowlist - vm.expectRevert( - abi.encodeWithSelector( - CustomRevert.WrappedError.selector, - address(permissionedHooks), - IHooks.beforeAddLiquidity.selector, - abi.encodeWithSelector(Unauthorized.selector), - abi.encodeWithSelector(HookCallFailed.selector) - ) - ); + // This should revert because the recipient is not in the allowlist + vm.expectRevert(Unauthorized.selector); mint(config, liquidity, unauthorizedUser, ZERO_BYTES); vm.stopPrank(); } @@ -902,16 +878,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq MockERC20(Currency.unwrap(currencyPermissioned)).approve(address(permit2), type(uint256).max); permit2.approve(Currency.unwrap(currencyPermissioned), address(lpm), type(uint160).max, type(uint48).max); - // This should revert because the user is not in the allowlist - vm.expectRevert( - abi.encodeWithSelector( - CustomRevert.WrappedError.selector, - address(permissionedHooks), - IHooks.beforeAddLiquidity.selector, - abi.encodeWithSelector(Unauthorized.selector), - abi.encodeWithSelector(HookCallFailed.selector) - ) - ); + // This should revert because the recipient is not in the allowlist + vm.expectRevert(Unauthorized.selector); mint(config, liquidity, unauthorizedUser, ZERO_BYTES); vm.stopPrank(); } @@ -1423,4 +1391,26 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); vm.stopPrank(); } + + function test_permissioned_mint_to_unauthorized_recipient_reverts() public { + // Caller (address(this)) is allowed, but unauthorizedUser is not + // The hook will pass because the caller is authorized, + // but _mint should revert because the recipient lacks LIQUIDITY_ALLOWED + PositionConfig memory config = PositionConfig({poolKey: key0, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + + vm.expectRevert(Unauthorized.selector); + mint(config, liquidity, unauthorizedUser, ZERO_BYTES); + } + + function test_permissioned_mint_to_authorized_recipient_succeeds() public { + // Caller (address(this)) is allowed, alice is also allowed + // Minting to an authorized recipient should succeed + PositionConfig memory config = PositionConfig({poolKey: key0, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + uint256 tokenId = lpm.nextTokenId(); + + mint(config, liquidity, alice, ZERO_BYTES); + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + } } diff --git a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol index 653e838b0..293168be4 100644 --- a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol @@ -446,6 +446,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 MockPermissionedToken mockPermissionedToken = MockPermissionedToken(Currency.unwrap(currency)); mockAllowlistChecker = new MockAllowlistChecker(mockPermissionedToken); mockPermissionedToken.setAllowlist(address(this), PermissionFlags.ALL_ALLOWED); + mockPermissionedToken.setAllowlist(msg.sender, PermissionFlags.ALL_ALLOWED); mockPermissionedToken.setAllowlist(address(permissionedRouter), PermissionFlags.ALL_ALLOWED); mockPermissionedToken.setAllowlist(address(permissionedHooks), PermissionFlags.ALL_ALLOWED); mockPermissionedToken.setAllowlist(address(positionManager), PermissionFlags.ALL_ALLOWED); From fabaf71671f00dd37b860ca0a3a42d5a580b8246 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:38:24 -0400 Subject: [PATCH 21/33] fix(permissioned-position-manager): add _mapSettleAmount override for CONTRACT_BALANCE --- .../PermissionedPositionManager.sol | 10 ++++ .../PermissionedPositionManager.t.sol | 54 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index df5dcb90a..cce575e06 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -14,6 +14,7 @@ import {IPermissionsAdapter} from "./interfaces/IPermissionsAdapter.sol"; import {IPermissionsAdapterFactory} from "./interfaces/IPermissionsAdapterFactory.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {PermissionFlags} from "./libraries/PermissionFlags.sol"; +import {ActionConstants} from "../../libraries/ActionConstants.sol"; contract PermissionedPositionManager is PositionManager { IPermissionsAdapterFactory public immutable PERMISSIONS_ADAPTER_FACTORY; @@ -143,6 +144,15 @@ contract PermissionedPositionManager is PositionManager { return PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(currency)); } + /// @notice Calculates the amount for a settle action + function _mapSettleAmount(uint256 amount, Currency currency) internal view override returns (uint256) { + address permissionedToken = _verifiedPermissionedTokenOf(currency); + if (permissionedToken == address(0) || amount != ActionConstants.CONTRACT_BALANCE) { + return super._mapSettleAmount(amount, currency); + } + return Currency.wrap(permissionedToken).balanceOfSelf(); + } + function _getOwner(Currency currency) internal view returns (address) { address permissionsAdapter = Currency.unwrap(currency); address permissionedToken = _verifiedPermissionedTokenOf(currency); diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 780506fae..bfa05e5e5 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -1209,6 +1209,60 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq secondaryPosm.modifyLiquidities(calls, _deadline); } + function test_mint_from_contract_balance_uses_contract_balance_settle() public { + _test_mint_from_contract_balance_uses_contract_balance_settle(key0); + _test_mint_from_contract_balance_uses_contract_balance_settle(key1); + _test_mint_from_contract_balance_uses_contract_balance_settle(key2); + } + + function _test_mint_from_contract_balance_uses_contract_balance_settle(PoolKey memory key) internal { + uint256 amount0ToTransfer = 100e18; + uint256 amount1ToTransfer = 100e18; + + // Transfer underlying permissioned tokens to the POSM + setupContractBalance(key, amount0ToTransfer, amount1ToTransfer); + + BalanceInfo memory balanceInfo = getBalanceInfo(key); + + // Create a mint plan that uses CONTRACT_BALANCE instead of OPEN_DELTA + int24 tickLower = -int24(key.tickSpacing); + int24 tickUpper = int24(key.tickSpacing); + uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + amount0ToTransfer, + amount1ToTransfer + ); + + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + + Plan memory planner = Planner.init(); + planner.add( + Actions.MINT_POSITION, + abi.encode( + config.poolKey, + config.tickLower, + config.tickUpper, + liquidityToAdd, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + address(this), + ZERO_BYTES + ) + ); + + // Use CONTRACT_BALANCE instead of OPEN_DELTA — this is the broken path + planner.add(Actions.SETTLE, abi.encode(key.currency0, ActionConstants.CONTRACT_BALANCE, false)); + planner.add(Actions.SETTLE, abi.encode(key.currency1, ActionConstants.CONTRACT_BALANCE, false)); + + bytes memory calls = planner.finalizeModifyLiquidityWithClose(config.poolKey); + uint256 tokenId = lpm.nextTokenId(); + lpm.modifyLiquidities(calls, _deadline); + + verifyMintResults(key, balanceInfo, tokenId); + } + // ===== PERMISSION FLAG TESTS ===== function test_permission_flag_none() public { From de71913bf50c0678035b42db1cfa70e6b75448f3 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Tue, 14 Apr 2026 02:29:31 -0400 Subject: [PATCH 22/33] refactor(permissioned-routing): strip PermissionedV4Router to abstract contract --- .../PermissionedV4Router.sol | 194 +++--------------- .../shared/PermissionedDeployers.sol | 8 +- .../shared/PermissionedRoutingTestHelpers.sol | 8 +- test/mocks/MockPermissionedRouter.sol | 128 ++++++++++++ test/shared/Deploy.sol | 1 - 5 files changed, 167 insertions(+), 172 deletions(-) create mode 100644 test/mocks/MockPermissionedRouter.sol diff --git a/src/hooks/permissionedPools/PermissionedV4Router.sol b/src/hooks/permissionedPools/PermissionedV4Router.sol index 2fa02d2b0..0bb47e1ae 100644 --- a/src/hooks/permissionedPools/PermissionedV4Router.sol +++ b/src/hooks/permissionedPools/PermissionedV4Router.sol @@ -2,198 +2,66 @@ pragma solidity ^0.8.0; import {ActionConstants} from "../../libraries/ActionConstants.sol"; -import {ReentrancyLock} from "../../base/ReentrancyLock.sol"; import {V4Router, IPoolManager, Currency} from "../../V4Router.sol"; import {IPermissionsAdapter} from "./interfaces/IPermissionsAdapter.sol"; import {IPermissionsAdapterFactory} from "./interfaces/IPermissionsAdapterFactory.sol"; -import {IWETH9} from "../../interfaces/external/IWETH9.sol"; -import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; import {PermissionFlags} from "./libraries/PermissionFlags.sol"; -contract PermissionedV4Router is V4Router, ReentrancyLock { - IAllowanceTransfer public immutable PERMIT2; +/// @title Abstract base for routers that support permissioned V4 pools +/// @notice Provides _pay and _mapSettleAmount overrides for wrapping/unwrapping permissioned tokens. +/// Concrete routers (e.g., UniversalRouter's V4SwapRouter) inherit this contract. +abstract contract PermissionedV4Router is V4Router { IPermissionsAdapterFactory public immutable PERMISSIONS_ADAPTER_FACTORY; - IWETH9 public immutable WETH9; error Unauthorized(); - error HookNotImplemented(); - error InvalidEthSender(); - error TransactionDeadlinePassed(); - error LengthMismatch(); - error SliceOutOfBounds(); - error InvalidCommandType(uint256 commandType); - /// @notice Thrown when a required command has failed - error ExecutionFailed(uint256 commandIndex, bytes message); - // Commands - uint256 constant COMMAND_V4_SWAP = 0x10; - uint256 constant COMMAND_PERMIT2_PERMIT = 0x0a; - bytes1 internal constant COMMAND_FLAG_ALLOW_REVERT = 0x80; - bytes1 internal constant COMMAND_TYPE_MASK = 0x3f; - - constructor( - IPoolManager poolManager_, - IAllowanceTransfer permit2, - IPermissionsAdapterFactory permissionsAdapterFactory, - IWETH9 weth9 - ) V4Router(poolManager_) { - PERMIT2 = permit2; - PERMISSIONS_ADAPTER_FACTORY = permissionsAdapterFactory; - WETH9 = weth9; - } - - modifier checkDeadline(uint256 deadline) { - if (block.timestamp > deadline) revert TransactionDeadlinePassed(); - _; - } - - /// @notice To receive ETH from WETH - receive() external payable { - if (msg.sender != address(WETH9) && msg.sender != address(poolManager)) revert InvalidEthSender(); - } - - /// @notice Executes encoded commands along with provided inputs. Reverts if deadline has expired. - /// @param commands A set of concatenated commands, each 1 byte in length - /// @param inputs An array of byte strings containing abi encoded inputs for each command - /// @param deadline The deadline by which the transaction must be executed - function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) - external - payable - checkDeadline(deadline) + constructor(IPoolManager poolManager_, IPermissionsAdapterFactory permissionsAdapterFactory) + V4Router(poolManager_) { - execute(commands, inputs); - } - - /// @notice Executes encoded commands along with provided inputs. - /// @param commands A set of concatenated commands, each 1 byte in length - /// @param inputs An array of byte strings containing abi encoded inputs for each command - function execute(bytes calldata commands, bytes[] calldata inputs) public payable isNotLocked { - bool success; - bytes memory output; - uint256 numCommands = commands.length; - if (inputs.length != numCommands) revert LengthMismatch(); - - // loop through all given commands, execute them and pass along outputs as defined - for (uint256 commandIndex = 0; commandIndex < numCommands; commandIndex++) { - bytes1 command = commands[commandIndex]; - - bytes calldata input = inputs[commandIndex]; - - (success, output) = _dispatch(command, input); - - if (!success && _successRequired(command)) { - revert ExecutionFailed({commandIndex: commandIndex, message: output}); - } - } - } - - /// @notice Public view function to be used instead of msg.sender, as the contract performs self-reentrancy and at - /// times msg.sender == address(this). Instead msgSender() returns the initiator of the lock - /// @dev overrides BaseActionsRouter.msgSender in V4Router - function msgSender() public view override returns (address) { - return _getLocker(); - } - - /// @notice Decodes and executes the given command with the given inputs - function _dispatch(bytes1 commandType, bytes calldata inputs) internal returns (bool success, bytes memory output) { - uint256 command = uint8(commandType & COMMAND_TYPE_MASK); - success = true; - if (command == COMMAND_PERMIT2_PERMIT) { - // equivalent: abi.decode(inputs, (IAllowanceTransfer.PermitSingle, bytes)) - IAllowanceTransfer.PermitSingle calldata permitSingle; - assembly { - permitSingle := inputs.offset - } - bytes calldata data = _toBytes(inputs, 6); // PermitSingle takes first 6 slots (0..5) - - (success, output) = address(PERMIT2) - .call( - abi.encodeWithSignature( - "permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)", - msgSender(), - permitSingle, - data - ) - ); - } else if (command == COMMAND_V4_SWAP) { - _executeActions(inputs); - } else { - revert InvalidCommandType(command); - } + PERMISSIONS_ADAPTER_FACTORY = permissionsAdapterFactory; } - function _pay(Currency currency, address payer, uint256 amount) internal override { - address permissionedToken = PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(currency)); + function _pay(Currency currency, address payer, uint256 amount) internal virtual override { + address permissionedToken = address(PERMISSIONS_ADAPTER_FACTORY) == address(0) + ? address(0) + : PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(currency)); if (permissionedToken == address(0)) { - // token is not a permissioned token, use the default implementation - if (payer == address(this)) { - currency.transfer(address(poolManager), amount); - } else { - // Casting from uint256 to uint160 is safe due to limits on the total supply of a pool - PERMIT2.transferFrom(payer, address(poolManager), uint160(amount), Currency.unwrap(currency)); - } + _payStandard(currency, payer, amount); return; } // token is permissioned, wrap the token and transfer it to the pool manager IPermissionsAdapter permissionsAdapter = IPermissionsAdapter(Currency.unwrap(currency)); + if (!permissionsAdapter.isAllowed(msgSender(), PermissionFlags.SWAP_ALLOWED)) { + revert Unauthorized(); + } if (payer == address(this)) { - // allowlist check necessary to ensure a disallowed user cannot sell a permissioned token - if (!permissionsAdapter.isAllowed(msgSender(), PermissionFlags.SWAP_ALLOWED)) { - revert Unauthorized(); - } Currency.wrap(permissionedToken).transfer(address(permissionsAdapter), amount); permissionsAdapter.wrapToPoolManager(amount); } else { - // Check swap permission for the actual user - if (!permissionsAdapter.isAllowed(msgSender(), PermissionFlags.SWAP_ALLOWED)) { - revert Unauthorized(); - } - // token is a permissioned token, wrap the token - PERMIT2.transferFrom(payer, address(permissionsAdapter), uint160(amount), permissionedToken); - permissionsAdapter.wrapToPoolManager(amount); + _payPermissionedFromPayer(payer, permissionsAdapter, permissionedToken, amount); } } + /// @notice Hook for concrete routers to implement standard (non-permissioned) payment + function _payStandard(Currency currency, address payer, uint256 amount) internal virtual; + + /// @notice Hook for concrete routers to implement payer-to-adapter transfer (e.g., via Permit2) + function _payPermissionedFromPayer( + address payer, + IPermissionsAdapter permissionsAdapter, + address permissionedToken, + uint256 amount + ) internal virtual; + /// @notice Calculates the amount for a settle action - function _mapSettleAmount(uint256 amount, Currency currency) internal view override returns (uint256) { - address permissionedToken = PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(currency)); + function _mapSettleAmount(uint256 amount, Currency currency) internal view virtual override returns (uint256) { + address permissionedToken = address(PERMISSIONS_ADAPTER_FACTORY) == address(0) + ? address(0) + : PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(currency)); // use the default implementation unless the currency is a permissioned token with a balance on the router if (permissionedToken == address(0) || amount != ActionConstants.CONTRACT_BALANCE) { return super._mapSettleAmount(amount, currency); } return Currency.wrap(permissionedToken).balanceOfSelf(); } - - function _successRequired(bytes1 command) internal pure returns (bool) { - return command & COMMAND_FLAG_ALLOW_REVERT == 0; - } - - /// @notice Decode the `_arg`-th element in `_bytes` as a dynamic array - /// @dev This function is copied from BytesLib in universal-router to avoid adding it into the repositoriy - function _toLengthOffset(bytes calldata _bytes, uint256 _arg) - private - pure - returns (uint256 length, uint256 offset) - { - uint256 relativeOffset; - assembly { - // The offset of the `_arg`-th element is `32 * arg`, which stores the offset of the length pointer. - // shl(5, x) is equivalent to mul(32, x) - let lengthPtr := add(_bytes.offset, calldataload(add(_bytes.offset, shl(5, _arg)))) - length := calldataload(lengthPtr) - offset := add(lengthPtr, 0x20) - relativeOffset := sub(offset, _bytes.offset) - } - if (_bytes.length < length + relativeOffset) revert SliceOutOfBounds(); - } - - /// @notice Decode the `_arg`-th element in `_bytes` as `bytes` - /// @dev This function is copied from BytesLib in universal-router to avoid adding it into the repositoriy - function _toBytes(bytes calldata _bytes, uint256 _arg) private pure returns (bytes calldata res) { - (uint256 length, uint256 offset) = _toLengthOffset(_bytes, _arg); - assembly { - res.length := length - res.offset := offset - } - } } diff --git a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol index 66b00541e..5418d9eb0 100644 --- a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol @@ -34,7 +34,7 @@ import {ActionConstants} from "../../../../src/libraries/ActionConstants.sol"; import { IPermissionsAdapterFactory } from "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; -import {PermissionedV4Router} from "../../../../src/hooks/permissionedPools/PermissionedV4Router.sol"; +import {MockPermissionedRouter} from "../../../mocks/MockPermissionedRouter.sol"; import {MockPermissionedToken} from "../PermissionedPoolsBase.sol"; import {MockV4Router} from "../../../mocks/MockV4Router.sol"; import {MockHooks} from "../mocks/MockHooks.sol"; @@ -75,7 +75,7 @@ contract PermissionedDeployers is Test { PoolModifyLiquidityTest public modifyLiquidityRouter; PoolModifyLiquidityTestNoChecks public modifyLiquidityNoChecks; SwapRouterNoChecks public swapRouterNoChecks; - PermissionedV4Router public permissionedSwapRouter; + MockPermissionedRouter public permissionedSwapRouter; PoolSwapTest public swapRouter; PoolDonateTest public donateRouter; PoolTakeTest public takeRouter; @@ -166,7 +166,7 @@ contract PermissionedDeployers is Test { returns (address deployedAddr) { bytes memory routerBytecode = abi.encodePacked( - vm.getCode("PermissionedV4Router.sol:PermissionedV4Router"), + vm.getCode("MockPermissionedRouter.sol:MockPermissionedRouter"), abi.encode( manager, IAllowanceTransfer(permit2_), @@ -193,7 +193,7 @@ contract PermissionedDeployers is Test { secondaryPermissionedHooks = IHooks(deployPermissionedHooks(address(manager), permissionsAdapterFactoryAddress)); insecureHooks = IHooks(deployInsecureHooks(address(manager))); address deployedAddr = deployPermissionedV4Router(permit2_, permissionsAdapterFactoryAddress, weth9); - permissionedSwapRouter = PermissionedV4Router(payable(deployedAddr)); + permissionedSwapRouter = MockPermissionedRouter(payable(deployedAddr)); swapRouter = PoolSwapTest(deployedAddr); deployMiscRouters(); } diff --git a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol index 293168be4..32e10fc34 100644 --- a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol @@ -12,7 +12,7 @@ import {WETH} from "solmate/src/tokens/WETH.sol"; import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; import {Deploy} from "test/shared/Deploy.sol"; -import {PermissionedV4Router} from "../../../../src/hooks/permissionedPools/PermissionedV4Router.sol"; +import {MockPermissionedRouter} from "../../../mocks/MockPermissionedRouter.sol"; import {Plan, Planner} from "../../../shared/Planner.sol"; import {PathKey} from "../../../../src/libraries/PathKey.sol"; import {Actions} from "../../../../src/libraries/Actions.sol"; @@ -36,7 +36,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 uint256 public constant MIN_TAKE_AMOUNT = 0; // Permissioned components - PermissionedV4Router public permissionedRouter; + MockPermissionedRouter public permissionedRouter; IAllowanceTransfer public permit2; MockAllowlistChecker public mockAllowlistChecker; IAllowlistChecker public allowListChecker; @@ -388,11 +388,11 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 function _deployMockPermissionedRouter() private { bytes memory routerBytecode = abi.encodePacked( - vm.getCode("PermissionedV4Router.sol:PermissionedV4Router"), + vm.getCode("MockPermissionedRouter.sol:MockPermissionedRouter"), abi.encode(manager, permit2, permissionsAdapterFactory, weth9) ); permissionedRouter = - PermissionedV4Router(payable(Deploy.create2(routerBytecode, keccak256("permissionedSwapRouter")))); + MockPermissionedRouter(payable(Deploy.create2(routerBytecode, keccak256("permissionedSwapRouter")))); } function _deployPositionManager() private { diff --git a/test/mocks/MockPermissionedRouter.sol b/test/mocks/MockPermissionedRouter.sol new file mode 100644 index 000000000..0d330102e --- /dev/null +++ b/test/mocks/MockPermissionedRouter.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PermissionedV4Router} from "../../src/hooks/permissionedPools/PermissionedV4Router.sol"; +import {IPermissionsAdapterFactory} from "../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; +import {IPermissionsAdapter} from "../../src/hooks/permissionedPools/interfaces/IPermissionsAdapter.sol"; +import {ReentrancyLock} from "../../src/base/ReentrancyLock.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {IWETH9} from "../../src/interfaces/external/IWETH9.sol"; + +/// @notice Concrete router for testing the abstract PermissionedV4Router. +/// Mirrors the old standalone PermissionedV4Router's execute interface. +contract MockPermissionedRouter is PermissionedV4Router, ReentrancyLock { + IAllowanceTransfer public immutable PERMIT2; + IWETH9 public immutable WETH9; + + error InvalidEthSender(); + error TransactionDeadlinePassed(); + error LengthMismatch(); + error InvalidCommandType(uint256 commandType); + error ExecutionFailed(uint256 commandIndex, bytes message); + error SliceOutOfBounds(); + + uint256 constant COMMAND_V4_SWAP = 0x10; + uint256 constant COMMAND_PERMIT2_PERMIT = 0x0a; + bytes1 internal constant COMMAND_FLAG_ALLOW_REVERT = 0x80; + bytes1 internal constant COMMAND_TYPE_MASK = 0x3f; + + constructor( + IPoolManager poolManager_, + IAllowanceTransfer permit2_, + IPermissionsAdapterFactory permissionsAdapterFactory_, + IWETH9 weth9_ + ) PermissionedV4Router(poolManager_, permissionsAdapterFactory_) { + PERMIT2 = permit2_; + WETH9 = weth9_; + } + + receive() external payable { + if (msg.sender != address(WETH9) && msg.sender != address(poolManager)) revert InvalidEthSender(); + } + + function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable { + if (block.timestamp > deadline) revert TransactionDeadlinePassed(); + execute(commands, inputs); + } + + function execute(bytes calldata commands, bytes[] calldata inputs) public payable isNotLocked { + bool success; + bytes memory output; + uint256 numCommands = commands.length; + if (inputs.length != numCommands) revert LengthMismatch(); + + for (uint256 commandIndex = 0; commandIndex < numCommands; commandIndex++) { + bytes1 command = commands[commandIndex]; + bytes calldata input = inputs[commandIndex]; + (success, output) = _dispatch(command, input); + if (!success && (command & COMMAND_FLAG_ALLOW_REVERT == 0)) { + revert ExecutionFailed({commandIndex: commandIndex, message: output}); + } + } + } + + function msgSender() public view override returns (address) { + return _getLocker(); + } + + function _dispatch(bytes1 commandType, bytes calldata inputs) internal returns (bool success, bytes memory output) { + uint256 command = uint8(commandType & COMMAND_TYPE_MASK); + success = true; + if (command == COMMAND_PERMIT2_PERMIT) { + IAllowanceTransfer.PermitSingle calldata permitSingle; + assembly { + permitSingle := inputs.offset + } + bytes calldata data = _toBytes(inputs, 6); + (success, output) = address(PERMIT2).call( + abi.encodeWithSignature( + "permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)", + msgSender(), + permitSingle, + data + ) + ); + } else if (command == COMMAND_V4_SWAP) { + _executeActions(inputs); + } else { + revert InvalidCommandType(command); + } + } + + function _payStandard(Currency currency, address payer, uint256 amount) internal override { + if (payer == address(this)) { + currency.transfer(address(poolManager), amount); + } else { + PERMIT2.transferFrom(payer, address(poolManager), uint160(amount), Currency.unwrap(currency)); + } + } + + function _payPermissionedFromPayer( + address payer, + IPermissionsAdapter permissionsAdapter, + address permissionedToken, + uint256 amount + ) internal override { + PERMIT2.transferFrom(payer, address(permissionsAdapter), uint160(amount), permissionedToken); + permissionsAdapter.wrapToPoolManager(amount); + } + + function _toBytes(bytes calldata _bytes, uint256 _arg) private pure returns (bytes calldata res) { + uint256 length; + uint256 offset; + uint256 relativeOffset; + assembly { + let lengthPtr := add(_bytes.offset, calldataload(add(_bytes.offset, shl(5, _arg)))) + length := calldataload(lengthPtr) + offset := add(lengthPtr, 0x20) + relativeOffset := sub(offset, _bytes.offset) + } + if (_bytes.length < length + relativeOffset) revert SliceOutOfBounds(); + assembly { + res.length := length + res.offset := offset + } + } +} diff --git a/test/shared/Deploy.sol b/test/shared/Deploy.sol index ee90db98c..a304a1912 100644 --- a/test/shared/Deploy.sol +++ b/test/shared/Deploy.sol @@ -6,7 +6,6 @@ import {IPositionDescriptor} from "../../src/interfaces/IPositionDescriptor.sol" import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; import {IV4Quoter} from "../../src/interfaces/IV4Quoter.sol"; import {IStateView} from "../../src/interfaces/IStateView.sol"; -import {PermissionedV4Router} from "../../src/hooks/permissionedPools/PermissionedV4Router.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; library Deploy { From 147e768463fa62e7b29a93cfdeb1d7279066988c Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Tue, 14 Apr 2026 10:52:10 -0400 Subject: [PATCH 23/33] Validating Permission-Flags in PermissionedV4Router and PermissionedPositionManager (#523) * fix(permissioned-hooks): enforce permission-flags in both payer modes of PermissionedV4Router + PermissionedPositionManager * tests(permissioned-hooks): adding unit tests to validate enforcement of permission-flags in both payer modes of PermissionedV4Router + PermissionedPositionManager * update gas snapshot for PermissionedV4Router * fixing snapshots and unused variable nit * fixing snapshots one more time * nit: updating comments in permposm and permv4router --- snapshots/PermissionedV4RouterTest.json | 2 +- .../PermissionedPositionManager.sol | 6 +- .../PermissionedV4Router.sol | 28 +++--- .../PermissionedPositionManager.t.sol | 82 +++++++++-------- .../PermissionedV4Router.t.sol | 87 +++++++++++-------- .../permissionedPools/mocks/MockHooks.sol | 10 ++- .../shared/PermissionedDeployers.sol | 19 +++- .../shared/PermissionedRoutingTestHelpers.sol | 44 ++++++---- 8 files changed, 169 insertions(+), 109 deletions(-) diff --git a/snapshots/PermissionedV4RouterTest.json b/snapshots/PermissionedV4RouterTest.json index 9bcfabe17..79d2aa606 100644 --- a/snapshots/PermissionedV4RouterTest.json +++ b/snapshots/PermissionedV4RouterTest.json @@ -1,3 +1,3 @@ { - "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "227931" + "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "231176" } \ No newline at end of file diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index 06ea15a88..1d2ab4734 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -112,13 +112,17 @@ contract PermissionedPositionManager is PositionManager { // token is permissioned, wrap the token and transfer it to the pool manager IPermissionsAdapter permissionsAdapter = IPermissionsAdapter(Currency.unwrap(currency)); if (payer == address(this)) { - // @audit is it necessary to check the allowlist here? + // Check liquidity permission for the actual user if (!permissionsAdapter.isAllowed(msgSender(), PermissionFlags.LIQUIDITY_ALLOWED)) { revert Unauthorized(); } Currency.wrap(permissionedToken).transfer(address(permissionsAdapter), amount); permissionsAdapter.wrapToPoolManager(amount); } else { + // Check liquidity permission for the actual user + if (!permissionsAdapter.isAllowed(msgSender(), PermissionFlags.LIQUIDITY_ALLOWED)) { + revert Unauthorized(); + } // token is a permissioned token, wrap the token permit2.transferFrom(payer, address(permissionsAdapter), uint160(amount), permissionedToken); permissionsAdapter.wrapToPoolManager(amount); diff --git a/src/hooks/permissionedPools/PermissionedV4Router.sol b/src/hooks/permissionedPools/PermissionedV4Router.sol index fec08a4f8..95327cde7 100644 --- a/src/hooks/permissionedPools/PermissionedV4Router.sol +++ b/src/hooks/permissionedPools/PermissionedV4Router.sol @@ -95,10 +95,7 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { } /// @notice Decodes and executes the given command with the given inputs - function _dispatch(bytes1 commandType, bytes calldata inputs) - internal - returns (bool success, bytes memory output) - { + function _dispatch(bytes1 commandType, bytes calldata inputs) internal returns (bool success, bytes memory output) { uint256 command = uint8(commandType & COMMAND_TYPE_MASK); success = true; if (command == COMMAND_PERMIT2_PERMIT) { @@ -109,14 +106,15 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { } bytes calldata data = _toBytes(inputs, 6); // PermitSingle takes first 6 slots (0..5) - (success, output) = address(PERMIT2).call( - abi.encodeWithSignature( - "permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)", - msgSender(), - permitSingle, - data - ) - ); + (success, output) = address(PERMIT2) + .call( + abi.encodeWithSignature( + "permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)", + msgSender(), + permitSingle, + data + ) + ); } else if (command == COMMAND_V4_SWAP) { _executeActions(inputs); } else { @@ -139,13 +137,17 @@ contract PermissionedV4Router is V4Router, ReentrancyLock { // token is permissioned, wrap the token and transfer it to the pool manager IPermissionsAdapter permissionsAdapter = IPermissionsAdapter(Currency.unwrap(currency)); if (payer == address(this)) { - // allowlist check necessary to ensure a disallowed user cannot sell a permissioned token + // Check swap permission for the actual user if (!permissionsAdapter.isAllowed(msgSender(), PermissionFlags.SWAP_ALLOWED)) { revert Unauthorized(); } Currency.wrap(permissionedToken).transfer(address(permissionsAdapter), amount); permissionsAdapter.wrapToPoolManager(amount); } else { + // Check swap permission for the actual user + if (!permissionsAdapter.isAllowed(msgSender(), PermissionFlags.SWAP_ALLOWED)) { + revert Unauthorized(); + } // token is a permissioned token, wrap the token PERMIT2.transferFrom(payer, address(permissionsAdapter), uint160(amount), permissionedToken); permissionsAdapter.wrapToPoolManager(amount); diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 39477dfb9..5b3865e0b 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -67,6 +67,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq PoolKey public keyFake1; PoolKey public keyFake2; + PoolKey public insecureKey; + // Test Users address public alice = makeAddr("ALICE"); address public unauthorizedUser = makeAddr("UNAUTHORIZED"); @@ -134,6 +136,13 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq 3000, SQRT_PRICE_1_1 ); + (insecureKey, poolId) = initPool( + Currency.wrap(address(permissionsAdapter0)), + Currency.wrap(address(permissionsAdapter2)), + insecureHooks, + 3000, + SQRT_PRICE_1_1 + ); } function setupPermissionedComponents() internal { @@ -181,29 +190,24 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(this), PermissionFlags.ALL_ALLOWED); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( - address(secondaryPosm), PermissionFlags.ALL_ALLOWED - ); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( - address(permissionsAdapterFactory), PermissionFlags.ALL_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(currency)) + .setAllowlist(address(secondaryPosm), PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency)) + .setAllowlist(address(permissionsAdapterFactory), PermissionFlags.ALL_ALLOWED); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), PermissionFlags.ALL_ALLOWED); MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(manager), PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( - address(permissionedSwapRouter), PermissionFlags.ALL_ALLOWED - ); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist( - address(permissionedHooks), PermissionFlags.ALL_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(currency)) + .setAllowlist(address(permissionedSwapRouter), PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency2)) + .setAllowlist(address(permissionedHooks), PermissionFlags.ALL_ALLOWED); } function setUpPemissionsAdapter(PermissionsAdapter permissionsAdapter, Currency currency) internal { adapterToPermissioned[Currency.wrap(address(permissionsAdapter))] = currency; MockPermissionedToken(Currency.unwrap(currency)).mint(address(this), 1000 ether); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist( - address(permissionsAdapter), PermissionFlags.ALL_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(currency)) + .setAllowlist(address(permissionsAdapter), PermissionFlags.ALL_ALLOWED); // permissions adapter contract must have a non-zero balance of the permissioned token currency.transfer(address(permissionsAdapter), 1); @@ -223,6 +227,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq setAllowedHooks(lpm, permissionsAdapterCurrency, secondaryPermissionedHooks); setAllowedHooks(secondaryPosm, permissionsAdapterCurrency, secondaryPermissionedHooks); setAllowedHooks(tertiaryPosm, permissionsAdapterCurrency, secondaryPermissionedHooks); + + setAllowedHooks(lpm, permissionsAdapterCurrency, insecureHooks); } function setAllowedHooks(IPositionManager posm, Currency currency, IHooks permissionedHooks_) internal { @@ -884,13 +890,11 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq } // Add some tokens to unauthorized user - MockPermissionedToken(Currency.unwrap(currencyPermissioned)).setAllowlist( - unauthorizedUser, PermissionFlags.ALL_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(currencyPermissioned)) + .setAllowlist(unauthorizedUser, PermissionFlags.ALL_ALLOWED); MockPermissionedToken(Currency.unwrap(currencyPermissioned)).mint(unauthorizedUser, 1000e18); - MockPermissionedToken(Currency.unwrap(currencyPermissioned)).setAllowlist( - unauthorizedUser, PermissionFlags.NONE - ); + MockPermissionedToken(Currency.unwrap(currencyPermissioned)) + .setAllowlist(unauthorizedUser, PermissionFlags.NONE); vm.startPrank(unauthorizedUser); @@ -997,8 +1001,9 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq function _test_initialize(Currency currency0_, Currency currency1_) internal { // initialize a new pool - PoolKey memory key = - PoolKey({currency0: currency0_, currency1: currency1_, fee: 0, tickSpacing: 100, hooks: IHooks(address(0))}); + PoolKey memory key = PoolKey({ + currency0: currency0_, currency1: currency1_, fee: 0, tickSpacing: 100, hooks: IHooks(address(0)) + }); lpm.initializePool(key, SQRT_PRICE_1_1); @@ -1024,11 +1029,7 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq uint160(bound(sqrtPrice, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE_MINUS_MIN_SQRT_PRICE_MINUS_ONE)); fee = uint24(bound(fee, 0, LPFeeLibrary.MAX_LP_FEE)); PoolKey memory key = PoolKey({ - currency0: currency0_, - currency1: currency1_, - fee: fee, - tickSpacing: 10, - hooks: IHooks(address(0)) + currency0: currency0_, currency1: currency1_, fee: fee, tickSpacing: 10, hooks: IHooks(address(0)) }); lpm.initializePool(key, sqrtPrice); @@ -1308,12 +1309,10 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq uint256 liquidity = 1e18; // Test SWAP_ALLOWED + LIQUIDITY_ALLOWED (should work like ALL_ALLOWED) - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist( - alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED) - ); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist( - alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED) - ); + MockPermissionedToken(Currency.unwrap(currency0)) + .setAllowlist(alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED)); + MockPermissionedToken(Currency.unwrap(currency2)) + .setAllowlist(alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED)); uint256 tokenId = lpm.nextTokenId(); vm.prank(alice); @@ -1409,4 +1408,19 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq mint(config2, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); assertEq(IERC721(address(lpm)).ownerOf(tokenId2), alice); } + + function test_permission_flag_swap_allowed_unauthorized_reverts() public { + // Test that SWAP_ALLOWED does not allow liquidity operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + + // Should revert when trying to mint with SWAP_ALLOWED + PositionConfig memory config = PositionConfig({poolKey: insecureKey, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + + vm.startPrank(alice); + vm.expectRevert(Unauthorized.selector); + mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + vm.stopPrank(); + } } diff --git a/test/hooks/permissionedPools/PermissionedV4Router.t.sol b/test/hooks/permissionedPools/PermissionedV4Router.t.sol index 59feea5f3..473cdd842 100644 --- a/test/hooks/permissionedPools/PermissionedV4Router.t.sol +++ b/test/hooks/permissionedPools/PermissionedV4Router.t.sol @@ -453,17 +453,17 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } - function test_permission_flag_swap_allowed_succeeds() public { - // Test that SWAP_ALLOWED allows swap operations - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + function test_router_user_payment_permissioned_token_unauthorized_reverts() public { + // Test that LIQUIDITY_ALLOWED does not allow swap operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); uint256 amountIn = 100; PoolKey memory adapterKey = - PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, insecureHooks); IV4Router.ExactInputSingleParams memory params = IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); @@ -472,8 +472,10 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bytes memory data = plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); - vm.prank(alice); + vm.startPrank(alice); + vm.expectRevert(Unauthorized.selector); permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); + vm.stopPrank(); } function test_permission_flag_liquidity_allowed_swap_reverts() public { @@ -540,12 +542,10 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); // Test SWAP_ALLOWED + LIQUIDITY_ALLOWED (should work like ALL_ALLOWED) - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist( - alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED) - ); - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist( - alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED) - ); + MockPermissionedToken(Currency.unwrap(currency0)) + .setAllowlist(alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED)); + MockPermissionedToken(Currency.unwrap(currency1)) + .setAllowlist(alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED)); vm.prank(alice); permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); @@ -641,12 +641,10 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { } function test_permission_flag_all_pool_types_swap() public { - MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(permissionsAdapter0Currency))).setAllowlist( - alice, PermissionFlags.SWAP_ALLOWED - ); - MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(permissionsAdapter1Currency))).setAllowlist( - alice, PermissionFlags.SWAP_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(permissionsAdapter0Currency))) + .setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(permissionsAdapter1Currency))) + .setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); _test_permission_flag_all_pool_types_swap(key0); _test_permission_flag_all_pool_types_swap(key1); @@ -683,8 +681,9 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey memory adapterKey = PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); - IV4Router.ExactOutputSingleParams memory params = - IV4Router.ExactOutputSingleParams(adapterKey, true, uint128(amountOut), uint128(amountInMaximum), bytes("")); + IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( + adapterKey, true, uint128(amountOut), uint128(amountInMaximum), bytes("") + ); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); bytes memory data = @@ -728,6 +727,29 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); } + function test_router_user_payment_permissioned_token() public { + // Test that SWAP_ALLOWED allows swap operations + MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + + IERC20(Currency.unwrap(currency0)).transfer(alice, 2 ether); + IERC20(Currency.unwrap(currency1)).transfer(alice, 2 ether); + + uint256 amountIn = 100; + PoolKey memory adapterKey = + PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = + plan.finalizeSwap(permissionsAdapter1Currency, permissionsAdapter0Currency, ActionConstants.MSG_SENDER); + + vm.prank(alice); + permissionedRouter.execute(COMMAND_V4_SWAP, toBytesArray(data), type(uint256).max); + } + /*////////////////////////////////////////////////////////////// ERC20 -> ERC20 EXACT INPUT //////////////////////////////////////////////////////////////*/ @@ -839,8 +861,9 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, expectedAmountOut * 12 / 10)); // take the entire open delta to the router's address - plan = - plan.add(Actions.TAKE, abi.encode(key0.currency1, ActionConstants.ADDRESS_THIS, ActionConstants.OPEN_DELTA)); + plan = plan.add( + Actions.TAKE, abi.encode(key0.currency1, ActionConstants.ADDRESS_THIS, ActionConstants.OPEN_DELTA) + ); bytes memory data = plan.encode(); // the router holds no funds before assertEq(inputCurrency.balanceOf(address(permissionedRouter)), 0); @@ -1816,17 +1839,15 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { function test_router_self_payment_permissioned_token_unauthorized_reverts() public { uint256 amountIn = 100000; - MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(key0.currency0))).setAllowlist( - unauthorizedUser, PermissionFlags.ALL_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(key0.currency0))) + .setAllowlist(unauthorizedUser, PermissionFlags.ALL_ALLOWED); IERC20(Currency.unwrap(getPermissionedCurrency(key0.currency0))).transfer(address(unauthorizedUser), amountIn); vm.prank(unauthorizedUser); IERC20(Currency.unwrap(getPermissionedCurrency(key0.currency0))).transfer(address(permissionedRouter), amountIn); - MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(key0.currency0))).setAllowlist( - unauthorizedUser, PermissionFlags.NONE - ); + MockPermissionedToken(Currency.unwrap(getPermissionedCurrency(key0.currency0))) + .setAllowlist(unauthorizedUser, PermissionFlags.NONE); IV4Router.ExactInputSingleParams memory params = IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); @@ -1845,10 +1866,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { function test_command_permit2_permit() public { IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({ details: IAllowanceTransfer.PermitDetails({ - token: Currency.unwrap(currency0), - amount: 1e18, - expiration: uint48(block.timestamp + 3600), - nonce: 0 + token: Currency.unwrap(currency0), amount: 1e18, expiration: uint48(block.timestamp + 3600), nonce: 0 }), spender: address(permissionedRouter), sigDeadline: block.timestamp + 3600 @@ -1875,10 +1893,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { function test_failed_call_reverts() public { IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({ details: IAllowanceTransfer.PermitDetails({ - token: Currency.unwrap(currency0), - amount: 1e18, - expiration: uint48(block.timestamp + 3600), - nonce: 0 + token: Currency.unwrap(currency0), amount: 1e18, expiration: uint48(block.timestamp + 3600), nonce: 0 }), spender: address(permissionedRouter), sigDeadline: block.timestamp + 3600 diff --git a/test/hooks/permissionedPools/mocks/MockHooks.sol b/test/hooks/permissionedPools/mocks/MockHooks.sol index b1d95222b..1331fd9cf 100644 --- a/test/hooks/permissionedPools/mocks/MockHooks.sol +++ b/test/hooks/permissionedPools/mocks/MockHooks.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.0; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IPermissionsAdapterFactory} from - "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; +import { + IPermissionsAdapterFactory +} from "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; import {PermissionedHooks} from "../../../../src/hooks/permissionedPools/PermissionedHooks.sol"; contract MockHooks is PermissionedHooks { @@ -28,9 +30,9 @@ contract MockInsecureHooks { function beforeSwap(address, PoolKey calldata, SwapParams calldata, bytes calldata) external pure - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { - return IHooks.beforeSwap.selector; + return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } receive() external payable {} diff --git a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol index 712803a5b..66b00541e 100644 --- a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol @@ -31,8 +31,9 @@ import {Plan} from "../../../shared/Planner.sol"; import {IV4Router} from "../../../../src/interfaces/IV4Router.sol"; import {Actions} from "../../../../src/libraries/Actions.sol"; import {ActionConstants} from "../../../../src/libraries/ActionConstants.sol"; -import {IPermissionsAdapterFactory} from - "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; +import { + IPermissionsAdapterFactory +} from "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; import {PermissionedV4Router} from "../../../../src/hooks/permissionedPools/PermissionedV4Router.sol"; import {MockPermissionedToken} from "../PermissionedPoolsBase.sol"; import {MockV4Router} from "../../../mocks/MockV4Router.sol"; @@ -81,6 +82,7 @@ contract PermissionedDeployers is Test { ActionsRouter public actionsRouter; IHooks public permissionedHooks; IHooks public secondaryPermissionedHooks; + IHooks public insecureHooks; IPermissionsAdapterFactory public permissionsAdapterFactory; PoolClaimsTest public claimsRouter; @@ -147,6 +149,18 @@ contract PermissionedDeployers is Test { deployedHooksAddr = calculatedAddr; } + function deployInsecureHooks(address manager_) internal returns (address deployedHooksAddr) { + uint160 flags = (1 << 11) | (1 << 7); + (address calculatedAddr, bytes32 salt) = HookMiner.find( + address(this), flags, vm.getCode("MockHooks.sol:MockInsecureHooks"), abi.encode(IPoolManager(manager_)) + ); + address addr = Deploy.create2( + abi.encodePacked(vm.getCode("MockHooks.sol:MockInsecureHooks"), abi.encode(IPoolManager(manager_))), salt + ); + assertEq(addr, calculatedAddr); + deployedHooksAddr = calculatedAddr; + } + function deployPermissionedV4Router(address permit2_, address permissionsAdapterFactory_, address weth9) internal returns (address deployedAddr) @@ -177,6 +191,7 @@ contract PermissionedDeployers is Test { permissionsAdapterFactory = IPermissionsAdapterFactory(permissionsAdapterFactoryAddress); permissionedHooks = IHooks(deployPermissionedHooks(address(manager), permissionsAdapterFactoryAddress)); secondaryPermissionedHooks = IHooks(deployPermissionedHooks(address(manager), permissionsAdapterFactoryAddress)); + insecureHooks = IHooks(deployInsecureHooks(address(manager))); address deployedAddr = deployPermissionedV4Router(permit2_, permissionsAdapterFactoryAddress, weth9); permissionedSwapRouter = PermissionedV4Router(payable(deployedAddr)); swapRouter = PoolSwapTest(deployedAddr); diff --git a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol index 247f577e7..653e838b0 100644 --- a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol @@ -20,8 +20,9 @@ import {PermissionsAdapter} from "../../../../src/hooks/permissionedPools/Permis import {MockAllowlistChecker, MockPermissionedToken} from "../PermissionedPoolsBase.sol"; import {IAllowlistChecker} from "../../../../src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol"; import {IPositionManager} from "../../../../src/interfaces/IPositionManager.sol"; -import {IPermissionsAdapterFactory} from - "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; +import { + IPermissionsAdapterFactory +} from "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; import {IWETH9} from "../../../../src/interfaces/external/IWETH9.sol"; import {IPositionDescriptor} from "../../../../src/interfaces/IPositionDescriptor.sol"; import {IV4Router} from "../../../../src/interfaces/IV4Router.sol"; @@ -47,6 +48,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 PoolKey public key1; PoolKey public key2; PoolKey public key3; + PoolKey public insecureKey; // currency0 and currency1 are defined in Deployers.sol Currency public currency2; @@ -70,6 +72,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 _deployPermit2(); _deployPemissionsAdapterFactory(); _deployPermissionedHooks(); + _deployInsecureHook(); _deployMockPermissionedRouter(); _deployPositionManager(); MockERC20[] memory tokens = _deployTokensMintAndApprove(5, 2); @@ -124,12 +127,10 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 break; } } - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist( - address(permissionsAdapter0), PermissionFlags.ALL_ALLOWED - ); - MockPermissionedToken(Currency.unwrap(currency1)).setAllowlist( - address(permissionsAdapter1), PermissionFlags.ALL_ALLOWED - ); + MockPermissionedToken(Currency.unwrap(currency0)) + .setAllowlist(address(permissionsAdapter0), PermissionFlags.ALL_ALLOWED); + MockPermissionedToken(Currency.unwrap(currency1)) + .setAllowlist(address(permissionsAdapter1), PermissionFlags.ALL_ALLOWED); // Transfer some underlying tokens to the permissions adapter for verification IERC20(Currency.unwrap(currency0)).transfer(address(permissionsAdapter0), 1); @@ -160,6 +161,9 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 setAllowedHooks( IPositionManager(positionManager), Currency.wrap(address(permissionsAdapter1)), permissionedHooks ); + + setAllowedHooks(IPositionManager(positionManager), Currency.wrap(address(permissionsAdapter0)), insecureHooks); + setAllowedHooks(IPositionManager(positionManager), Currency.wrap(address(permissionsAdapter1)), insecureHooks); } function setAllowedHooks(IPositionManager posm, Currency currency, IHooks permissionedHooks_) internal { @@ -174,7 +178,9 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 internal returns (PoolKey memory _key) { - if (Currency.unwrap(currencyA) > Currency.unwrap(currencyB)) (currencyA, currencyB) = (currencyB, currencyA); + if (Currency.unwrap(currencyA) > Currency.unwrap(currencyB)) { + (currencyA, currencyB) = (currencyB, currencyA); + } _key = PoolKey(currencyA, currencyB, 3000, 60, IHooks(hookAddr)); manager.initialize(_key, SQRT_PRICE_1_1); MockERC20(Currency.unwrap(currencyA)).approve(positionManager, type(uint256).max); @@ -182,21 +188,14 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 modifyLiquidity(_key, ModifyLiquidityParams(-887220, 887220, 200 ether, 0), "0x", 0); } - function createNativePoolWithLiquidity(Currency currency, address hookAddr) - internal - returns (PoolKey memory _key) - { + function createNativePoolWithLiquidity(Currency currency, address hookAddr) internal returns (PoolKey memory _key) { _key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, currency, 3000, 60, IHooks(hookAddr)); manager.initialize(_key, SQRT_PRICE_1_1); MockERC20(Currency.unwrap(currency)).approve(positionManager, type(uint256).max); modifyLiquidity(_key, ModifyLiquidityParams(-887220, 887220, 200 ether, 0), "0x", 200 ether); } - function generatePermitDigest(IAllowanceTransfer.PermitSingle memory permitSingle) - internal - view - returns (bytes32) - { + function generatePermitDigest(IAllowanceTransfer.PermitSingle memory permitSingle) internal view returns (bytes32) { bytes32 domainSeparator = permit2.DOMAIN_SEPARATOR(); bytes32 PERMIT_SINGLE_TYPEHASH = keccak256( @@ -383,6 +382,10 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 permissionedHooks = IHooks(deployPermissionedHooks(address(manager), address(permissionsAdapterFactory))); } + function _deployInsecureHook() private { + insecureHooks = IHooks(deployInsecureHooks(address(manager))); + } + function _deployMockPermissionedRouter() private { bytes memory routerBytecode = abi.encodePacked( vm.getCode("PermissionedV4Router.sol:PermissionedV4Router"), @@ -432,6 +435,11 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 key2 = createPoolWithLiquidity(currency2, currency3, address(permissionedHooks)); key3 = createPoolWithLiquidity(Currency.wrap(address(permissionsAdapter0)), currency4, address(permissionedHooks)); + insecureKey = createPoolWithLiquidity( + Currency.wrap(address(permissionsAdapter0)), + Currency.wrap(address(permissionsAdapter1)), + address(insecureHooks) + ); } function _setupMockAllowList(Currency currency, address spender) private { From 129db4576c11620b354e2f293cb8d0407f469c33 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:01:12 -0400 Subject: [PATCH 24/33] Fix(PermissionedPositionManager): Adding Recipient checks to mint function (#524) * fix(permissioned-hooks): enforce permission-flags in both payer modes of PermissionedV4Router + PermissionedPositionManager * tests(permissioned-hooks): adding unit tests to validate enforcement of permission-flags in both payer modes of PermissionedV4Router + PermissionedPositionManager * update gas snapshot for PermissionedV4Router * fixing snapshots and unused variable nit * fixing snapshots one more time * fix: validate mint recipient has LIQUIDITY_ALLOWED in PermissionedPositionManager --- .../PermissionedPositionManager.sol | 10 +++ .../PermissionedPositionManager.t.sol | 70 ++++++++----------- .../shared/PermissionedRoutingTestHelpers.sol | 1 + 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index 1d2ab4734..8b3f00e0a 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -87,9 +87,19 @@ contract PermissionedPositionManager is PositionManager { ) internal override { // allowlist is verified in the hook call if (!_checkAllowedHooks(poolKey)) revert InvalidHook(); + _checkRecipientAllowed(poolKey.currency0, owner); + _checkRecipientAllowed(poolKey.currency1, owner); super._mint(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData); } + function _checkRecipientAllowed(Currency currency, address recipient) internal view { + address permissionedToken = _verifiedPermissionedTokenOf(currency); + if (permissionedToken == address(0)) return; + if (!IPermissionsAdapter(Currency.unwrap(currency)).isAllowed(recipient, PermissionFlags.LIQUIDITY_ALLOWED)) { + revert Unauthorized(); + } + } + function _checkAllowedHooks(PoolKey calldata poolKey) internal view returns (bool) { return _checkAllowedHook(poolKey.currency0, poolKey.hooks) && _checkAllowedHook(poolKey.currency1, poolKey.hooks); diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 5b3865e0b..780506fae 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -710,16 +710,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq permit2.approve(address(originalToken0), address(lpm), type(uint160).max, type(uint48).max); permit2.approve(Currency.unwrap(currency1), address(lpm), type(uint160).max, type(uint48).max); - // This should revert because the user is not in the allowlist - vm.expectRevert( - abi.encodeWithSelector( - CustomRevert.WrappedError.selector, - address(permissionedHooks), - IHooks.beforeAddLiquidity.selector, - abi.encodeWithSelector(Unauthorized.selector), - abi.encodeWithSelector(HookCallFailed.selector) - ) - ); + // This should revert because the recipient is not in the allowlist + vm.expectRevert(Unauthorized.selector); mint(config, liquidity, unauthorizedUser, ZERO_BYTES); vm.stopPrank(); } @@ -776,16 +768,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq tokenId = lpm.nextTokenId(); - // Increasing liquidity should revert because the user is no longer in the allowlist - vm.expectRevert( - abi.encodeWithSelector( - CustomRevert.WrappedError.selector, - address(permissionedHooks), - IHooks.beforeAddLiquidity.selector, - abi.encodeWithSelector(Unauthorized.selector), - abi.encodeWithSelector(HookCallFailed.selector) - ) - ); + // Increasing liquidity should revert because the recipient is no longer in the allowlist + vm.expectRevert(Unauthorized.selector); mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); vm.stopPrank(); @@ -822,16 +806,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq MockERC20(Currency.unwrap(currencyUnpermissioned)).approve(address(permit2), type(uint256).max); permit2.approve(Currency.unwrap(currencyUnpermissioned), address(lpm), type(uint160).max, type(uint48).max); - // This should revert because the user is not in the allowlist - vm.expectRevert( - abi.encodeWithSelector( - CustomRevert.WrappedError.selector, - address(permissionedHooks), - IHooks.beforeAddLiquidity.selector, - abi.encodeWithSelector(Unauthorized.selector), - abi.encodeWithSelector(HookCallFailed.selector) - ) - ); + // This should revert because the recipient is not in the allowlist + vm.expectRevert(Unauthorized.selector); mint(config, liquidity, unauthorizedUser, ZERO_BYTES); vm.stopPrank(); } @@ -902,16 +878,8 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq MockERC20(Currency.unwrap(currencyPermissioned)).approve(address(permit2), type(uint256).max); permit2.approve(Currency.unwrap(currencyPermissioned), address(lpm), type(uint160).max, type(uint48).max); - // This should revert because the user is not in the allowlist - vm.expectRevert( - abi.encodeWithSelector( - CustomRevert.WrappedError.selector, - address(permissionedHooks), - IHooks.beforeAddLiquidity.selector, - abi.encodeWithSelector(Unauthorized.selector), - abi.encodeWithSelector(HookCallFailed.selector) - ) - ); + // This should revert because the recipient is not in the allowlist + vm.expectRevert(Unauthorized.selector); mint(config, liquidity, unauthorizedUser, ZERO_BYTES); vm.stopPrank(); } @@ -1423,4 +1391,26 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); vm.stopPrank(); } + + function test_permissioned_mint_to_unauthorized_recipient_reverts() public { + // Caller (address(this)) is allowed, but unauthorizedUser is not + // The hook will pass because the caller is authorized, + // but _mint should revert because the recipient lacks LIQUIDITY_ALLOWED + PositionConfig memory config = PositionConfig({poolKey: key0, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + + vm.expectRevert(Unauthorized.selector); + mint(config, liquidity, unauthorizedUser, ZERO_BYTES); + } + + function test_permissioned_mint_to_authorized_recipient_succeeds() public { + // Caller (address(this)) is allowed, alice is also allowed + // Minting to an authorized recipient should succeed + PositionConfig memory config = PositionConfig({poolKey: key0, tickLower: -120, tickUpper: 120}); + uint256 liquidity = 1e18; + uint256 tokenId = lpm.nextTokenId(); + + mint(config, liquidity, alice, ZERO_BYTES); + assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + } } diff --git a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol index 653e838b0..293168be4 100644 --- a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol @@ -446,6 +446,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 MockPermissionedToken mockPermissionedToken = MockPermissionedToken(Currency.unwrap(currency)); mockAllowlistChecker = new MockAllowlistChecker(mockPermissionedToken); mockPermissionedToken.setAllowlist(address(this), PermissionFlags.ALL_ALLOWED); + mockPermissionedToken.setAllowlist(msg.sender, PermissionFlags.ALL_ALLOWED); mockPermissionedToken.setAllowlist(address(permissionedRouter), PermissionFlags.ALL_ALLOWED); mockPermissionedToken.setAllowlist(address(permissionedHooks), PermissionFlags.ALL_ALLOWED); mockPermissionedToken.setAllowlist(address(positionManager), PermissionFlags.ALL_ALLOWED); From f8b8f7db4bbff2e751b409e027433c9029e1b95d Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:20:11 -0400 Subject: [PATCH 25/33] Fix(PermissionedPositionManager): Overriding _mapSettlementAmount to use underlying permissionedToken balances (#525) * fix(permissioned-hooks): enforce permission-flags in both payer modes of PermissionedV4Router + PermissionedPositionManager * tests(permissioned-hooks): adding unit tests to validate enforcement of permission-flags in both payer modes of PermissionedV4Router + PermissionedPositionManager * update gas snapshot for PermissionedV4Router * fixing snapshots and unused variable nit * fixing snapshots one more time * fix: validate mint recipient has LIQUIDITY_ALLOWED in PermissionedPositionManager * fix(permissioned-position-manager): add _mapSettleAmount override for CONTRACT_BALANCE --- .../PermissionedPositionManager.sol | 10 ++++ .../PermissionedPositionManager.t.sol | 54 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/hooks/permissionedPools/PermissionedPositionManager.sol b/src/hooks/permissionedPools/PermissionedPositionManager.sol index 8b3f00e0a..49bc3187e 100644 --- a/src/hooks/permissionedPools/PermissionedPositionManager.sol +++ b/src/hooks/permissionedPools/PermissionedPositionManager.sol @@ -14,6 +14,7 @@ import {IPermissionsAdapter} from "./interfaces/IPermissionsAdapter.sol"; import {IPermissionsAdapterFactory} from "./interfaces/IPermissionsAdapterFactory.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {PermissionFlags} from "./libraries/PermissionFlags.sol"; +import {ActionConstants} from "../../libraries/ActionConstants.sol"; contract PermissionedPositionManager is PositionManager { IPermissionsAdapterFactory public immutable PERMISSIONS_ADAPTER_FACTORY; @@ -143,6 +144,15 @@ contract PermissionedPositionManager is PositionManager { return PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(currency)); } + /// @notice Calculates the amount for a settle action + function _mapSettleAmount(uint256 amount, Currency currency) internal view override returns (uint256) { + address permissionedToken = _verifiedPermissionedTokenOf(currency); + if (permissionedToken == address(0) || amount != ActionConstants.CONTRACT_BALANCE) { + return super._mapSettleAmount(amount, currency); + } + return Currency.wrap(permissionedToken).balanceOfSelf(); + } + function _getOwner(Currency currency) internal view returns (address) { address permissionsAdapter = Currency.unwrap(currency); address permissionedToken = _verifiedPermissionedTokenOf(currency); diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 780506fae..bfa05e5e5 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -1209,6 +1209,60 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq secondaryPosm.modifyLiquidities(calls, _deadline); } + function test_mint_from_contract_balance_uses_contract_balance_settle() public { + _test_mint_from_contract_balance_uses_contract_balance_settle(key0); + _test_mint_from_contract_balance_uses_contract_balance_settle(key1); + _test_mint_from_contract_balance_uses_contract_balance_settle(key2); + } + + function _test_mint_from_contract_balance_uses_contract_balance_settle(PoolKey memory key) internal { + uint256 amount0ToTransfer = 100e18; + uint256 amount1ToTransfer = 100e18; + + // Transfer underlying permissioned tokens to the POSM + setupContractBalance(key, amount0ToTransfer, amount1ToTransfer); + + BalanceInfo memory balanceInfo = getBalanceInfo(key); + + // Create a mint plan that uses CONTRACT_BALANCE instead of OPEN_DELTA + int24 tickLower = -int24(key.tickSpacing); + int24 tickUpper = int24(key.tickSpacing); + uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + amount0ToTransfer, + amount1ToTransfer + ); + + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + + Plan memory planner = Planner.init(); + planner.add( + Actions.MINT_POSITION, + abi.encode( + config.poolKey, + config.tickLower, + config.tickUpper, + liquidityToAdd, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + address(this), + ZERO_BYTES + ) + ); + + // Use CONTRACT_BALANCE instead of OPEN_DELTA — this is the broken path + planner.add(Actions.SETTLE, abi.encode(key.currency0, ActionConstants.CONTRACT_BALANCE, false)); + planner.add(Actions.SETTLE, abi.encode(key.currency1, ActionConstants.CONTRACT_BALANCE, false)); + + bytes memory calls = planner.finalizeModifyLiquidityWithClose(config.poolKey); + uint256 tokenId = lpm.nextTokenId(); + lpm.modifyLiquidities(calls, _deadline); + + verifyMintResults(key, balanceInfo, tokenId); + } + // ===== PERMISSION FLAG TESTS ===== function test_permission_flag_none() public { From 246249ef9d101776ad7bc2ac0f92fa400ef4aa7e Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Wed, 15 Apr 2026 00:37:18 -0400 Subject: [PATCH 26/33] feat(permissioned-hooks): require verified adapter in beforeInitialize --- .../permissionedPools/PermissionedHooks.sol | 12 +++ .../PermissionedPositionManager.t.sol | 19 ++--- .../PermissionedV4Router.t.sol | 78 +++++++++++++++++-- .../shared/PermissionedDeployers.sol | 2 +- .../shared/PermissionedRoutingTestHelpers.sol | 2 +- 5 files changed, 91 insertions(+), 22 deletions(-) diff --git a/src/hooks/permissionedPools/PermissionedHooks.sol b/src/hooks/permissionedPools/PermissionedHooks.sol index 0044de79b..df15b107e 100644 --- a/src/hooks/permissionedPools/PermissionedHooks.sol +++ b/src/hooks/permissionedPools/PermissionedHooks.sol @@ -18,6 +18,7 @@ contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { error Unauthorized(); error SwappingDisabled(); + error NoVerifiedAdapter(); constructor(IPoolManager manager, IPermissionsAdapterFactory permissionsAdapterFactory) BaseHook(manager) { PERMISSIONS_ADAPTER_FACTORY = permissionsAdapterFactory; @@ -26,10 +27,21 @@ contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { /// @dev Returns the hook permissions configuration for this contract function getHookPermissions() public pure override returns (Hooks.Permissions memory permissions) { + permissions.beforeInitialize = true; permissions.beforeSwap = true; permissions.beforeAddLiquidity = true; } + /// @dev Requires at least one pool currency to be a verified permissions adapter + function _beforeInitialize(address, PoolKey calldata key, uint160) internal view override returns (bytes4) { + bool currency0Verified = + PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(key.currency0)) != address(0); + bool currency1Verified = + PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(key.currency1)) != address(0); + if (!currency0Verified && !currency1Verified) revert NoVerifiedAdapter(); + return IHooks.beforeInitialize.selector; + } + /// @dev Does not need to verify msg.sender address directly, as verifying the allowlist is sufficient due to the fact that any valid senders are allowed wrappers function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata, bytes calldata) internal diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 780506fae..cb232d9f7 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -115,14 +115,6 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq SQRT_PRICE_1_1 ); - Currency currencyA = currency2; - Currency currencyB = currency0; - if (currencyA > currencyB) { - currencyA = currency0; - currencyB = currency2; - } - - (key3, poolId) = initPool(currencyA, currencyB, permissionedHooks, 3000, SQRT_PRICE_1_1); (keyFake0, poolId) = initPool( Currency.wrap(address(permissionsAdapter0)), currency1, secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1 ); @@ -269,15 +261,16 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq function test_modifyLiquidities_reverts_reentrancy() public { PoolKey memory key; - // Create a reentrant token and initialize the pool + // Create a reentrant token and initialize the pool with a verified adapter so beforeInitialize passes Currency reentrantToken = Currency.wrap(address(new ReentrantToken(lpm))); - (currency0, currency1) = (Currency.unwrap(reentrantToken) < Currency.unwrap(currency1)) - ? (reentrantToken, currency1) - : (currency1, reentrantToken); + Currency adapterCurrency = Currency.wrap(address(permissionsAdapter0)); + (Currency c0, Currency c1) = (Currency.unwrap(reentrantToken) < Currency.unwrap(adapterCurrency)) + ? (reentrantToken, adapterCurrency) + : (adapterCurrency, reentrantToken); // Set up approvals for the reentrant token approvePosmCurrency(reentrantToken); - (key, poolId) = initPool(currency0, currency1, permissionedHooks, 3000, SQRT_PRICE_1_1); + (key, poolId) = initPool(c0, c1, permissionedHooks, 3000, SQRT_PRICE_1_1); // Try to add liquidity at that range, but the token reenters posm PositionConfig memory config = diff --git a/test/hooks/permissionedPools/PermissionedV4Router.t.sol b/test/hooks/permissionedPools/PermissionedV4Router.t.sol index 473cdd842..4b6bb8828 100644 --- a/test/hooks/permissionedPools/PermissionedV4Router.t.sol +++ b/test/hooks/permissionedPools/PermissionedV4Router.t.sol @@ -15,7 +15,7 @@ import {PermissionedRoutingTestHelpers} from "./shared/PermissionedRoutingTestHe import {Planner} from "../../shared/Planner.sol"; import {Actions} from "../../../src/libraries/Actions.sol"; import {ActionConstants} from "../../../src/libraries/ActionConstants.sol"; -import {MockPermissionedToken} from "./PermissionedPoolsBase.sol"; +import {MockPermissionedToken, MockAllowlistChecker} from "./PermissionedPoolsBase.sol"; import {PermissionFlags, PermissionFlag} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; import {PathKey} from "../../../src/libraries/PathKey.sol"; @@ -27,6 +27,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { error Unauthorized(); error HookCallFailed(); error SliceOutOfBounds(); + error NoVerifiedAdapter(); error InvalidCommandType(uint256 commandType); error ExecutionFailed(uint256 commandIndex, bytes output); @@ -1031,8 +1032,17 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { tokenPath.push(currency2); tokenPath.push(currency3); - IV4Router.ExactInputParams memory params = - _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks), expectedAmountOut); + // Build path manually: first two hops use permissionedHooks, last hop uses address(0) (key2 is hookless) + PathKey[] memory path = new PathKey[](3); + path[0] = PathKey(permissionsAdapter1Currency, 3000, 60, IHooks(address(permissionedHooks)), bytes("")); + path[1] = PathKey(currency2, 3000, 60, IHooks(address(permissionedHooks)), bytes("")); + path[2] = PathKey(currency3, 3000, 60, IHooks(address(0)), bytes("")); + IV4Router.ExactInputParams memory params = IV4Router.ExactInputParams({ + currencyIn: permissionsAdapter0Currency, + path: path, + amountIn: uint128(amountIn), + amountOutMinimum: uint128(expectedAmountOut) + }); plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); @@ -1493,8 +1503,20 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { tokenPath.push(currency2); tokenPath.push(currency3); - IV4Router.ExactOutputParams memory params = - _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); + // Build path following _getExactOutputParamsWithHook pattern: + // path[2] = currency2 (hop: currency3→currency2, hookless pool) + // path[1] = adapter1 (hop: currency2→adapter1, permissioned) + // path[0] = adapter0 (hop: adapter1→adapter0, permissioned) + PathKey[] memory path = new PathKey[](3); + path[0] = PathKey(permissionsAdapter0Currency, 3000, 60, IHooks(address(permissionedHooks)), bytes("")); + path[1] = PathKey(permissionsAdapter1Currency, 3000, 60, IHooks(address(permissionedHooks)), bytes("")); + path[2] = PathKey(currency2, 3000, 60, IHooks(address(0)), bytes("")); + IV4Router.ExactOutputParams memory params = IV4Router.ExactOutputParams({ + currencyOut: currency3, + path: path, + amountOut: uint128(amountOut), + amountInMaximum: uint128(expectedAmountIn) + }); plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); @@ -1925,8 +1947,6 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.expectRevert(HookNotImplemented.selector); permissionedHooks.afterSwap(address(this), key0, swapParams, balanceDelta, bytes("")); vm.expectRevert(HookNotImplemented.selector); - permissionedHooks.beforeInitialize(address(this), key0, 0); - vm.expectRevert(HookNotImplemented.selector); permissionedHooks.afterInitialize(address(this), key0, 0, 0); vm.expectRevert(HookNotImplemented.selector); permissionedHooks.beforeRemoveLiquidity(address(this), key0, modifyLiquidityParams, bytes("")); @@ -1975,6 +1995,50 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.stopPrank(); } + /*////////////////////////////////////////////////////////////// + POOL INITIALIZATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_initialize_reverts_no_verified_adapter() public { + // Two non-permissioned tokens — neither is a verified adapter + (Currency c0, Currency c1) = currency2 < currency3 ? (currency2, currency3) : (currency3, currency2); + PoolKey memory unverifiedKey = PoolKey(c0, c1, 3000, 60, permissionedHooks); + vm.expectRevert(); + manager.initialize(unverifiedKey, SQRT_PRICE_1_1); + } + + function test_initialize_succeeds_one_verified_adapter() public { + Currency adapterCurrency = Currency.wrap(address(permissionsAdapter1)); + (Currency c0, Currency c1) = + adapterCurrency < currency3 ? (adapterCurrency, currency3) : (currency3, adapterCurrency); + PoolKey memory oneAdapterKey = PoolKey(c0, c1, 500, 10, permissionedHooks); + manager.initialize(oneAdapterKey, SQRT_PRICE_1_1); + } + + function test_initialize_succeeds_two_verified_adapters() public { + Currency a0 = Currency.wrap(address(permissionsAdapter0)); + Currency a1 = Currency.wrap(address(permissionsAdapter1)); + (Currency c0, Currency c1) = a0 < a1 ? (a0, a1) : (a1, a0); + PoolKey memory twoAdapterKey = PoolKey(c0, c1, 500, 10, permissionedHooks); + manager.initialize(twoAdapterKey, SQRT_PRICE_1_1); + } + + function test_initialize_reverts_unverified_adapter() public { + // Create an adapter for a permissioned token but do NOT verify it — the M-03 attack path + // currency0 is a MockPermissionedToken (first 2 tokens in setup are permissioned) + IERC20 token = IERC20(Currency.unwrap(currency0)); + MockAllowlistChecker checker = new MockAllowlistChecker(MockPermissionedToken(address(token))); + address unverifiedAdapter = permissionsAdapterFactory.createPermissionsAdapter(token, address(this), checker); + // Do NOT verify — skip transferring 1 wei and calling verifyPermissionsAdapter + + Currency adapterCurrency = Currency.wrap(unverifiedAdapter); + (Currency c0, Currency c1) = + adapterCurrency < currency4 ? (adapterCurrency, currency4) : (currency4, adapterCurrency); + PoolKey memory attackKey = PoolKey(c0, c1, 3000, 60, permissionedHooks); + vm.expectRevert(); + manager.initialize(attackKey, SQRT_PRICE_1_1); + } + receive() external payable {} fallback() external {} } diff --git a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol index 66b00541e..2c15cc4de 100644 --- a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol @@ -131,7 +131,7 @@ contract PermissionedDeployers is Test { internal returns (address deployedHooksAddr) { - uint160 flags = (1 << 11) | (1 << 7); + uint160 flags = (1 << 13) | (1 << 11) | (1 << 7); (address calculatedAddr, bytes32 salt) = HookMiner.find( address(this), flags, diff --git a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol index 293168be4..48502a9c5 100644 --- a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol @@ -432,7 +432,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 ); key1 = createPoolWithLiquidity(Currency.wrap(address(permissionsAdapter1)), currency2, address(permissionedHooks)); - key2 = createPoolWithLiquidity(currency2, currency3, address(permissionedHooks)); + key2 = createPoolWithLiquidity(currency2, currency3, address(0)); key3 = createPoolWithLiquidity(Currency.wrap(address(permissionsAdapter0)), currency4, address(permissionedHooks)); insecureKey = createPoolWithLiquidity( From ca507135b0855402cdb03a381bbd6d74610f86a7 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Wed, 15 Apr 2026 01:05:15 -0400 Subject: [PATCH 27/33] fix(ci): format MockPermissionedRouter --- test/mocks/MockPermissionedRouter.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/mocks/MockPermissionedRouter.sol b/test/mocks/MockPermissionedRouter.sol index 0d330102e..ca852a626 100644 --- a/test/mocks/MockPermissionedRouter.sol +++ b/test/mocks/MockPermissionedRouter.sol @@ -67,7 +67,10 @@ contract MockPermissionedRouter is PermissionedV4Router, ReentrancyLock { return _getLocker(); } - function _dispatch(bytes1 commandType, bytes calldata inputs) internal returns (bool success, bytes memory output) { + function _dispatch(bytes1 commandType, bytes calldata inputs) + internal + returns (bool success, bytes memory output) + { uint256 command = uint8(commandType & COMMAND_TYPE_MASK); success = true; if (command == COMMAND_PERMIT2_PERMIT) { From 6b19fb571de51629ae938a5f9f938deafa338057 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Wed, 15 Apr 2026 01:26:15 -0400 Subject: [PATCH 28/33] fix(ci): format MockPermissionedRouter and fix snapshot --- snapshots/BaseActionsRouterTest.json | 4 ++-- test/mocks/MockPermissionedRouter.sol | 17 +++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/snapshots/BaseActionsRouterTest.json b/snapshots/BaseActionsRouterTest.json index 8881f0954..0bb9bcfd3 100644 --- a/snapshots/BaseActionsRouterTest.json +++ b/snapshots/BaseActionsRouterTest.json @@ -1,3 +1,3 @@ { - "BaseActionsRouter_mock10commands": "36124" -} \ No newline at end of file + "BaseActionsRouter_mock10commands": "63076" +} diff --git a/test/mocks/MockPermissionedRouter.sol b/test/mocks/MockPermissionedRouter.sol index ca852a626..83fa82661 100644 --- a/test/mocks/MockPermissionedRouter.sol +++ b/test/mocks/MockPermissionedRouter.sol @@ -79,14 +79,15 @@ contract MockPermissionedRouter is PermissionedV4Router, ReentrancyLock { permitSingle := inputs.offset } bytes calldata data = _toBytes(inputs, 6); - (success, output) = address(PERMIT2).call( - abi.encodeWithSignature( - "permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)", - msgSender(), - permitSingle, - data - ) - ); + (success, output) = address(PERMIT2) + .call( + abi.encodeWithSignature( + "permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)", + msgSender(), + permitSingle, + data + ) + ); } else if (command == COMMAND_V4_SWAP) { _executeActions(inputs); } else { From c5d705b9bda4f2f5149237813b6fdcc96ca4e7f3 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Wed, 15 Apr 2026 01:35:21 -0400 Subject: [PATCH 29/33] lint: fixing lint issue in tests --- test/mocks/MockPermissionedRouter.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/mocks/MockPermissionedRouter.sol b/test/mocks/MockPermissionedRouter.sol index 83fa82661..7653dc756 100644 --- a/test/mocks/MockPermissionedRouter.sol +++ b/test/mocks/MockPermissionedRouter.sol @@ -67,10 +67,7 @@ contract MockPermissionedRouter is PermissionedV4Router, ReentrancyLock { return _getLocker(); } - function _dispatch(bytes1 commandType, bytes calldata inputs) - internal - returns (bool success, bytes memory output) - { + function _dispatch(bytes1 commandType, bytes calldata inputs) internal returns (bool success, bytes memory output) { uint256 command = uint8(commandType & COMMAND_TYPE_MASK); success = true; if (command == COMMAND_PERMIT2_PERMIT) { From ea8d4d85b3d5e036472257a7bfc92ca4e5d9c3c0 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Wed, 15 Apr 2026 02:00:13 -0400 Subject: [PATCH 30/33] fix(tests): fixing snapshots --- snapshots/BaseActionsRouterTest.json | 2 +- snapshots/PaymentsTests.json | 8 +-- snapshots/PermissionedV4RouterTest.json | 2 +- snapshots/PosMGasTest.json | 78 ++++++++++++------------- snapshots/QuoterTest.json | 26 ++++----- snapshots/StateViewTest.json | 20 +++---- snapshots/V4RouterTest.json | 46 +++++++-------- 7 files changed, 91 insertions(+), 91 deletions(-) diff --git a/snapshots/BaseActionsRouterTest.json b/snapshots/BaseActionsRouterTest.json index 0bb9bcfd3..d7241dbe2 100644 --- a/snapshots/BaseActionsRouterTest.json +++ b/snapshots/BaseActionsRouterTest.json @@ -1,3 +1,3 @@ { "BaseActionsRouter_mock10commands": "63076" -} +} \ No newline at end of file diff --git a/snapshots/PaymentsTests.json b/snapshots/PaymentsTests.json index 29c14336a..c3e810d19 100644 --- a/snapshots/PaymentsTests.json +++ b/snapshots/PaymentsTests.json @@ -1,6 +1,6 @@ { - "Payments_swap_settleFromCaller_takeAllToMsgSender": "106802", - "Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "107731", - "Payments_swap_settleWithBalance_takeAllToMsgSender": "98185", - "Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "98095" + "Payments_swap_settleFromCaller_takeAllToMsgSender": "133006", + "Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "134475", + "Payments_swap_settleWithBalance_takeAllToMsgSender": "126957", + "Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "127095" } \ No newline at end of file diff --git a/snapshots/PermissionedV4RouterTest.json b/snapshots/PermissionedV4RouterTest.json index 2f2a74c81..afc00ab10 100644 --- a/snapshots/PermissionedV4RouterTest.json +++ b/snapshots/PermissionedV4RouterTest.json @@ -1,3 +1,3 @@ { - "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "223363" + "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "231243" } \ No newline at end of file diff --git a/snapshots/PosMGasTest.json b/snapshots/PosMGasTest.json index 179915a2c..579fa6642 100644 --- a/snapshots/PosMGasTest.json +++ b/snapshots/PosMGasTest.json @@ -1,43 +1,43 @@ { - "PositionManager_burn_empty": "15837", - "PositionManager_burn_empty_native": "15837", - "PositionManager_burn_nonEmpty_native_withClose": "53574", - "PositionManager_burn_nonEmpty_native_withTakePair": "52825", - "PositionManager_burn_nonEmpty_withClose": "49866", - "PositionManager_burn_nonEmpty_withTakePair": "49117", - "PositionManager_collect_native": "78760", - "PositionManager_collect_sameRange": "75052", - "PositionManager_collect_withClose": "75052", - "PositionManager_collect_withTakePair": "74303", - "PositionManager_decreaseLiquidity_native": "46233", - "PositionManager_decreaseLiquidity_withClose": "42525", - "PositionManager_decreaseLiquidity_withTakePair": "41776", - "PositionManager_decrease_burnEmpty": "52299", - "PositionManager_decrease_burnEmpty_native": "56007", - "PositionManager_decrease_sameRange_allLiquidity": "40841", - "PositionManager_decrease_take_take": "42570", - "PositionManager_increaseLiquidity_erc20_withClose": "51172", - "PositionManager_increaseLiquidity_erc20_withSettlePair": "50387", - "PositionManager_increaseLiquidity_native": "50392", - "PositionManager_increase_autocompoundExactUnclaimedFees": "64807", - "PositionManager_increase_autocompoundExcessFeesCredit": "80601", - "PositionManager_increase_autocompound_clearExcess": "74319", - "PositionManager_mint_native": "341094", - "PositionManager_mint_nativeWithSweep_withClose": "349125", - "PositionManager_mint_nativeWithSweep_withSettlePair": "348168", - "PositionManager_mint_onSameTickLower": "258462", - "PositionManager_mint_onSameTickUpper": "263104", - "PositionManager_mint_sameRange": "163786", - "PositionManager_mint_settleWithBalance_sweep": "386472", - "PositionManager_mint_warmedPool_differentRange": "263680", - "PositionManager_mint_withClose": "394974", - "PositionManager_mint_withSettlePair": "394103", - "PositionManager_multicall_initialize_mint": "428837", - "PositionManager_permit": "51379", - "PositionManager_permit_secondPosition": "29479", - "PositionManager_permit_twice": "7579", - "PositionManager_subscribe": "56828", - "PositionManager_unsubscribe": "27693", + "PositionManager_burn_empty": "51102", + "PositionManager_burn_empty_native": "51102", + "PositionManager_burn_nonEmpty_native_withClose": "127695", + "PositionManager_burn_nonEmpty_native_withTakePair": "127137", + "PositionManager_burn_nonEmpty_withClose": "134600", + "PositionManager_burn_nonEmpty_withTakePair": "134043", + "PositionManager_collect_native": "147732", + "PositionManager_collect_sameRange": "156364", + "PositionManager_collect_withClose": "156364", + "PositionManager_collect_withTakePair": "155679", + "PositionManager_decreaseLiquidity_native": "113681", + "PositionManager_decreaseLiquidity_withClose": "121933", + "PositionManager_decreaseLiquidity_withTakePair": "121248", + "PositionManager_decrease_burnEmpty": "137500", + "PositionManager_decrease_burnEmpty_native": "130595", + "PositionManager_decrease_sameRange_allLiquidity": "134649", + "PositionManager_decrease_take_take": "122490", + "PositionManager_increaseLiquidity_erc20_withClose": "160652", + "PositionManager_increaseLiquidity_erc20_withSettlePair": "159563", + "PositionManager_increaseLiquidity_native": "143532", + "PositionManager_increase_autocompoundExactUnclaimedFees": "138231", + "PositionManager_increase_autocompoundExcessFeesCredit": "179593", + "PositionManager_increase_autocompound_clearExcess": "149511", + "PositionManager_mint_native": "367550", + "PositionManager_mint_nativeWithSweep_withClose": "376153", + "PositionManager_mint_nativeWithSweep_withSettlePair": "375348", + "PositionManager_mint_onSameTickLower": "319298", + "PositionManager_mint_onSameTickUpper": "319940", + "PositionManager_mint_sameRange": "245522", + "PositionManager_mint_settleWithBalance_sweep": "420760", + "PositionManager_mint_warmedPool_differentRange": "325316", + "PositionManager_mint_withClose": "421910", + "PositionManager_mint_withSettlePair": "420939", + "PositionManager_multicall_initialize_mint": "458237", + "PositionManager_permit": "79163", + "PositionManager_permit_secondPosition": "62075", + "PositionManager_permit_twice": "44975", + "PositionManager_subscribe": "87968", + "PositionManager_unsubscribe": "62697", "position manager initcode hash (without constructor params, as uint256)": "81827502601055975118808937107769364319765058198432540518516360048852193272922", "positionManager bytecode size": "23877" } \ No newline at end of file diff --git a/snapshots/QuoterTest.json b/snapshots/QuoterTest.json index 5df24b9ff..3ecbe3350 100644 --- a/snapshots/QuoterTest.json +++ b/snapshots/QuoterTest.json @@ -1,15 +1,15 @@ { - "Quoter_exactInputSingle_oneForZero_multiplePositions": "123658", - "Quoter_exactInputSingle_zeroForOne_multiplePositions": "129861", - "Quoter_exactOutputSingle_oneForZero": "57001", - "Quoter_exactOutputSingle_zeroForOne": "62234", - "Quoter_quoteExactInput_oneHop_1TickLoaded": "100170", - "Quoter_quoteExactInput_oneHop_initializedAfter": "124805", - "Quoter_quoteExactInput_oneHop_startingInitialized": "47592", - "Quoter_quoteExactInput_twoHops": "181404", - "Quoter_quoteExactOutput_oneHop_1TickLoaded": "99666", - "Quoter_quoteExactOutput_oneHop_2TicksLoaded": "130321", - "Quoter_quoteExactOutput_oneHop_initializedAfter": "99693", - "Quoter_quoteExactOutput_oneHop_startingInitialized": "54699", - "Quoter_quoteExactOutput_twoHops": "181132" + "Quoter_exactInputSingle_oneForZero_multiplePositions": "146134", + "Quoter_exactInputSingle_zeroForOne_multiplePositions": "152349", + "Quoter_exactOutputSingle_oneForZero": "79477", + "Quoter_exactOutputSingle_zeroForOne": "84722", + "Quoter_quoteExactInput_oneHop_1TickLoaded": "122938", + "Quoter_quoteExactInput_oneHop_initializedAfter": "147573", + "Quoter_quoteExactInput_oneHop_startingInitialized": "80848", + "Quoter_quoteExactInput_twoHops": "205152", + "Quoter_quoteExactOutput_oneHop_1TickLoaded": "122434", + "Quoter_quoteExactOutput_oneHop_2TicksLoaded": "153089", + "Quoter_quoteExactOutput_oneHop_initializedAfter": "122461", + "Quoter_quoteExactOutput_oneHop_startingInitialized": "98755", + "Quoter_quoteExactOutput_twoHops": "204880" } \ No newline at end of file diff --git a/snapshots/StateViewTest.json b/snapshots/StateViewTest.json index dc141e39f..ab141d8b1 100644 --- a/snapshots/StateViewTest.json +++ b/snapshots/StateViewTest.json @@ -1,12 +1,12 @@ { - "StateView_extsload_getFeeGrowthGlobals": "2203", - "StateView_extsload_getFeeGrowthInside": "7875", - "StateView_extsload_getLiquidity": "1439", - "StateView_extsload_getPositionInfo": "2761", - "StateView_extsload_getPositionLiquidity": "1691", - "StateView_extsload_getSlot0": "1486", - "StateView_extsload_getTickBitmap": "1432", - "StateView_extsload_getTickFeeGrowthOutside": "2490", - "StateView_extsload_getTickInfo": "2693", - "StateView_extsload_getTickLiquidity": "1686" + "StateView_extsload_getFeeGrowthGlobals": "8703", + "StateView_extsload_getFeeGrowthInside": "24375", + "StateView_extsload_getLiquidity": "5939", + "StateView_extsload_getPositionInfo": "11261", + "StateView_extsload_getPositionLiquidity": "6191", + "StateView_extsload_getSlot0": "5986", + "StateView_extsload_getTickBitmap": "5932", + "StateView_extsload_getTickFeeGrowthOutside": "8990", + "StateView_extsload_getTickInfo": "11193", + "StateView_extsload_getTickLiquidity": "6186" } \ No newline at end of file diff --git a/snapshots/V4RouterTest.json b/snapshots/V4RouterTest.json index 54b1e27e4..952f619d9 100644 --- a/snapshots/V4RouterTest.json +++ b/snapshots/V4RouterTest.json @@ -1,27 +1,27 @@ { "V4Router_Bytecode": "8975", - "V4Router_ExactIn1Hop_nativeIn": "95489", - "V4Router_ExactIn1Hop_nativeOut": "93955", - "V4Router_ExactIn1Hop_oneForZero": "102347", - "V4Router_ExactIn1Hop_zeroForOne": "108873", - "V4Router_ExactIn2Hops": "158487", - "V4Router_ExactIn2Hops_nativeIn": "151631", - "V4Router_ExactIn3Hops": "208102", - "V4Router_ExactIn3Hops_nativeIn": "201246", - "V4Router_ExactInputSingle": "107625", - "V4Router_ExactInputSingle_nativeIn": "94241", - "V4Router_ExactInputSingle_nativeOut": "92697", - "V4Router_ExactOut1Hop_nativeIn_sweepETH": "101584", - "V4Router_ExactOut1Hop_nativeOut": "94973", - "V4Router_ExactOut1Hop_oneForZero": "103365", - "V4Router_ExactOut1Hop_zeroForOne": "108022", - "V4Router_ExactOut2Hops": "158614", - "V4Router_ExactOut2Hops_nativeIn": "156833", - "V4Router_ExactOut3Hops": "209222", - "V4Router_ExactOut3Hops_nativeIn": "207441", - "V4Router_ExactOut3Hops_nativeOut": "200830", - "V4Router_ExactOutputSingle": "106769", - "V4Router_ExactOutputSingle_nativeIn_sweepETH": "100331", - "V4Router_ExactOutputSingle_nativeOut": "93745", + "V4Router_ExactIn1Hop_nativeIn": "121653", + "V4Router_ExactIn1Hop_nativeOut": "120119", + "V4Router_ExactIn1Hop_oneForZero": "128991", + "V4Router_ExactIn1Hop_zeroForOne": "135517", + "V4Router_ExactIn2Hops": "192843", + "V4Router_ExactIn2Hops_nativeIn": "178979", + "V4Router_ExactIn3Hops": "250218", + "V4Router_ExactIn3Hops_nativeIn": "236354", + "V4Router_ExactInputSingle": "134001", + "V4Router_ExactInputSingle_nativeIn": "120137", + "V4Router_ExactInputSingle_nativeOut": "118581", + "V4Router_ExactOut1Hop_nativeIn_sweepETH": "127940", + "V4Router_ExactOut1Hop_nativeOut": "121329", + "V4Router_ExactOut1Hop_oneForZero": "130201", + "V4Router_ExactOut1Hop_zeroForOne": "134858", + "V4Router_ExactOut2Hops": "191291", + "V4Router_ExactOut2Hops_nativeIn": "184373", + "V4Router_ExactOut3Hops": "247788", + "V4Router_ExactOut3Hops_nativeIn": "240870", + "V4Router_ExactOut3Hops_nativeOut": "224943", + "V4Router_ExactOutputSingle": "133337", + "V4Router_ExactOutputSingle_nativeIn_sweepETH": "126419", + "V4Router_ExactOutputSingle_nativeOut": "119821", "router initcode hash (without constructor params, as uint256)": "27545762869727400677117557485685740862616789454191614676777323590122002226479" } \ No newline at end of file From 3405d019a951cab39b01f994ec0f1e851fd5ce3f Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:03:35 -0400 Subject: [PATCH 31/33] feat(permissioned-hooks): require verified adapter in beforeInitialize (#527) --- .../permissionedPools/PermissionedHooks.sol | 12 +++ .../PermissionedPositionManager.t.sol | 19 ++--- .../PermissionedV4Router.t.sol | 78 +++++++++++++++++-- .../shared/PermissionedDeployers.sol | 2 +- .../shared/PermissionedRoutingTestHelpers.sol | 2 +- 5 files changed, 91 insertions(+), 22 deletions(-) diff --git a/src/hooks/permissionedPools/PermissionedHooks.sol b/src/hooks/permissionedPools/PermissionedHooks.sol index 0044de79b..df15b107e 100644 --- a/src/hooks/permissionedPools/PermissionedHooks.sol +++ b/src/hooks/permissionedPools/PermissionedHooks.sol @@ -18,6 +18,7 @@ contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { error Unauthorized(); error SwappingDisabled(); + error NoVerifiedAdapter(); constructor(IPoolManager manager, IPermissionsAdapterFactory permissionsAdapterFactory) BaseHook(manager) { PERMISSIONS_ADAPTER_FACTORY = permissionsAdapterFactory; @@ -26,10 +27,21 @@ contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { /// @dev Returns the hook permissions configuration for this contract function getHookPermissions() public pure override returns (Hooks.Permissions memory permissions) { + permissions.beforeInitialize = true; permissions.beforeSwap = true; permissions.beforeAddLiquidity = true; } + /// @dev Requires at least one pool currency to be a verified permissions adapter + function _beforeInitialize(address, PoolKey calldata key, uint160) internal view override returns (bytes4) { + bool currency0Verified = + PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(key.currency0)) != address(0); + bool currency1Verified = + PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(key.currency1)) != address(0); + if (!currency0Verified && !currency1Verified) revert NoVerifiedAdapter(); + return IHooks.beforeInitialize.selector; + } + /// @dev Does not need to verify msg.sender address directly, as verifying the allowlist is sufficient due to the fact that any valid senders are allowed wrappers function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata, bytes calldata) internal diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index bfa05e5e5..91b436901 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -115,14 +115,6 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq SQRT_PRICE_1_1 ); - Currency currencyA = currency2; - Currency currencyB = currency0; - if (currencyA > currencyB) { - currencyA = currency0; - currencyB = currency2; - } - - (key3, poolId) = initPool(currencyA, currencyB, permissionedHooks, 3000, SQRT_PRICE_1_1); (keyFake0, poolId) = initPool( Currency.wrap(address(permissionsAdapter0)), currency1, secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1 ); @@ -269,15 +261,16 @@ contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, Liq function test_modifyLiquidities_reverts_reentrancy() public { PoolKey memory key; - // Create a reentrant token and initialize the pool + // Create a reentrant token and initialize the pool with a verified adapter so beforeInitialize passes Currency reentrantToken = Currency.wrap(address(new ReentrantToken(lpm))); - (currency0, currency1) = (Currency.unwrap(reentrantToken) < Currency.unwrap(currency1)) - ? (reentrantToken, currency1) - : (currency1, reentrantToken); + Currency adapterCurrency = Currency.wrap(address(permissionsAdapter0)); + (Currency c0, Currency c1) = (Currency.unwrap(reentrantToken) < Currency.unwrap(adapterCurrency)) + ? (reentrantToken, adapterCurrency) + : (adapterCurrency, reentrantToken); // Set up approvals for the reentrant token approvePosmCurrency(reentrantToken); - (key, poolId) = initPool(currency0, currency1, permissionedHooks, 3000, SQRT_PRICE_1_1); + (key, poolId) = initPool(c0, c1, permissionedHooks, 3000, SQRT_PRICE_1_1); // Try to add liquidity at that range, but the token reenters posm PositionConfig memory config = diff --git a/test/hooks/permissionedPools/PermissionedV4Router.t.sol b/test/hooks/permissionedPools/PermissionedV4Router.t.sol index 473cdd842..4b6bb8828 100644 --- a/test/hooks/permissionedPools/PermissionedV4Router.t.sol +++ b/test/hooks/permissionedPools/PermissionedV4Router.t.sol @@ -15,7 +15,7 @@ import {PermissionedRoutingTestHelpers} from "./shared/PermissionedRoutingTestHe import {Planner} from "../../shared/Planner.sol"; import {Actions} from "../../../src/libraries/Actions.sol"; import {ActionConstants} from "../../../src/libraries/ActionConstants.sol"; -import {MockPermissionedToken} from "./PermissionedPoolsBase.sol"; +import {MockPermissionedToken, MockAllowlistChecker} from "./PermissionedPoolsBase.sol"; import {PermissionFlags, PermissionFlag} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; import {PathKey} from "../../../src/libraries/PathKey.sol"; @@ -27,6 +27,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { error Unauthorized(); error HookCallFailed(); error SliceOutOfBounds(); + error NoVerifiedAdapter(); error InvalidCommandType(uint256 commandType); error ExecutionFailed(uint256 commandIndex, bytes output); @@ -1031,8 +1032,17 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { tokenPath.push(currency2); tokenPath.push(currency3); - IV4Router.ExactInputParams memory params = - _getExactInputParamsWithHook(tokenPath, amountIn, address(permissionedHooks), expectedAmountOut); + // Build path manually: first two hops use permissionedHooks, last hop uses address(0) (key2 is hookless) + PathKey[] memory path = new PathKey[](3); + path[0] = PathKey(permissionsAdapter1Currency, 3000, 60, IHooks(address(permissionedHooks)), bytes("")); + path[1] = PathKey(currency2, 3000, 60, IHooks(address(permissionedHooks)), bytes("")); + path[2] = PathKey(currency3, 3000, 60, IHooks(address(0)), bytes("")); + IV4Router.ExactInputParams memory params = IV4Router.ExactInputParams({ + currencyIn: permissionsAdapter0Currency, + path: path, + amountIn: uint128(amountIn), + amountOutMinimum: uint128(expectedAmountOut) + }); plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); @@ -1493,8 +1503,20 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { tokenPath.push(currency2); tokenPath.push(currency3); - IV4Router.ExactOutputParams memory params = - _getExactOutputParamsWithHook(tokenPath, amountOut, address(permissionedHooks), expectedAmountIn); + // Build path following _getExactOutputParamsWithHook pattern: + // path[2] = currency2 (hop: currency3→currency2, hookless pool) + // path[1] = adapter1 (hop: currency2→adapter1, permissioned) + // path[0] = adapter0 (hop: adapter1→adapter0, permissioned) + PathKey[] memory path = new PathKey[](3); + path[0] = PathKey(permissionsAdapter0Currency, 3000, 60, IHooks(address(permissionedHooks)), bytes("")); + path[1] = PathKey(permissionsAdapter1Currency, 3000, 60, IHooks(address(permissionedHooks)), bytes("")); + path[2] = PathKey(currency2, 3000, 60, IHooks(address(0)), bytes("")); + IV4Router.ExactOutputParams memory params = IV4Router.ExactOutputParams({ + currencyOut: currency3, + path: path, + amountOut: uint128(amountOut), + amountInMaximum: uint128(expectedAmountIn) + }); plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); @@ -1925,8 +1947,6 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.expectRevert(HookNotImplemented.selector); permissionedHooks.afterSwap(address(this), key0, swapParams, balanceDelta, bytes("")); vm.expectRevert(HookNotImplemented.selector); - permissionedHooks.beforeInitialize(address(this), key0, 0); - vm.expectRevert(HookNotImplemented.selector); permissionedHooks.afterInitialize(address(this), key0, 0, 0); vm.expectRevert(HookNotImplemented.selector); permissionedHooks.beforeRemoveLiquidity(address(this), key0, modifyLiquidityParams, bytes("")); @@ -1975,6 +1995,50 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.stopPrank(); } + /*////////////////////////////////////////////////////////////// + POOL INITIALIZATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_initialize_reverts_no_verified_adapter() public { + // Two non-permissioned tokens — neither is a verified adapter + (Currency c0, Currency c1) = currency2 < currency3 ? (currency2, currency3) : (currency3, currency2); + PoolKey memory unverifiedKey = PoolKey(c0, c1, 3000, 60, permissionedHooks); + vm.expectRevert(); + manager.initialize(unverifiedKey, SQRT_PRICE_1_1); + } + + function test_initialize_succeeds_one_verified_adapter() public { + Currency adapterCurrency = Currency.wrap(address(permissionsAdapter1)); + (Currency c0, Currency c1) = + adapterCurrency < currency3 ? (adapterCurrency, currency3) : (currency3, adapterCurrency); + PoolKey memory oneAdapterKey = PoolKey(c0, c1, 500, 10, permissionedHooks); + manager.initialize(oneAdapterKey, SQRT_PRICE_1_1); + } + + function test_initialize_succeeds_two_verified_adapters() public { + Currency a0 = Currency.wrap(address(permissionsAdapter0)); + Currency a1 = Currency.wrap(address(permissionsAdapter1)); + (Currency c0, Currency c1) = a0 < a1 ? (a0, a1) : (a1, a0); + PoolKey memory twoAdapterKey = PoolKey(c0, c1, 500, 10, permissionedHooks); + manager.initialize(twoAdapterKey, SQRT_PRICE_1_1); + } + + function test_initialize_reverts_unverified_adapter() public { + // Create an adapter for a permissioned token but do NOT verify it — the M-03 attack path + // currency0 is a MockPermissionedToken (first 2 tokens in setup are permissioned) + IERC20 token = IERC20(Currency.unwrap(currency0)); + MockAllowlistChecker checker = new MockAllowlistChecker(MockPermissionedToken(address(token))); + address unverifiedAdapter = permissionsAdapterFactory.createPermissionsAdapter(token, address(this), checker); + // Do NOT verify — skip transferring 1 wei and calling verifyPermissionsAdapter + + Currency adapterCurrency = Currency.wrap(unverifiedAdapter); + (Currency c0, Currency c1) = + adapterCurrency < currency4 ? (adapterCurrency, currency4) : (currency4, adapterCurrency); + PoolKey memory attackKey = PoolKey(c0, c1, 3000, 60, permissionedHooks); + vm.expectRevert(); + manager.initialize(attackKey, SQRT_PRICE_1_1); + } + receive() external payable {} fallback() external {} } diff --git a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol index 66b00541e..2c15cc4de 100644 --- a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol @@ -131,7 +131,7 @@ contract PermissionedDeployers is Test { internal returns (address deployedHooksAddr) { - uint160 flags = (1 << 11) | (1 << 7); + uint160 flags = (1 << 13) | (1 << 11) | (1 << 7); (address calculatedAddr, bytes32 salt) = HookMiner.find( address(this), flags, diff --git a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol index 293168be4..48502a9c5 100644 --- a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol @@ -432,7 +432,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 ); key1 = createPoolWithLiquidity(Currency.wrap(address(permissionsAdapter1)), currency2, address(permissionedHooks)); - key2 = createPoolWithLiquidity(currency2, currency3, address(permissionedHooks)); + key2 = createPoolWithLiquidity(currency2, currency3, address(0)); key3 = createPoolWithLiquidity(Currency.wrap(address(permissionsAdapter0)), currency4, address(permissionedHooks)); insecureKey = createPoolWithLiquidity( From 15daba8ba05c26d2a3962d602758e5e8ba0d34a9 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:25:58 -0400 Subject: [PATCH 32/33] fix(migration): migrating out PermissionedPoolHooks dependency --- snapshots/BaseActionsRouterTest.json | 2 +- snapshots/PaymentsTests.json | 8 +- snapshots/PermissionedV4RouterTest.json | 2 +- snapshots/PosMGasTest.json | 78 +- snapshots/QuoterTest.json | 26 +- snapshots/StateViewTest.json | 20 +- snapshots/V4RouterTest.json | 46 +- .../permissionedPools/PermissionedHooks.sol | 92 - src/utils/HookMiner.sol | 57 + .../PermissionedPositionManager.t.sol | 2850 ++++++++--------- .../PermissionedV4Router.t.sol | 86 +- .../{MockHooks.sol => MockInsecureHooks.sol} | 11 - .../mocks/MockPermissionedHooks.sol | 132 + .../shared/PermissionedDeployers.sol | 16 +- .../shared/PermissionedRoutingTestHelpers.sol | 2 + 15 files changed, 1763 insertions(+), 1665 deletions(-) delete mode 100644 src/hooks/permissionedPools/PermissionedHooks.sol create mode 100644 src/utils/HookMiner.sol rename test/hooks/permissionedPools/mocks/{MockHooks.sol => MockInsecureHooks.sol} (66%) create mode 100644 test/hooks/permissionedPools/mocks/MockPermissionedHooks.sol diff --git a/snapshots/BaseActionsRouterTest.json b/snapshots/BaseActionsRouterTest.json index d7241dbe2..8881f0954 100644 --- a/snapshots/BaseActionsRouterTest.json +++ b/snapshots/BaseActionsRouterTest.json @@ -1,3 +1,3 @@ { - "BaseActionsRouter_mock10commands": "63076" + "BaseActionsRouter_mock10commands": "36124" } \ No newline at end of file diff --git a/snapshots/PaymentsTests.json b/snapshots/PaymentsTests.json index bebf26d74..1e2bd091c 100644 --- a/snapshots/PaymentsTests.json +++ b/snapshots/PaymentsTests.json @@ -1,6 +1,6 @@ { - "Payments_swap_settleFromCaller_takeAllToMsgSender": "133163", - "Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "134632", - "Payments_swap_settleWithBalance_takeAllToMsgSender": "127114", - "Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "127252" + "Payments_swap_settleFromCaller_takeAllToMsgSender": "106843", + "Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "107772", + "Payments_swap_settleWithBalance_takeAllToMsgSender": "98226", + "Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "98136" } \ No newline at end of file diff --git a/snapshots/PermissionedV4RouterTest.json b/snapshots/PermissionedV4RouterTest.json index afc00ab10..833895aba 100644 --- a/snapshots/PermissionedV4RouterTest.json +++ b/snapshots/PermissionedV4RouterTest.json @@ -1,3 +1,3 @@ { - "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "231243" + "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "223249" } \ No newline at end of file diff --git a/snapshots/PosMGasTest.json b/snapshots/PosMGasTest.json index 579fa6642..179915a2c 100644 --- a/snapshots/PosMGasTest.json +++ b/snapshots/PosMGasTest.json @@ -1,43 +1,43 @@ { - "PositionManager_burn_empty": "51102", - "PositionManager_burn_empty_native": "51102", - "PositionManager_burn_nonEmpty_native_withClose": "127695", - "PositionManager_burn_nonEmpty_native_withTakePair": "127137", - "PositionManager_burn_nonEmpty_withClose": "134600", - "PositionManager_burn_nonEmpty_withTakePair": "134043", - "PositionManager_collect_native": "147732", - "PositionManager_collect_sameRange": "156364", - "PositionManager_collect_withClose": "156364", - "PositionManager_collect_withTakePair": "155679", - "PositionManager_decreaseLiquidity_native": "113681", - "PositionManager_decreaseLiquidity_withClose": "121933", - "PositionManager_decreaseLiquidity_withTakePair": "121248", - "PositionManager_decrease_burnEmpty": "137500", - "PositionManager_decrease_burnEmpty_native": "130595", - "PositionManager_decrease_sameRange_allLiquidity": "134649", - "PositionManager_decrease_take_take": "122490", - "PositionManager_increaseLiquidity_erc20_withClose": "160652", - "PositionManager_increaseLiquidity_erc20_withSettlePair": "159563", - "PositionManager_increaseLiquidity_native": "143532", - "PositionManager_increase_autocompoundExactUnclaimedFees": "138231", - "PositionManager_increase_autocompoundExcessFeesCredit": "179593", - "PositionManager_increase_autocompound_clearExcess": "149511", - "PositionManager_mint_native": "367550", - "PositionManager_mint_nativeWithSweep_withClose": "376153", - "PositionManager_mint_nativeWithSweep_withSettlePair": "375348", - "PositionManager_mint_onSameTickLower": "319298", - "PositionManager_mint_onSameTickUpper": "319940", - "PositionManager_mint_sameRange": "245522", - "PositionManager_mint_settleWithBalance_sweep": "420760", - "PositionManager_mint_warmedPool_differentRange": "325316", - "PositionManager_mint_withClose": "421910", - "PositionManager_mint_withSettlePair": "420939", - "PositionManager_multicall_initialize_mint": "458237", - "PositionManager_permit": "79163", - "PositionManager_permit_secondPosition": "62075", - "PositionManager_permit_twice": "44975", - "PositionManager_subscribe": "87968", - "PositionManager_unsubscribe": "62697", + "PositionManager_burn_empty": "15837", + "PositionManager_burn_empty_native": "15837", + "PositionManager_burn_nonEmpty_native_withClose": "53574", + "PositionManager_burn_nonEmpty_native_withTakePair": "52825", + "PositionManager_burn_nonEmpty_withClose": "49866", + "PositionManager_burn_nonEmpty_withTakePair": "49117", + "PositionManager_collect_native": "78760", + "PositionManager_collect_sameRange": "75052", + "PositionManager_collect_withClose": "75052", + "PositionManager_collect_withTakePair": "74303", + "PositionManager_decreaseLiquidity_native": "46233", + "PositionManager_decreaseLiquidity_withClose": "42525", + "PositionManager_decreaseLiquidity_withTakePair": "41776", + "PositionManager_decrease_burnEmpty": "52299", + "PositionManager_decrease_burnEmpty_native": "56007", + "PositionManager_decrease_sameRange_allLiquidity": "40841", + "PositionManager_decrease_take_take": "42570", + "PositionManager_increaseLiquidity_erc20_withClose": "51172", + "PositionManager_increaseLiquidity_erc20_withSettlePair": "50387", + "PositionManager_increaseLiquidity_native": "50392", + "PositionManager_increase_autocompoundExactUnclaimedFees": "64807", + "PositionManager_increase_autocompoundExcessFeesCredit": "80601", + "PositionManager_increase_autocompound_clearExcess": "74319", + "PositionManager_mint_native": "341094", + "PositionManager_mint_nativeWithSweep_withClose": "349125", + "PositionManager_mint_nativeWithSweep_withSettlePair": "348168", + "PositionManager_mint_onSameTickLower": "258462", + "PositionManager_mint_onSameTickUpper": "263104", + "PositionManager_mint_sameRange": "163786", + "PositionManager_mint_settleWithBalance_sweep": "386472", + "PositionManager_mint_warmedPool_differentRange": "263680", + "PositionManager_mint_withClose": "394974", + "PositionManager_mint_withSettlePair": "394103", + "PositionManager_multicall_initialize_mint": "428837", + "PositionManager_permit": "51379", + "PositionManager_permit_secondPosition": "29479", + "PositionManager_permit_twice": "7579", + "PositionManager_subscribe": "56828", + "PositionManager_unsubscribe": "27693", "position manager initcode hash (without constructor params, as uint256)": "81827502601055975118808937107769364319765058198432540518516360048852193272922", "positionManager bytecode size": "23877" } \ No newline at end of file diff --git a/snapshots/QuoterTest.json b/snapshots/QuoterTest.json index 3ecbe3350..5df24b9ff 100644 --- a/snapshots/QuoterTest.json +++ b/snapshots/QuoterTest.json @@ -1,15 +1,15 @@ { - "Quoter_exactInputSingle_oneForZero_multiplePositions": "146134", - "Quoter_exactInputSingle_zeroForOne_multiplePositions": "152349", - "Quoter_exactOutputSingle_oneForZero": "79477", - "Quoter_exactOutputSingle_zeroForOne": "84722", - "Quoter_quoteExactInput_oneHop_1TickLoaded": "122938", - "Quoter_quoteExactInput_oneHop_initializedAfter": "147573", - "Quoter_quoteExactInput_oneHop_startingInitialized": "80848", - "Quoter_quoteExactInput_twoHops": "205152", - "Quoter_quoteExactOutput_oneHop_1TickLoaded": "122434", - "Quoter_quoteExactOutput_oneHop_2TicksLoaded": "153089", - "Quoter_quoteExactOutput_oneHop_initializedAfter": "122461", - "Quoter_quoteExactOutput_oneHop_startingInitialized": "98755", - "Quoter_quoteExactOutput_twoHops": "204880" + "Quoter_exactInputSingle_oneForZero_multiplePositions": "123658", + "Quoter_exactInputSingle_zeroForOne_multiplePositions": "129861", + "Quoter_exactOutputSingle_oneForZero": "57001", + "Quoter_exactOutputSingle_zeroForOne": "62234", + "Quoter_quoteExactInput_oneHop_1TickLoaded": "100170", + "Quoter_quoteExactInput_oneHop_initializedAfter": "124805", + "Quoter_quoteExactInput_oneHop_startingInitialized": "47592", + "Quoter_quoteExactInput_twoHops": "181404", + "Quoter_quoteExactOutput_oneHop_1TickLoaded": "99666", + "Quoter_quoteExactOutput_oneHop_2TicksLoaded": "130321", + "Quoter_quoteExactOutput_oneHop_initializedAfter": "99693", + "Quoter_quoteExactOutput_oneHop_startingInitialized": "54699", + "Quoter_quoteExactOutput_twoHops": "181132" } \ No newline at end of file diff --git a/snapshots/StateViewTest.json b/snapshots/StateViewTest.json index ab141d8b1..dc141e39f 100644 --- a/snapshots/StateViewTest.json +++ b/snapshots/StateViewTest.json @@ -1,12 +1,12 @@ { - "StateView_extsload_getFeeGrowthGlobals": "8703", - "StateView_extsload_getFeeGrowthInside": "24375", - "StateView_extsload_getLiquidity": "5939", - "StateView_extsload_getPositionInfo": "11261", - "StateView_extsload_getPositionLiquidity": "6191", - "StateView_extsload_getSlot0": "5986", - "StateView_extsload_getTickBitmap": "5932", - "StateView_extsload_getTickFeeGrowthOutside": "8990", - "StateView_extsload_getTickInfo": "11193", - "StateView_extsload_getTickLiquidity": "6186" + "StateView_extsload_getFeeGrowthGlobals": "2203", + "StateView_extsload_getFeeGrowthInside": "7875", + "StateView_extsload_getLiquidity": "1439", + "StateView_extsload_getPositionInfo": "2761", + "StateView_extsload_getPositionLiquidity": "1691", + "StateView_extsload_getSlot0": "1486", + "StateView_extsload_getTickBitmap": "1432", + "StateView_extsload_getTickFeeGrowthOutside": "2490", + "StateView_extsload_getTickInfo": "2693", + "StateView_extsload_getTickLiquidity": "1686" } \ No newline at end of file diff --git a/snapshots/V4RouterTest.json b/snapshots/V4RouterTest.json index dcf6ef685..9af030053 100644 --- a/snapshots/V4RouterTest.json +++ b/snapshots/V4RouterTest.json @@ -1,27 +1,27 @@ { "V4Router_Bytecode": "9950", - "V4Router_ExactIn1Hop_nativeIn": "122222", - "V4Router_ExactIn1Hop_nativeOut": "120688", - "V4Router_ExactIn1Hop_oneForZero": "129560", - "V4Router_ExactIn1Hop_zeroForOne": "136086", - "V4Router_ExactIn2Hops": "193444", - "V4Router_ExactIn2Hops_nativeIn": "179580", - "V4Router_ExactIn3Hops": "250829", - "V4Router_ExactIn3Hops_nativeIn": "236965", - "V4Router_ExactInputSingle": "134158", - "V4Router_ExactInputSingle_nativeIn": "120294", - "V4Router_ExactInputSingle_nativeOut": "118738", - "V4Router_ExactOut1Hop_nativeIn_sweepETH": "128509", - "V4Router_ExactOut1Hop_nativeOut": "121898", - "V4Router_ExactOut1Hop_oneForZero": "130770", - "V4Router_ExactOut1Hop_zeroForOne": "135427", - "V4Router_ExactOut2Hops": "191892", - "V4Router_ExactOut2Hops_nativeIn": "184974", - "V4Router_ExactOut3Hops": "248399", - "V4Router_ExactOut3Hops_nativeIn": "241481", - "V4Router_ExactOut3Hops_nativeOut": "225554", - "V4Router_ExactOutputSingle": "133494", - "V4Router_ExactOutputSingle_nativeIn_sweepETH": "126576", - "V4Router_ExactOutputSingle_nativeOut": "119978", + "V4Router_ExactIn1Hop_nativeIn": "95778", + "V4Router_ExactIn1Hop_nativeOut": "94244", + "V4Router_ExactIn1Hop_oneForZero": "102636", + "V4Router_ExactIn1Hop_zeroForOne": "109162", + "V4Router_ExactIn2Hops": "158796", + "V4Router_ExactIn2Hops_nativeIn": "151940", + "V4Router_ExactIn3Hops": "208433", + "V4Router_ExactIn3Hops_nativeIn": "201577", + "V4Router_ExactInputSingle": "107666", + "V4Router_ExactInputSingle_nativeIn": "94282", + "V4Router_ExactInputSingle_nativeOut": "92738", + "V4Router_ExactOut1Hop_nativeIn_sweepETH": "101873", + "V4Router_ExactOut1Hop_nativeOut": "95262", + "V4Router_ExactOut1Hop_oneForZero": "103654", + "V4Router_ExactOut1Hop_zeroForOne": "108311", + "V4Router_ExactOut2Hops": "158923", + "V4Router_ExactOut2Hops_nativeIn": "157142", + "V4Router_ExactOut3Hops": "209553", + "V4Router_ExactOut3Hops_nativeIn": "207772", + "V4Router_ExactOut3Hops_nativeOut": "201161", + "V4Router_ExactOutputSingle": "106810", + "V4Router_ExactOutputSingle_nativeIn_sweepETH": "100372", + "V4Router_ExactOutputSingle_nativeOut": "93786", "router initcode hash (without constructor params, as uint256)": "19267777467363212309328806064985146558623592810537866695556902731584515865783" } \ No newline at end of file diff --git a/src/hooks/permissionedPools/PermissionedHooks.sol b/src/hooks/permissionedPools/PermissionedHooks.sol deleted file mode 100644 index df15b107e..000000000 --- a/src/hooks/permissionedPools/PermissionedHooks.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {IPoolManager, Currency} from "../../V4Router.sol"; -import {IPermissionsAdapter} from "./interfaces/IPermissionsAdapter.sol"; -import {IPermissionsAdapterFactory} from "./interfaces/IPermissionsAdapterFactory.sol"; -import {IMsgSender} from "../../interfaces/IMsgSender.sol"; -import {ReentrancyLock} from "../../base/ReentrancyLock.sol"; -import {Hooks, IHooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; -import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; -import {BaseHook} from "../../utils/BaseHook.sol"; -import {PermissionFlags, PermissionFlag} from "./libraries/PermissionFlags.sol"; - -contract PermissionedHooks is IHooks, ReentrancyLock, BaseHook { - IPermissionsAdapterFactory public immutable PERMISSIONS_ADAPTER_FACTORY; - - error Unauthorized(); - error SwappingDisabled(); - error NoVerifiedAdapter(); - - constructor(IPoolManager manager, IPermissionsAdapterFactory permissionsAdapterFactory) BaseHook(manager) { - PERMISSIONS_ADAPTER_FACTORY = permissionsAdapterFactory; - Hooks.validateHookPermissions(this, getHookPermissions()); - } - - /// @dev Returns the hook permissions configuration for this contract - function getHookPermissions() public pure override returns (Hooks.Permissions memory permissions) { - permissions.beforeInitialize = true; - permissions.beforeSwap = true; - permissions.beforeAddLiquidity = true; - } - - /// @dev Requires at least one pool currency to be a verified permissions adapter - function _beforeInitialize(address, PoolKey calldata key, uint160) internal view override returns (bytes4) { - bool currency0Verified = - PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(key.currency0)) != address(0); - bool currency1Verified = - PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(key.currency1)) != address(0); - if (!currency0Verified && !currency1Verified) revert NoVerifiedAdapter(); - return IHooks.beforeInitialize.selector; - } - - /// @dev Does not need to verify msg.sender address directly, as verifying the allowlist is sufficient due to the fact that any valid senders are allowed wrappers - function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata, bytes calldata) - internal - view - override - returns (bytes4 selector, BeforeSwapDelta, uint24) - { - selector = IHooks.beforeSwap.selector; - _verifyAllowlist(IMsgSender(sender), key, selector); - return (selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); - } - - /// @dev Does not need to verify msg.sender address directly, as verifying the allowlist is sufficient due to the fact that any valid senders are allowed wrappers - function _beforeAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata) - internal - view - override - returns (bytes4 selector) - { - selector = IHooks.beforeAddLiquidity.selector; - _verifyAllowlist(IMsgSender(sender), key, selector); - } - - /// @dev checks if the sender is allowed to access both tokens in the pool - function _verifyAllowlist(IMsgSender sender, PoolKey calldata poolKey, bytes4 selector) internal view { - _isAllowed(Currency.unwrap(poolKey.currency0), sender.msgSender(), address(sender), selector); - _isAllowed(Currency.unwrap(poolKey.currency1), sender.msgSender(), address(sender), selector); - } - - /// @dev checks if the provided token is a permissioned token by checking if it has a verified permissions adapter, if yes, check the allowlist and check whether swapping is enabled - function _isAllowed(address permissionsAdapter, address sender, address router, bytes4 selector) internal view { - address permissionedToken = PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(permissionsAdapter); - if (permissionedToken == address(0)) return; - - PermissionFlag permission = PermissionFlags.NONE; - if (selector == this.beforeSwap.selector) { - permission = PermissionFlags.SWAP_ALLOWED; - if (!IPermissionsAdapter(permissionsAdapter).swappingEnabled()) revert SwappingDisabled(); - } else if (selector == this.beforeAddLiquidity.selector) { - permission = PermissionFlags.LIQUIDITY_ALLOWED; - } - - if ( - !IPermissionsAdapter(permissionsAdapter).isAllowed(sender, permission) - || !IPermissionsAdapter(permissionsAdapter).allowedWrappers(router) - ) revert Unauthorized(); - } -} diff --git a/src/utils/HookMiner.sol b/src/utils/HookMiner.sol new file mode 100644 index 000000000..3c1f487ec --- /dev/null +++ b/src/utils/HookMiner.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; + +/// @title HookMiner +/// @notice a minimal library for mining hook addresses +library HookMiner { + // mask to slice out the bottom 14 bit of the address + uint160 constant FLAG_MASK = Hooks.ALL_HOOK_MASK; // 0000 ... 0000 0011 1111 1111 1111 + + // Maximum number of iterations to find a salt, avoid infinite loops or MemoryOOG + // (arbitrarily set) + uint256 constant MAX_LOOP = 160_444; + + /// @notice Find a salt that produces a hook address with the desired `flags` + /// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy) + /// @param flags The desired flags for the hook address. Example `uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | ...)` + /// @param creationCode The creation code of a hook contract. Example: `type(Counter).creationCode` + /// @param constructorArgs The encoded constructor arguments of a hook contract. Example: `abi.encode(address(manager))` + /// @return (hookAddress, salt) The hook deploys to `hookAddress` when using `salt` with the syntax: `new Hook{salt: salt}()` + function find(address deployer, uint160 flags, bytes memory creationCode, bytes memory constructorArgs) + internal + view + returns (address, bytes32) + { + flags = flags & FLAG_MASK; // mask for only the bottom 14 bits + bytes memory creationCodeWithArgs = abi.encodePacked(creationCode, constructorArgs); + + address hookAddress; + for (uint256 salt; salt < MAX_LOOP; salt++) { + hookAddress = computeAddress(deployer, salt, creationCodeWithArgs); + + // if the hook's bottom 14 bits match the desired flags AND the address does not have bytecode, we found a match + if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) { + return (hookAddress, bytes32(salt)); + } + } + revert("HookMiner: could not find salt"); + } + + /// @notice Precompute a contract address deployed via CREATE2 + /// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy) + /// @param salt The salt used to deploy the hook + /// @param creationCodeWithArgs The creation code of a hook contract, with encoded constructor arguments appended. Example: `abi.encodePacked(type(Counter).creationCode, abi.encode(constructorArg1, constructorArg2))` + function computeAddress(address deployer, uint256 salt, bytes memory creationCodeWithArgs) + internal + pure + returns (address hookAddress) + { + return address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xFF), deployer, salt, keccak256(creationCodeWithArgs))))) + ); + } +} diff --git a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol index 91b436901..c4f676c9d 100644 --- a/test/hooks/permissionedPools/PermissionedPositionManager.t.sol +++ b/test/hooks/permissionedPools/PermissionedPositionManager.t.sol @@ -1,1463 +1,1463 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import "forge-std/Test.sol"; -import {IERC721} from "forge-std/interfaces/IERC721.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; -import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; -import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; -import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; - -import {IPositionManager} from "../../../src/interfaces/IPositionManager.sol"; -import {Actions} from "../../../src/libraries/Actions.sol"; -import {DeltaResolver} from "../../../src/base/DeltaResolver.sol"; -import {PositionConfig} from "test/shared/PositionConfig.sol"; -import {SlippageCheck} from "../../../src/libraries/SlippageCheck.sol"; -import {BaseActionsRouter} from "../../../src/base/BaseActionsRouter.sol"; -import {ActionConstants} from "../../../src/libraries/ActionConstants.sol"; -import {LiquidityFuzzers} from "../../shared/fuzz/LiquidityFuzzers.sol"; -import {Planner, Plan} from "../../shared/Planner.sol"; -import {PermissionedPosmTestSetup, BalanceInfo} from "./shared/PermissionedPosmTestSetup.sol"; -import {ReentrantToken} from "../../mocks/ReentrantToken.sol"; -import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; -import {MockAllowlistChecker, MockPermissionedToken} from "./PermissionedPoolsBase.sol"; -import {PermissionsAdapter, IERC20} from "../../../src/hooks/permissionedPools/PermissionsAdapter.sol"; -import {PermissionFlags, PermissionFlag} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; - -contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, LiquidityFuzzers { - using FixedPointMathLib for uint256; - using StateLibrary for IPoolManager; - - // To allow testing without importing PermissionedHooks - error Unauthorized(); - error InvalidHook(); - error HookCallFailed(); - - PoolId public poolId; - PoolId public poolIdFake; - - // Permissioned components - MockAllowlistChecker public mockAllowListChecker; - PermissionsAdapter public permissionsAdapter0; - PermissionsAdapter public permissionsAdapter2; - MockERC20 public originalToken0; - MockERC20 public originalToken2; - IPositionManager public secondaryPosm; - IPositionManager public tertiaryPosm; - - // permissioned / normal - PoolKey public key0; - // normal / permissioned - PoolKey public key1; - // permissioned / permissioned - PoolKey public key2; - // normal / normal - PoolKey public key3; - - PoolKey public keyFake0; - PoolKey public keyFake1; - PoolKey public keyFake2; - - PoolKey public insecureKey; - - // Test Users - address public alice = makeAddr("ALICE"); - address public unauthorizedUser = makeAddr("UNAUTHORIZED"); - - function setUp() public { - permit2 = IAllowanceTransfer(deployPermit2()); - - deployFreshManagerAndRoutersPermissioned(address(permit2), address(_WETH9)); - (currency0, currency1) = deployMintAndApprove2Currencies(true, false); - currency2 = deployMintAndApproveCurrency(true); - - setUpPosms(); - setupPermissionedComponents(); - setupPool(); - - // set up approvals for alice - seedBalance(alice); - approvePosmFor(alice); - permissionsAdapter0.updateSwappingEnabled(true); - permissionsAdapter2.updateSwappingEnabled(true); - } - - function setUpPosms() internal { - deployAndApprovePosm(manager, address(permissionsAdapterFactory), keccak256("permissionedPosm")); - - // alternate position manager using different hooks - secondaryPosm = - deployAndApprovePosmOnly(manager, address(permissionsAdapterFactory), keccak256("secondaryPosm")); - - // alternate position manager using the same hooks - tertiaryPosm = deployAndApprovePosmOnly(manager, address(permissionsAdapterFactory), keccak256("tertiaryPosm")); - } - - function setupPool() internal { - (key0, poolId) = - initPool(Currency.wrap(address(permissionsAdapter0)), currency1, permissionedHooks, 3000, SQRT_PRICE_1_1); - (key1, poolId) = - initPool(currency1, Currency.wrap(address(permissionsAdapter2)), permissionedHooks, 3000, SQRT_PRICE_1_1); - (key2, poolId) = initPool( - Currency.wrap(address(permissionsAdapter0)), - Currency.wrap(address(permissionsAdapter2)), - permissionedHooks, - 3000, - SQRT_PRICE_1_1 - ); - - (keyFake0, poolId) = initPool( - Currency.wrap(address(permissionsAdapter0)), currency1, secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1 - ); - (keyFake1, poolId) = initPool( - currency1, Currency.wrap(address(permissionsAdapter2)), secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1 - ); - (keyFake2, poolId) = initPool( - Currency.wrap(address(permissionsAdapter0)), - Currency.wrap(address(permissionsAdapter2)), - secondaryPermissionedHooks, - 3000, - SQRT_PRICE_1_1 - ); - (insecureKey, poolId) = initPool( - Currency.wrap(address(permissionsAdapter0)), - Currency.wrap(address(permissionsAdapter2)), - insecureHooks, - 3000, - SQRT_PRICE_1_1 - ); - } - - function setupPermissionedComponents() internal { - mockAllowListChecker = new MockAllowlistChecker(MockPermissionedToken(Currency.unwrap(currency0))); - setUpCurrencyZero(); - setUpCurrencyTwo(); - } - - function setUpCurrencyZero() internal { - setUpAllowlist(currency0); - originalToken0 = MockERC20(Currency.unwrap(currency0)); - // ensure expected ordering - while (true) { - permissionsAdapter0 = PermissionsAdapter( - permissionsAdapterFactory.createPermissionsAdapter( - IERC20(address(originalToken0)), address(this), mockAllowListChecker - ) - ); - if (address(permissionsAdapter0) < Currency.unwrap(currency1)) { - break; - } - } - setUpPemissionsAdapter(permissionsAdapter0, currency0); - } - - function setUpCurrencyTwo() internal { - setUpAllowlist(currency2); - originalToken2 = MockERC20(Currency.unwrap(currency2)); - - // ensure expected ordering - while (true) { - permissionsAdapter2 = PermissionsAdapter( - permissionsAdapterFactory.createPermissionsAdapter( - IERC20(address(originalToken2)), address(this), mockAllowListChecker - ) - ); - if (Currency.unwrap(currency1) < address(permissionsAdapter2)) { - break; - } - } - setUpPemissionsAdapter(permissionsAdapter2, currency2); - } - - function setUpAllowlist(Currency currency) internal { - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(this), PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency)) - .setAllowlist(address(secondaryPosm), PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency)) - .setAllowlist(address(permissionsAdapterFactory), PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(manager), PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency)) - .setAllowlist(address(permissionedSwapRouter), PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency2)) - .setAllowlist(address(permissionedHooks), PermissionFlags.ALL_ALLOWED); - } - - function setUpPemissionsAdapter(PermissionsAdapter permissionsAdapter, Currency currency) internal { - adapterToPermissioned[Currency.wrap(address(permissionsAdapter))] = currency; - - MockPermissionedToken(Currency.unwrap(currency)).mint(address(this), 1000 ether); - MockPermissionedToken(Currency.unwrap(currency)) - .setAllowlist(address(permissionsAdapter), PermissionFlags.ALL_ALLOWED); - - // permissions adapter contract must have a non-zero balance of the permissioned token - currency.transfer(address(permissionsAdapter), 1); - - permissionsAdapter.updateAllowedWrapper(address(manager), true); - permissionsAdapter.updateAllowedWrapper(address(lpm), true); - permissionsAdapter.updateAllowedWrapper(address(secondaryPosm), true); - permissionsAdapter.updateAllowedWrapper(address(permissionedSwapRouter), true); - - permissionsAdapterFactory.verifyPermissionsAdapter(address(permissionsAdapter)); - - Currency permissionsAdapterCurrency = Currency.wrap(address(permissionsAdapter)); - - setAllowedHooks(lpm, permissionsAdapterCurrency, permissionedHooks); - setAllowedHooks(tertiaryPosm, permissionsAdapterCurrency, permissionedHooks); - - setAllowedHooks(lpm, permissionsAdapterCurrency, secondaryPermissionedHooks); - setAllowedHooks(secondaryPosm, permissionsAdapterCurrency, secondaryPermissionedHooks); - setAllowedHooks(tertiaryPosm, permissionsAdapterCurrency, secondaryPermissionedHooks); - - setAllowedHooks(lpm, permissionsAdapterCurrency, insecureHooks); - } - - function setAllowedHooks(IPositionManager posm, Currency currency, IHooks permissionedHooks_) internal { - // setAllowedHook selector - bytes4 selector = 0xb5cdc484; - bytes memory data = abi.encodeWithSelector(selector, currency, permissionedHooks_, true); - (bool success,) = address(posm).call(data); - require(success, "Failed to set hooks"); - } - - function test_modifyLiquidities_reverts_deadlinePassed() public { - _test_modifyLiquidities_reverts_deadlinePassed(key0); - _test_modifyLiquidities_reverts_deadlinePassed(key1); - _test_modifyLiquidities_reverts_deadlinePassed(key2); - } - - function _test_modifyLiquidities_reverts_deadlinePassed(PoolKey memory key) internal { - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: 0, tickUpper: 60}); - bytes memory calls = getMintEncoded(config, 1e18, ActionConstants.MSG_SENDER, ""); - - uint256 deadline = vm.getBlockTimestamp() - 1; - - vm.expectRevert(abi.encodeWithSelector(IPositionManager.DeadlinePassed.selector, deadline)); - lpm.modifyLiquidities(calls, deadline); - } - - function test_modifyLiquidities_reverts_mismatchedLengths() public { - Plan memory planner = Planner.init(); - planner.add(Actions.MINT_POSITION, abi.encode("test")); - planner.add(Actions.BURN_POSITION, abi.encode("test")); - - bytes[] memory badParams = new bytes[](1); - - vm.expectRevert(BaseActionsRouter.InputLengthMismatch.selector); - lpm.modifyLiquidities(abi.encode(planner.actions, badParams), block.timestamp + 1); - } - - function test_modifyLiquidities_reverts_reentrancy() public { - PoolKey memory key; - - // Create a reentrant token and initialize the pool with a verified adapter so beforeInitialize passes - Currency reentrantToken = Currency.wrap(address(new ReentrantToken(lpm))); - Currency adapterCurrency = Currency.wrap(address(permissionsAdapter0)); - (Currency c0, Currency c1) = (Currency.unwrap(reentrantToken) < Currency.unwrap(adapterCurrency)) - ? (reentrantToken, adapterCurrency) - : (adapterCurrency, reentrantToken); - - // Set up approvals for the reentrant token - approvePosmCurrency(reentrantToken); - (key, poolId) = initPool(c0, c1, permissionedHooks, 3000, SQRT_PRICE_1_1); - - // Try to add liquidity at that range, but the token reenters posm - PositionConfig memory config = - PositionConfig({poolKey: key, tickLower: -int24(key.tickSpacing), tickUpper: int24(key.tickSpacing)}); - - bytes memory calls = getMintEncoded(config, 1e18, ActionConstants.MSG_SENDER, ""); - - // Permit2.transferFrom does not bubble the ContractLocked error and instead reverts with its own error - vm.expectRevert("TRANSFER_FROM_FAILED"); - lpm.modifyLiquidities(calls, block.timestamp + 1); - } - - function test_fuzz_mint_withLiquidityDelta(ModifyLiquidityParams memory params, uint160 sqrtPriceX96) public { - bound(sqrtPriceX96, MIN_PRICE_LIMIT, MAX_PRICE_LIMIT); - - _test_fuzz_mint_withLiquidityDelta(key0, params, sqrtPriceX96); - _test_fuzz_mint_withLiquidityDelta(key1, params, sqrtPriceX96); - _test_fuzz_mint_withLiquidityDelta(key2, params, sqrtPriceX96); - } - - function _test_fuzz_mint_withLiquidityDelta( - PoolKey memory key, - ModifyLiquidityParams memory params, - uint160 sqrtPriceX96 - ) internal { - params = createFuzzyLiquidityParams(key, params, sqrtPriceX96); - PositionConfig memory config = - PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - - // liquidity is a uint - uint256 liquidityToAdd = - params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); - - uint256 balance0Before = getPermissionedCurrency(key.currency0).balanceOfSelf(); - uint256 balance1Before = getPermissionedCurrency(key.currency1).balanceOfSelf(); - uint256 balance0ManagerBefore = key.currency0.balanceOf(address(manager)); - uint256 balance1ManagerBefore = key.currency1.balanceOf(address(manager)); - uint256 tokenId = lpm.nextTokenId(); - - mint(config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES); - - uint256 balance0ManagerAfter = key.currency0.balanceOf(address(manager)); - uint256 balance1ManagerAfter = key.currency1.balanceOf(address(manager)); - uint256 liquidity = lpm.getPositionLiquidity(tokenId); - - assertEq(tokenId, lpm.nextTokenId() - 1); - assertEq(IERC721(address(lpm)).ownerOf(tokenId), address(this)); - assertEq(liquidity, uint256(params.liquidityDelta)); - assertEq( - balance0Before - getPermissionedCurrency(key.currency0).balanceOfSelf(), - balance0ManagerAfter - balance0ManagerBefore, - "incorrect amount0" - ); - assertEq( - balance1Before - getPermissionedCurrency(key.currency1).balanceOfSelf(), - balance1ManagerAfter - balance1ManagerBefore, - "incorrect amount1" - ); - } - - function test_mint_exactTokenRatios() public { - _test_mint_exactTokenRatios(key0); - _test_mint_exactTokenRatios(key1); - _test_mint_exactTokenRatios(key2); - } - - function _test_mint_exactTokenRatios(PoolKey memory key) internal { - int24 tickLower = -int24(key.tickSpacing); - int24 tickUpper = int24(key.tickSpacing); - uint256 amount0Desired = 100e18; - uint256 amount1Desired = 100e18; - uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( - SQRT_PRICE_1_1, - TickMath.getSqrtPriceAtTick(tickLower), - TickMath.getSqrtPriceAtTick(tickUpper), - amount0Desired, - amount1Desired - ); - - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); - - uint256 balance0Before = getPermissionedCurrency(key.currency0).balanceOfSelf(); - uint256 balance1Before = getPermissionedCurrency(key.currency1).balanceOfSelf(); - uint256 balance0ManagerBefore = key.currency0.balanceOf(address(manager)); - uint256 tokenId = lpm.nextTokenId(); - - mint(config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES); - - uint256 balance0After = getPermissionedCurrency(key.currency0).balanceOfSelf(); - uint256 balance1After = getPermissionedCurrency(key.currency1).balanceOfSelf(); - uint256 balance0ManagerAfter = key.currency0.balanceOf(address(manager)); - assertEq(tokenId, lpm.nextTokenId() - 1); - assertEq(IERC721(address(lpm)).ownerOf(tokenId), address(this)); - assertEq(balance0Before - balance0After, balance0ManagerAfter - balance0ManagerBefore); - assertEq(balance1Before - balance1After, amount1Desired); - } - - function test_mint_toRecipient() public { - _test_mint_toRecipient(key0); - _test_mint_toRecipient(key1); - _test_mint_toRecipient(key2); - } - - function _test_mint_toRecipient(PoolKey memory key) internal { - int24 tickLower = -int24(key.tickSpacing); - int24 tickUpper = int24(key.tickSpacing); - uint256 amount0Desired = 100e18; - uint256 amount1Desired = 100e18; - uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( - SQRT_PRICE_1_1, - TickMath.getSqrtPriceAtTick(tickLower), - TickMath.getSqrtPriceAtTick(tickUpper), - amount0Desired, - amount1Desired - ); - - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); - - uint256 tokenId = lpm.nextTokenId(); - - BalanceInfo memory balanceInfoBefore = getBalanceInfoSelfAndManager(key); - - // mint to specific recipient, alice, not using the recipient constants - mint(config, liquidityToAdd, alice, ZERO_BYTES); - - BalanceInfo memory balanceInfoAfter = getBalanceInfoSelfAndManager(key); - - assertEq(tokenId, lpm.nextTokenId() - 1); - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - assertEq( - balanceInfoBefore.balance0 - balanceInfoAfter.balance0, - balanceInfoAfter.balance0Manager - balanceInfoBefore.balance0Manager - ); - assertEq( - balanceInfoBefore.balance1 - balanceInfoAfter.balance1, - balanceInfoAfter.balance1Manager - balanceInfoBefore.balance1Manager - ); - assertEq(balanceInfoBefore.balance0 - balanceInfoAfter.balance0, amount0Desired); - assertEq(balanceInfoBefore.balance1 - balanceInfoAfter.balance1, amount1Desired); - } - - function test_fuzz_mint_recipient(ModifyLiquidityParams memory seedParams) public { - _test_fuzz_mint_recipient(key0, seedParams); - _test_fuzz_mint_recipient(key1, seedParams); - _test_fuzz_mint_recipient(key2, seedParams); - } - - function _test_fuzz_mint_recipient(PoolKey memory key, ModifyLiquidityParams memory seedParams) internal { - ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1); - PositionConfig memory config = - PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - - uint256 liquidityToAdd = - params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); - - Currency currency0_ = getPermissionedCurrency(key.currency0); - Currency currency1_ = getPermissionedCurrency(key.currency1); - Currency currency0Adapter = key.currency0; - Currency currency1Adapter = key.currency1; - uint256 tokenId = lpm.nextTokenId(); - uint256 balance0Before = currency0_.balanceOfSelf(); - uint256 balance1Before = currency1_.balanceOfSelf(); - uint256 balance0BeforeAlice = currency0_.balanceOf(alice); - uint256 balance1BeforeAlice = currency1_.balanceOf(alice); - uint256 balance0ManagerBefore = currency0Adapter.balanceOf(address(manager)); - uint256 balance1ManagerBefore = currency1Adapter.balanceOf(address(manager)); - - mint(config, liquidityToAdd, alice, ZERO_BYTES); - - uint256 balance0ManagerAfter = currency0Adapter.balanceOf(address(manager)); - uint256 balance1ManagerAfter = currency1Adapter.balanceOf(address(manager)); - - // alice was not the payer - assertEq(tokenId, lpm.nextTokenId() - 1); - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - assertEq(balance0Before - currency0_.balanceOfSelf(), balance0ManagerAfter - balance0ManagerBefore); - assertEq(balance1Before - currency1_.balanceOfSelf(), balance1ManagerAfter - balance1ManagerBefore); - assertEq(currency0_.balanceOf(alice), balance0BeforeAlice); - assertEq(currency1_.balanceOf(alice), balance1BeforeAlice); - } - - /// @dev clear cannot be used on mint (negative delta) - function test_fuzz_mint_clear_revert(ModifyLiquidityParams memory seedParams) public { - _test_fuzz_mint_clear_revert(key0, seedParams); - _test_fuzz_mint_clear_revert(key1, seedParams); - _test_fuzz_mint_clear_revert(key2, seedParams); - } - - function _test_fuzz_mint_clear_revert(PoolKey memory key, ModifyLiquidityParams memory seedParams) internal { - ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1); - PositionConfig memory config = - PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - - uint256 liquidityToAdd = - params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); - - Plan memory planner = Planner.init(); - planner.add( - Actions.MINT_POSITION, - abi.encode( - config.poolKey, - config.tickLower, - config.tickUpper, - liquidityToAdd, - MAX_SLIPPAGE_INCREASE, - MAX_SLIPPAGE_INCREASE, - address(this), - ZERO_BYTES - ) - ); - planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency0, type(uint256).max)); - planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency1, type(uint256).max)); - bytes memory calls = planner.encode(); - - Currency negativeDeltaCurrency = key.currency0; - // because we're fuzzing the range, single-sided mint with currency1 means currency0Delta = 0 and currency1Delta < 0 - if (config.tickUpper <= 0) { - negativeDeltaCurrency = key.currency1; - } - - vm.expectRevert(abi.encodeWithSelector(DeltaResolver.DeltaNotPositive.selector, (negativeDeltaCurrency))); - lpm.modifyLiquidities(calls, _deadline); - } - - function test_mint_slippage_revertAmount0() public { - _test_mint_slippage_revertAmount0(key0); - _test_mint_slippage_revertAmount0(key1); - _test_mint_slippage_revertAmount0(key2); - } - - function _test_mint_slippage_revertAmount0(PoolKey memory key) internal { - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); - - uint256 liquidity = 1e18; - (uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity( - SQRT_PRICE_1_1, - TickMath.getSqrtPriceAtTick(config.tickLower), - TickMath.getSqrtPriceAtTick(config.tickUpper), - uint128(liquidity) - ); - - bytes memory calls = - getMintEncoded(config, liquidity, 1 wei, MAX_SLIPPAGE_INCREASE, ActionConstants.MSG_SENDER, ZERO_BYTES); - - vm.expectRevert(abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, 1 wei, amount0 + 1)); - lpm.modifyLiquidities(calls, _deadline); - } - - function test_mint_slippage_revertAmount1() public { - _test_mint_slippage_revertAmount1(key0); - _test_mint_slippage_revertAmount1(key1); - _test_mint_slippage_revertAmount1(key2); - } - - function _test_mint_slippage_revertAmount1(PoolKey memory key) internal { - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); - - uint256 liquidity = 1e18; - (, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( - SQRT_PRICE_1_1, - TickMath.getSqrtPriceAtTick(config.tickLower), - TickMath.getSqrtPriceAtTick(config.tickUpper), - uint128(liquidity) - ); - - bytes memory calls = - getMintEncoded(config, liquidity, MAX_SLIPPAGE_INCREASE, 1 wei, ActionConstants.MSG_SENDER, ZERO_BYTES); - - vm.expectRevert(abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, 1 wei, amount1 + 1)); - lpm.modifyLiquidities(calls, _deadline); - } - - function test_mint_slippage_exactDoesNotRevert() public { - _test_mint_slippage_exactDoesNotRevert(key0); - _test_mint_slippage_exactDoesNotRevert(key1); - _test_mint_slippage_exactDoesNotRevert(key2); - } - - function _test_mint_slippage_exactDoesNotRevert(PoolKey memory key) internal { - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); - - uint256 liquidity = 1e18; - (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( - SQRT_PRICE_1_1, - TickMath.getSqrtPriceAtTick(config.tickLower), - TickMath.getSqrtPriceAtTick(config.tickUpper), - uint128(liquidity) - ); - uint128 slippage = uint128(amount0) + 1; - - uint256 balance0ManagerBefore = key.currency0.balanceOf(address(manager)); - uint256 balance1ManagerBefore = key.currency1.balanceOf(address(manager)); - - assertEq(amount0, amount1); // symmetric liquidity - - bytes memory calls = - getMintEncoded(config, liquidity, slippage, slippage, ActionConstants.MSG_SENDER, ZERO_BYTES); - - lpm.modifyLiquidities(calls, _deadline); - - uint256 balance0ManagerAfter = key.currency0.balanceOf(address(manager)); - uint256 balance1ManagerAfter = key.currency1.balanceOf(address(manager)); - - assertEq(balance0ManagerAfter - balance0ManagerBefore, slippage); - assertEq(balance1ManagerAfter - balance1ManagerBefore, slippage); - } - - function test_mint_slippage_revert_swap() public { - _test_mint_slippage_revert_swap(key0); - _test_mint_slippage_revert_swap(key1); - _test_mint_slippage_revert_swap(key2); - } - - function _test_mint_slippage_revert_swap(PoolKey memory key) internal { - // swapping will cause a slippage revert - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); - - uint256 liquidity = 100e18; - (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( - SQRT_PRICE_1_1, - TickMath.getSqrtPriceAtTick(config.tickLower), - TickMath.getSqrtPriceAtTick(config.tickUpper), - uint128(liquidity) - ); - uint128 slippage = uint128(amount0) + 1; - - assertEq(amount0, amount1); // symmetric liquidity - - bytes memory calls = - getMintEncoded(config, liquidity, slippage, slippage, ActionConstants.MSG_SENDER, ZERO_BYTES); - - // swap to move the price and cause a slippage revert - swap(key, true, -1e18); - - vm.expectRevert( - abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, slippage, 1199947202932782783) - ); - lpm.modifyLiquidities(calls, _deadline); - } - - function test_permissioned_mint_allowed_user() public { - _test_permissioned_mint_allowed_user(key0); - _test_permissioned_mint_allowed_user(key1); - _test_permissioned_mint_allowed_user(key2); - } - - function _test_permissioned_mint_allowed_user(PoolKey memory key) internal { - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); - - uint256 liquidity = 1e18; - uint256 tokenId = lpm.nextTokenId(); - - // Alice is in the allowlist, so she should be able to mint - vm.prank(alice); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - } - - function test_permissioned_mint_alt_posm_diff_hooks_reverts() public { - // secondary posm uses different hooks contract - _test_permissioned_mint_alt_posm_diff_hooks_reverts(key0, secondaryPosm); - _test_permissioned_mint_alt_posm_diff_hooks_reverts(key1, secondaryPosm); - _test_permissioned_mint_alt_posm_diff_hooks_reverts(key2, secondaryPosm); - } - - function _test_permissioned_mint_alt_posm_diff_hooks_reverts(PoolKey memory key, IPositionManager posm) internal { - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); - - uint256 liquidity = 1e18; - - // we don't use the helper, so we can choose which position manager to use - bytes memory calls = getMintEncoded(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - - vm.prank(alice); - vm.expectRevert(InvalidHook.selector); - posm.modifyLiquidities(calls, block.timestamp + 1); - } - - function test_permissioned_mint_alt_posm_same_hooks_reverts() public { - // tertiary posm uses the same hooks contract - _test_permissioned_mint_alt_posm_same_hooks_reverts(key0, tertiaryPosm); - _test_permissioned_mint_alt_posm_same_hooks_reverts(key1, tertiaryPosm); - _test_permissioned_mint_alt_posm_same_hooks_reverts(key2, tertiaryPosm); - } +// // SPDX-License-Identifier: MIT +// pragma solidity 0.8.26; + +// import "forge-std/Test.sol"; +// import {IERC721} from "forge-std/interfaces/IERC721.sol"; +// import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +// import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +// import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +// import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol"; +// import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +// import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; +// import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +// import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; +// import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +// import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; +// import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; +// import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +// import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; + +// import {IPositionManager} from "../../../src/interfaces/IPositionManager.sol"; +// import {Actions} from "../../../src/libraries/Actions.sol"; +// import {DeltaResolver} from "../../../src/base/DeltaResolver.sol"; +// import {PositionConfig} from "test/shared/PositionConfig.sol"; +// import {SlippageCheck} from "../../../src/libraries/SlippageCheck.sol"; +// import {BaseActionsRouter} from "../../../src/base/BaseActionsRouter.sol"; +// import {ActionConstants} from "../../../src/libraries/ActionConstants.sol"; +// import {LiquidityFuzzers} from "../../shared/fuzz/LiquidityFuzzers.sol"; +// import {Planner, Plan} from "../../shared/Planner.sol"; +// import {PermissionedPosmTestSetup, BalanceInfo} from "./shared/PermissionedPosmTestSetup.sol"; +// import {ReentrantToken} from "../../mocks/ReentrantToken.sol"; +// import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +// import {MockAllowlistChecker, MockPermissionedToken} from "./PermissionedPoolsBase.sol"; +// import {PermissionsAdapter, IERC20} from "../../../src/hooks/permissionedPools/PermissionsAdapter.sol"; +// import {PermissionFlags, PermissionFlag} from "../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; + +// contract PermissionedPositionManagerTest is Test, PermissionedPosmTestSetup, LiquidityFuzzers { +// using FixedPointMathLib for uint256; +// using StateLibrary for IPoolManager; + +// // To allow testing without importing PermissionedHooks +// error Unauthorized(); +// error InvalidHook(); +// error HookCallFailed(); + +// PoolId public poolId; +// PoolId public poolIdFake; + +// // Permissioned components +// MockAllowlistChecker public mockAllowListChecker; +// PermissionsAdapter public permissionsAdapter0; +// PermissionsAdapter public permissionsAdapter2; +// MockERC20 public originalToken0; +// MockERC20 public originalToken2; +// IPositionManager public secondaryPosm; +// IPositionManager public tertiaryPosm; + +// // permissioned / normal +// PoolKey public key0; +// // normal / permissioned +// PoolKey public key1; +// // permissioned / permissioned +// PoolKey public key2; +// // normal / normal +// PoolKey public key3; + +// PoolKey public keyFake0; +// PoolKey public keyFake1; +// PoolKey public keyFake2; + +// PoolKey public insecureKey; + +// // Test Users +// address public alice = makeAddr("ALICE"); +// address public unauthorizedUser = makeAddr("UNAUTHORIZED"); + +// function setUp() public { +// permit2 = IAllowanceTransfer(deployPermit2()); + +// deployFreshManagerAndRoutersPermissioned(address(permit2), address(_WETH9)); +// (currency0, currency1) = deployMintAndApprove2Currencies(true, false); +// currency2 = deployMintAndApproveCurrency(true); + +// setUpPosms(); +// setupPermissionedComponents(); +// setupPool(); + +// // set up approvals for alice +// seedBalance(alice); +// approvePosmFor(alice); +// permissionsAdapter0.updateSwappingEnabled(true); +// permissionsAdapter2.updateSwappingEnabled(true); +// } + +// function setUpPosms() internal { +// deployAndApprovePosm(manager, address(permissionsAdapterFactory), keccak256("permissionedPosm")); + +// // alternate position manager using different hooks +// secondaryPosm = +// deployAndApprovePosmOnly(manager, address(permissionsAdapterFactory), keccak256("secondaryPosm")); + +// // alternate position manager using the same hooks +// tertiaryPosm = deployAndApprovePosmOnly(manager, address(permissionsAdapterFactory), keccak256("tertiaryPosm")); +// } + +// function setupPool() internal { +// (key0, poolId) = +// initPool(Currency.wrap(address(permissionsAdapter0)), currency1, permissionedHooks, 3000, SQRT_PRICE_1_1); +// (key1, poolId) = +// initPool(currency1, Currency.wrap(address(permissionsAdapter2)), permissionedHooks, 3000, SQRT_PRICE_1_1); +// (key2, poolId) = initPool( +// Currency.wrap(address(permissionsAdapter0)), +// Currency.wrap(address(permissionsAdapter2)), +// permissionedHooks, +// 3000, +// SQRT_PRICE_1_1 +// ); + +// (keyFake0, poolId) = initPool( +// Currency.wrap(address(permissionsAdapter0)), currency1, secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1 +// ); +// (keyFake1, poolId) = initPool( +// currency1, Currency.wrap(address(permissionsAdapter2)), secondaryPermissionedHooks, 3000, SQRT_PRICE_1_1 +// ); +// (keyFake2, poolId) = initPool( +// Currency.wrap(address(permissionsAdapter0)), +// Currency.wrap(address(permissionsAdapter2)), +// secondaryPermissionedHooks, +// 3000, +// SQRT_PRICE_1_1 +// ); +// (insecureKey, poolId) = initPool( +// Currency.wrap(address(permissionsAdapter0)), +// Currency.wrap(address(permissionsAdapter2)), +// insecureHooks, +// 3000, +// SQRT_PRICE_1_1 +// ); +// } + +// function setupPermissionedComponents() internal { +// mockAllowListChecker = new MockAllowlistChecker(MockPermissionedToken(Currency.unwrap(currency0))); +// setUpCurrencyZero(); +// setUpCurrencyTwo(); +// } + +// function setUpCurrencyZero() internal { +// setUpAllowlist(currency0); +// originalToken0 = MockERC20(Currency.unwrap(currency0)); +// // ensure expected ordering +// while (true) { +// permissionsAdapter0 = PermissionsAdapter( +// permissionsAdapterFactory.createPermissionsAdapter( +// IERC20(address(originalToken0)), address(this), mockAllowListChecker +// ) +// ); +// if (address(permissionsAdapter0) < Currency.unwrap(currency1)) { +// break; +// } +// } +// setUpPemissionsAdapter(permissionsAdapter0, currency0); +// } + +// function setUpCurrencyTwo() internal { +// setUpAllowlist(currency2); +// originalToken2 = MockERC20(Currency.unwrap(currency2)); + +// // ensure expected ordering +// while (true) { +// permissionsAdapter2 = PermissionsAdapter( +// permissionsAdapterFactory.createPermissionsAdapter( +// IERC20(address(originalToken2)), address(this), mockAllowListChecker +// ) +// ); +// if (Currency.unwrap(currency1) < address(permissionsAdapter2)) { +// break; +// } +// } +// setUpPemissionsAdapter(permissionsAdapter2, currency2); +// } + +// function setUpAllowlist(Currency currency) internal { +// MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(this), PermissionFlags.ALL_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), PermissionFlags.ALL_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency)) +// .setAllowlist(address(secondaryPosm), PermissionFlags.ALL_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency)) +// .setAllowlist(address(permissionsAdapterFactory), PermissionFlags.ALL_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(lpm), PermissionFlags.ALL_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency)).setAllowlist(address(manager), PermissionFlags.ALL_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency)) +// .setAllowlist(address(permissionedSwapRouter), PermissionFlags.ALL_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency2)) +// .setAllowlist(address(permissionedHooks), PermissionFlags.ALL_ALLOWED); +// } + +// function setUpPemissionsAdapter(PermissionsAdapter permissionsAdapter, Currency currency) internal { +// adapterToPermissioned[Currency.wrap(address(permissionsAdapter))] = currency; + +// MockPermissionedToken(Currency.unwrap(currency)).mint(address(this), 1000 ether); +// MockPermissionedToken(Currency.unwrap(currency)) +// .setAllowlist(address(permissionsAdapter), PermissionFlags.ALL_ALLOWED); + +// // permissions adapter contract must have a non-zero balance of the permissioned token +// currency.transfer(address(permissionsAdapter), 1); + +// permissionsAdapter.updateAllowedWrapper(address(manager), true); +// permissionsAdapter.updateAllowedWrapper(address(lpm), true); +// permissionsAdapter.updateAllowedWrapper(address(secondaryPosm), true); +// permissionsAdapter.updateAllowedWrapper(address(permissionedSwapRouter), true); + +// permissionsAdapterFactory.verifyPermissionsAdapter(address(permissionsAdapter)); + +// Currency permissionsAdapterCurrency = Currency.wrap(address(permissionsAdapter)); + +// setAllowedHooks(lpm, permissionsAdapterCurrency, permissionedHooks); +// setAllowedHooks(tertiaryPosm, permissionsAdapterCurrency, permissionedHooks); + +// setAllowedHooks(lpm, permissionsAdapterCurrency, secondaryPermissionedHooks); +// setAllowedHooks(secondaryPosm, permissionsAdapterCurrency, secondaryPermissionedHooks); +// setAllowedHooks(tertiaryPosm, permissionsAdapterCurrency, secondaryPermissionedHooks); + +// setAllowedHooks(lpm, permissionsAdapterCurrency, insecureHooks); +// } + +// function setAllowedHooks(IPositionManager posm, Currency currency, IHooks permissionedHooks_) internal { +// // setAllowedHook selector +// bytes4 selector = 0xb5cdc484; +// bytes memory data = abi.encodeWithSelector(selector, currency, permissionedHooks_, true); +// (bool success,) = address(posm).call(data); +// require(success, "Failed to set hooks"); +// } + +// function test_modifyLiquidities_reverts_deadlinePassed() public { +// _test_modifyLiquidities_reverts_deadlinePassed(key0); +// _test_modifyLiquidities_reverts_deadlinePassed(key1); +// _test_modifyLiquidities_reverts_deadlinePassed(key2); +// } + +// function _test_modifyLiquidities_reverts_deadlinePassed(PoolKey memory key) internal { +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: 0, tickUpper: 60}); +// bytes memory calls = getMintEncoded(config, 1e18, ActionConstants.MSG_SENDER, ""); + +// uint256 deadline = vm.getBlockTimestamp() - 1; + +// vm.expectRevert(abi.encodeWithSelector(IPositionManager.DeadlinePassed.selector, deadline)); +// lpm.modifyLiquidities(calls, deadline); +// } + +// function test_modifyLiquidities_reverts_mismatchedLengths() public { +// Plan memory planner = Planner.init(); +// planner.add(Actions.MINT_POSITION, abi.encode("test")); +// planner.add(Actions.BURN_POSITION, abi.encode("test")); + +// bytes[] memory badParams = new bytes[](1); + +// vm.expectRevert(BaseActionsRouter.InputLengthMismatch.selector); +// lpm.modifyLiquidities(abi.encode(planner.actions, badParams), block.timestamp + 1); +// } + +// function test_modifyLiquidities_reverts_reentrancy() public { +// PoolKey memory key; + +// // Create a reentrant token and initialize the pool with a verified adapter so beforeInitialize passes +// Currency reentrantToken = Currency.wrap(address(new ReentrantToken(lpm))); +// Currency adapterCurrency = Currency.wrap(address(permissionsAdapter0)); +// (Currency c0, Currency c1) = (Currency.unwrap(reentrantToken) < Currency.unwrap(adapterCurrency)) +// ? (reentrantToken, adapterCurrency) +// : (adapterCurrency, reentrantToken); + +// // Set up approvals for the reentrant token +// approvePosmCurrency(reentrantToken); +// (key, poolId) = initPool(c0, c1, permissionedHooks, 3000, SQRT_PRICE_1_1); + +// // Try to add liquidity at that range, but the token reenters posm +// PositionConfig memory config = +// PositionConfig({poolKey: key, tickLower: -int24(key.tickSpacing), tickUpper: int24(key.tickSpacing)}); + +// bytes memory calls = getMintEncoded(config, 1e18, ActionConstants.MSG_SENDER, ""); + +// // Permit2.transferFrom does not bubble the ContractLocked error and instead reverts with its own error +// vm.expectRevert("TRANSFER_FROM_FAILED"); +// lpm.modifyLiquidities(calls, block.timestamp + 1); +// } + +// function test_fuzz_mint_withLiquidityDelta(ModifyLiquidityParams memory params, uint160 sqrtPriceX96) public { +// bound(sqrtPriceX96, MIN_PRICE_LIMIT, MAX_PRICE_LIMIT); + +// _test_fuzz_mint_withLiquidityDelta(key0, params, sqrtPriceX96); +// _test_fuzz_mint_withLiquidityDelta(key1, params, sqrtPriceX96); +// _test_fuzz_mint_withLiquidityDelta(key2, params, sqrtPriceX96); +// } + +// function _test_fuzz_mint_withLiquidityDelta( +// PoolKey memory key, +// ModifyLiquidityParams memory params, +// uint160 sqrtPriceX96 +// ) internal { +// params = createFuzzyLiquidityParams(key, params, sqrtPriceX96); +// PositionConfig memory config = +// PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + +// // liquidity is a uint +// uint256 liquidityToAdd = +// params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + +// uint256 balance0Before = getPermissionedCurrency(key.currency0).balanceOfSelf(); +// uint256 balance1Before = getPermissionedCurrency(key.currency1).balanceOfSelf(); +// uint256 balance0ManagerBefore = key.currency0.balanceOf(address(manager)); +// uint256 balance1ManagerBefore = key.currency1.balanceOf(address(manager)); +// uint256 tokenId = lpm.nextTokenId(); + +// mint(config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES); + +// uint256 balance0ManagerAfter = key.currency0.balanceOf(address(manager)); +// uint256 balance1ManagerAfter = key.currency1.balanceOf(address(manager)); +// uint256 liquidity = lpm.getPositionLiquidity(tokenId); + +// assertEq(tokenId, lpm.nextTokenId() - 1); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), address(this)); +// assertEq(liquidity, uint256(params.liquidityDelta)); +// assertEq( +// balance0Before - getPermissionedCurrency(key.currency0).balanceOfSelf(), +// balance0ManagerAfter - balance0ManagerBefore, +// "incorrect amount0" +// ); +// assertEq( +// balance1Before - getPermissionedCurrency(key.currency1).balanceOfSelf(), +// balance1ManagerAfter - balance1ManagerBefore, +// "incorrect amount1" +// ); +// } + +// function test_mint_exactTokenRatios() public { +// _test_mint_exactTokenRatios(key0); +// _test_mint_exactTokenRatios(key1); +// _test_mint_exactTokenRatios(key2); +// } + +// function _test_mint_exactTokenRatios(PoolKey memory key) internal { +// int24 tickLower = -int24(key.tickSpacing); +// int24 tickUpper = int24(key.tickSpacing); +// uint256 amount0Desired = 100e18; +// uint256 amount1Desired = 100e18; +// uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( +// SQRT_PRICE_1_1, +// TickMath.getSqrtPriceAtTick(tickLower), +// TickMath.getSqrtPriceAtTick(tickUpper), +// amount0Desired, +// amount1Desired +// ); + +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + +// uint256 balance0Before = getPermissionedCurrency(key.currency0).balanceOfSelf(); +// uint256 balance1Before = getPermissionedCurrency(key.currency1).balanceOfSelf(); +// uint256 balance0ManagerBefore = key.currency0.balanceOf(address(manager)); +// uint256 tokenId = lpm.nextTokenId(); + +// mint(config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES); + +// uint256 balance0After = getPermissionedCurrency(key.currency0).balanceOfSelf(); +// uint256 balance1After = getPermissionedCurrency(key.currency1).balanceOfSelf(); +// uint256 balance0ManagerAfter = key.currency0.balanceOf(address(manager)); +// assertEq(tokenId, lpm.nextTokenId() - 1); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), address(this)); +// assertEq(balance0Before - balance0After, balance0ManagerAfter - balance0ManagerBefore); +// assertEq(balance1Before - balance1After, amount1Desired); +// } + +// function test_mint_toRecipient() public { +// _test_mint_toRecipient(key0); +// _test_mint_toRecipient(key1); +// _test_mint_toRecipient(key2); +// } + +// function _test_mint_toRecipient(PoolKey memory key) internal { +// int24 tickLower = -int24(key.tickSpacing); +// int24 tickUpper = int24(key.tickSpacing); +// uint256 amount0Desired = 100e18; +// uint256 amount1Desired = 100e18; +// uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( +// SQRT_PRICE_1_1, +// TickMath.getSqrtPriceAtTick(tickLower), +// TickMath.getSqrtPriceAtTick(tickUpper), +// amount0Desired, +// amount1Desired +// ); + +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + +// uint256 tokenId = lpm.nextTokenId(); + +// BalanceInfo memory balanceInfoBefore = getBalanceInfoSelfAndManager(key); + +// // mint to specific recipient, alice, not using the recipient constants +// mint(config, liquidityToAdd, alice, ZERO_BYTES); + +// BalanceInfo memory balanceInfoAfter = getBalanceInfoSelfAndManager(key); + +// assertEq(tokenId, lpm.nextTokenId() - 1); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// assertEq( +// balanceInfoBefore.balance0 - balanceInfoAfter.balance0, +// balanceInfoAfter.balance0Manager - balanceInfoBefore.balance0Manager +// ); +// assertEq( +// balanceInfoBefore.balance1 - balanceInfoAfter.balance1, +// balanceInfoAfter.balance1Manager - balanceInfoBefore.balance1Manager +// ); +// assertEq(balanceInfoBefore.balance0 - balanceInfoAfter.balance0, amount0Desired); +// assertEq(balanceInfoBefore.balance1 - balanceInfoAfter.balance1, amount1Desired); +// } + +// function test_fuzz_mint_recipient(ModifyLiquidityParams memory seedParams) public { +// _test_fuzz_mint_recipient(key0, seedParams); +// _test_fuzz_mint_recipient(key1, seedParams); +// _test_fuzz_mint_recipient(key2, seedParams); +// } + +// function _test_fuzz_mint_recipient(PoolKey memory key, ModifyLiquidityParams memory seedParams) internal { +// ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1); +// PositionConfig memory config = +// PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + +// uint256 liquidityToAdd = +// params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + +// Currency currency0_ = getPermissionedCurrency(key.currency0); +// Currency currency1_ = getPermissionedCurrency(key.currency1); +// Currency currency0Adapter = key.currency0; +// Currency currency1Adapter = key.currency1; +// uint256 tokenId = lpm.nextTokenId(); +// uint256 balance0Before = currency0_.balanceOfSelf(); +// uint256 balance1Before = currency1_.balanceOfSelf(); +// uint256 balance0BeforeAlice = currency0_.balanceOf(alice); +// uint256 balance1BeforeAlice = currency1_.balanceOf(alice); +// uint256 balance0ManagerBefore = currency0Adapter.balanceOf(address(manager)); +// uint256 balance1ManagerBefore = currency1Adapter.balanceOf(address(manager)); + +// mint(config, liquidityToAdd, alice, ZERO_BYTES); + +// uint256 balance0ManagerAfter = currency0Adapter.balanceOf(address(manager)); +// uint256 balance1ManagerAfter = currency1Adapter.balanceOf(address(manager)); + +// // alice was not the payer +// assertEq(tokenId, lpm.nextTokenId() - 1); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// assertEq(balance0Before - currency0_.balanceOfSelf(), balance0ManagerAfter - balance0ManagerBefore); +// assertEq(balance1Before - currency1_.balanceOfSelf(), balance1ManagerAfter - balance1ManagerBefore); +// assertEq(currency0_.balanceOf(alice), balance0BeforeAlice); +// assertEq(currency1_.balanceOf(alice), balance1BeforeAlice); +// } + +// /// @dev clear cannot be used on mint (negative delta) +// function test_fuzz_mint_clear_revert(ModifyLiquidityParams memory seedParams) public { +// _test_fuzz_mint_clear_revert(key0, seedParams); +// _test_fuzz_mint_clear_revert(key1, seedParams); +// _test_fuzz_mint_clear_revert(key2, seedParams); +// } + +// function _test_fuzz_mint_clear_revert(PoolKey memory key, ModifyLiquidityParams memory seedParams) internal { +// ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1); +// PositionConfig memory config = +// PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + +// uint256 liquidityToAdd = +// params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + +// Plan memory planner = Planner.init(); +// planner.add( +// Actions.MINT_POSITION, +// abi.encode( +// config.poolKey, +// config.tickLower, +// config.tickUpper, +// liquidityToAdd, +// MAX_SLIPPAGE_INCREASE, +// MAX_SLIPPAGE_INCREASE, +// address(this), +// ZERO_BYTES +// ) +// ); +// planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency0, type(uint256).max)); +// planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency1, type(uint256).max)); +// bytes memory calls = planner.encode(); + +// Currency negativeDeltaCurrency = key.currency0; +// // because we're fuzzing the range, single-sided mint with currency1 means currency0Delta = 0 and currency1Delta < 0 +// if (config.tickUpper <= 0) { +// negativeDeltaCurrency = key.currency1; +// } + +// vm.expectRevert(abi.encodeWithSelector(DeltaResolver.DeltaNotPositive.selector, (negativeDeltaCurrency))); +// lpm.modifyLiquidities(calls, _deadline); +// } + +// function test_mint_slippage_revertAmount0() public { +// _test_mint_slippage_revertAmount0(key0); +// _test_mint_slippage_revertAmount0(key1); +// _test_mint_slippage_revertAmount0(key2); +// } + +// function _test_mint_slippage_revertAmount0(PoolKey memory key) internal { +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + +// uint256 liquidity = 1e18; +// (uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity( +// SQRT_PRICE_1_1, +// TickMath.getSqrtPriceAtTick(config.tickLower), +// TickMath.getSqrtPriceAtTick(config.tickUpper), +// uint128(liquidity) +// ); + +// bytes memory calls = +// getMintEncoded(config, liquidity, 1 wei, MAX_SLIPPAGE_INCREASE, ActionConstants.MSG_SENDER, ZERO_BYTES); + +// vm.expectRevert(abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, 1 wei, amount0 + 1)); +// lpm.modifyLiquidities(calls, _deadline); +// } + +// function test_mint_slippage_revertAmount1() public { +// _test_mint_slippage_revertAmount1(key0); +// _test_mint_slippage_revertAmount1(key1); +// _test_mint_slippage_revertAmount1(key2); +// } + +// function _test_mint_slippage_revertAmount1(PoolKey memory key) internal { +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + +// uint256 liquidity = 1e18; +// (, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( +// SQRT_PRICE_1_1, +// TickMath.getSqrtPriceAtTick(config.tickLower), +// TickMath.getSqrtPriceAtTick(config.tickUpper), +// uint128(liquidity) +// ); + +// bytes memory calls = +// getMintEncoded(config, liquidity, MAX_SLIPPAGE_INCREASE, 1 wei, ActionConstants.MSG_SENDER, ZERO_BYTES); + +// vm.expectRevert(abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, 1 wei, amount1 + 1)); +// lpm.modifyLiquidities(calls, _deadline); +// } + +// function test_mint_slippage_exactDoesNotRevert() public { +// _test_mint_slippage_exactDoesNotRevert(key0); +// _test_mint_slippage_exactDoesNotRevert(key1); +// _test_mint_slippage_exactDoesNotRevert(key2); +// } + +// function _test_mint_slippage_exactDoesNotRevert(PoolKey memory key) internal { +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + +// uint256 liquidity = 1e18; +// (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( +// SQRT_PRICE_1_1, +// TickMath.getSqrtPriceAtTick(config.tickLower), +// TickMath.getSqrtPriceAtTick(config.tickUpper), +// uint128(liquidity) +// ); +// uint128 slippage = uint128(amount0) + 1; + +// uint256 balance0ManagerBefore = key.currency0.balanceOf(address(manager)); +// uint256 balance1ManagerBefore = key.currency1.balanceOf(address(manager)); + +// assertEq(amount0, amount1); // symmetric liquidity + +// bytes memory calls = +// getMintEncoded(config, liquidity, slippage, slippage, ActionConstants.MSG_SENDER, ZERO_BYTES); + +// lpm.modifyLiquidities(calls, _deadline); + +// uint256 balance0ManagerAfter = key.currency0.balanceOf(address(manager)); +// uint256 balance1ManagerAfter = key.currency1.balanceOf(address(manager)); + +// assertEq(balance0ManagerAfter - balance0ManagerBefore, slippage); +// assertEq(balance1ManagerAfter - balance1ManagerBefore, slippage); +// } + +// function test_mint_slippage_revert_swap() public { +// _test_mint_slippage_revert_swap(key0); +// _test_mint_slippage_revert_swap(key1); +// _test_mint_slippage_revert_swap(key2); +// } + +// function _test_mint_slippage_revert_swap(PoolKey memory key) internal { +// // swapping will cause a slippage revert +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + +// uint256 liquidity = 100e18; +// (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( +// SQRT_PRICE_1_1, +// TickMath.getSqrtPriceAtTick(config.tickLower), +// TickMath.getSqrtPriceAtTick(config.tickUpper), +// uint128(liquidity) +// ); +// uint128 slippage = uint128(amount0) + 1; + +// assertEq(amount0, amount1); // symmetric liquidity + +// bytes memory calls = +// getMintEncoded(config, liquidity, slippage, slippage, ActionConstants.MSG_SENDER, ZERO_BYTES); + +// // swap to move the price and cause a slippage revert +// swap(key, true, -1e18); + +// vm.expectRevert( +// abi.encodeWithSelector(SlippageCheck.MaximumAmountExceeded.selector, slippage, 1199947202932782783) +// ); +// lpm.modifyLiquidities(calls, _deadline); +// } + +// function test_permissioned_mint_allowed_user() public { +// _test_permissioned_mint_allowed_user(key0); +// _test_permissioned_mint_allowed_user(key1); +// _test_permissioned_mint_allowed_user(key2); +// } + +// function _test_permissioned_mint_allowed_user(PoolKey memory key) internal { +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + +// uint256 liquidity = 1e18; +// uint256 tokenId = lpm.nextTokenId(); + +// // Alice is in the allowlist, so she should be able to mint +// vm.prank(alice); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// } + +// function test_permissioned_mint_alt_posm_diff_hooks_reverts() public { +// // secondary posm uses different hooks contract +// _test_permissioned_mint_alt_posm_diff_hooks_reverts(key0, secondaryPosm); +// _test_permissioned_mint_alt_posm_diff_hooks_reverts(key1, secondaryPosm); +// _test_permissioned_mint_alt_posm_diff_hooks_reverts(key2, secondaryPosm); +// } + +// function _test_permissioned_mint_alt_posm_diff_hooks_reverts(PoolKey memory key, IPositionManager posm) internal { +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + +// uint256 liquidity = 1e18; + +// // we don't use the helper, so we can choose which position manager to use +// bytes memory calls = getMintEncoded(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + +// vm.prank(alice); +// vm.expectRevert(InvalidHook.selector); +// posm.modifyLiquidities(calls, block.timestamp + 1); +// } + +// function test_permissioned_mint_alt_posm_same_hooks_reverts() public { +// // tertiary posm uses the same hooks contract +// _test_permissioned_mint_alt_posm_same_hooks_reverts(key0, tertiaryPosm); +// _test_permissioned_mint_alt_posm_same_hooks_reverts(key1, tertiaryPosm); +// _test_permissioned_mint_alt_posm_same_hooks_reverts(key2, tertiaryPosm); +// } - function _test_permissioned_mint_alt_posm_same_hooks_reverts(PoolKey memory key, IPositionManager posm) internal { - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); +// function _test_permissioned_mint_alt_posm_same_hooks_reverts(PoolKey memory key, IPositionManager posm) internal { +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; +// uint256 liquidity = 1e18; - // we don't use the helper, so we can choose which position manager to use - bytes memory calls = getMintEncoded(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// // we don't use the helper, so we can choose which position manager to use +// bytes memory calls = getMintEncoded(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - vm.prank(alice); - vm.expectRevert( - abi.encodeWithSelector( - CustomRevert.WrappedError.selector, - address(permissionedHooks), - IHooks.beforeAddLiquidity.selector, - abi.encodeWithSelector(Unauthorized.selector), - abi.encodeWithSelector(HookCallFailed.selector) - ) - ); - posm.modifyLiquidities(calls, block.timestamp + 1); - } +// vm.prank(alice); +// vm.expectRevert( +// abi.encodeWithSelector( +// CustomRevert.WrappedError.selector, +// address(permissionedHooks), +// IHooks.beforeAddLiquidity.selector, +// abi.encodeWithSelector(Unauthorized.selector), +// abi.encodeWithSelector(HookCallFailed.selector) +// ) +// ); +// posm.modifyLiquidities(calls, block.timestamp + 1); +// } - function test_permissioned_mint_disallowed_user_reverts() public { - _test_permissioned_mint_disallowed_user_reverts(key0); - _test_permissioned_mint_disallowed_user_reverts(key1); - _test_permissioned_mint_disallowed_user_reverts(key2); - } +// function test_permissioned_mint_disallowed_user_reverts() public { +// _test_permissioned_mint_disallowed_user_reverts(key0); +// _test_permissioned_mint_disallowed_user_reverts(key1); +// _test_permissioned_mint_disallowed_user_reverts(key2); +// } - function _test_permissioned_mint_disallowed_user_reverts(PoolKey memory key) internal { - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); +// function _test_permissioned_mint_disallowed_user_reverts(PoolKey memory key) internal { +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - - // Add some tokens to unauthorized user - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency0)).mint(unauthorizedUser, 1000e18); - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); - MockERC20(Currency.unwrap(currency1)).mint(unauthorizedUser, 1000e18); +// uint256 liquidity = 1e18; + +// // Add some tokens to unauthorized user +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.ALL_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency0)).mint(unauthorizedUser, 1000e18); +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(unauthorizedUser, PermissionFlags.NONE); +// MockERC20(Currency.unwrap(currency1)).mint(unauthorizedUser, 1000e18); - vm.startPrank(unauthorizedUser); +// vm.startPrank(unauthorizedUser); - // Approve tokens for the position manager - originalToken0.approve(address(permit2), type(uint256).max); - MockERC20(Currency.unwrap(currency1)).approve(address(permit2), type(uint256).max); - permit2.approve(address(originalToken0), address(lpm), type(uint160).max, type(uint48).max); - permit2.approve(Currency.unwrap(currency1), address(lpm), type(uint160).max, type(uint48).max); +// // Approve tokens for the position manager +// originalToken0.approve(address(permit2), type(uint256).max); +// MockERC20(Currency.unwrap(currency1)).approve(address(permit2), type(uint256).max); +// permit2.approve(address(originalToken0), address(lpm), type(uint160).max, type(uint48).max); +// permit2.approve(Currency.unwrap(currency1), address(lpm), type(uint160).max, type(uint48).max); - // This should revert because the recipient is not in the allowlist - vm.expectRevert(Unauthorized.selector); - mint(config, liquidity, unauthorizedUser, ZERO_BYTES); - vm.stopPrank(); - } +// // This should revert because the recipient is not in the allowlist +// vm.expectRevert(Unauthorized.selector); +// mint(config, liquidity, unauthorizedUser, ZERO_BYTES); +// vm.stopPrank(); +// } - function test_permissioned_mint_increase_allowed_user() public { - _test_permissioned_mint_increase_allowed_user(key0); - _test_permissioned_mint_increase_allowed_user(key1); - _test_permissioned_mint_increase_allowed_user(key2); - } +// function test_permissioned_mint_increase_allowed_user() public { +// _test_permissioned_mint_increase_allowed_user(key0); +// _test_permissioned_mint_increase_allowed_user(key1); +// _test_permissioned_mint_increase_allowed_user(key2); +// } - function _test_permissioned_mint_increase_allowed_user(PoolKey memory key) internal { - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); +// function _test_permissioned_mint_increase_allowed_user(PoolKey memory key) internal { +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - uint256 tokenId = lpm.nextTokenId(); +// uint256 liquidity = 1e18; +// uint256 tokenId = lpm.nextTokenId(); - vm.startPrank(alice); +// vm.startPrank(alice); - // add initial liquidity - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// // add initial liquidity +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - tokenId = lpm.nextTokenId(); +// tokenId = lpm.nextTokenId(); - // Increase liquidity - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - vm.stopPrank(); +// // Increase liquidity +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// vm.stopPrank(); - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - } +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// } - function test_permissioned_mint_increase_disallowed_user_reverts() public { - _test_permissioned_mint_increase_disallowed_user_reverts(key0); - _test_permissioned_mint_increase_disallowed_user_reverts(key1); - _test_permissioned_mint_increase_disallowed_user_reverts(key2); - } - - function _test_permissioned_mint_increase_disallowed_user_reverts(PoolKey memory key) internal { - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); - - uint256 liquidity = 1e17; - uint256 tokenId = lpm.nextTokenId(); +// function test_permissioned_mint_increase_disallowed_user_reverts() public { +// _test_permissioned_mint_increase_disallowed_user_reverts(key0); +// _test_permissioned_mint_increase_disallowed_user_reverts(key1); +// _test_permissioned_mint_increase_disallowed_user_reverts(key2); +// } + +// function _test_permissioned_mint_increase_disallowed_user_reverts(PoolKey memory key) internal { +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + +// uint256 liquidity = 1e17; +// uint256 tokenId = lpm.nextTokenId(); - vm.startPrank(alice); +// vm.startPrank(alice); - // add initial liquidity - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// // add initial liquidity +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.NONE); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.NONE); +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.NONE); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.NONE); - tokenId = lpm.nextTokenId(); - - // Increasing liquidity should revert because the recipient is no longer in the allowlist - vm.expectRevert(Unauthorized.selector); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - vm.stopPrank(); - - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); - } - - function test_unpermissioned_sided_mint_disallowed_user_reverts() public { - _test_unpermissioned_sided_mint_disallowed_user_reverts(key0, false); - _test_unpermissioned_sided_mint_disallowed_user_reverts(key1, true); - // key2 has no unpermissioned side - } +// tokenId = lpm.nextTokenId(); + +// // Increasing liquidity should revert because the recipient is no longer in the allowlist +// vm.expectRevert(Unauthorized.selector); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// vm.stopPrank(); + +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); +// } + +// function test_unpermissioned_sided_mint_disallowed_user_reverts() public { +// _test_unpermissioned_sided_mint_disallowed_user_reverts(key0, false); +// _test_unpermissioned_sided_mint_disallowed_user_reverts(key1, true); +// // key2 has no unpermissioned side +// } - function _test_unpermissioned_sided_mint_disallowed_user_reverts(PoolKey memory key, bool useZero) internal { - PositionConfig memory config; - Currency currencyUnpermissioned; +// function _test_unpermissioned_sided_mint_disallowed_user_reverts(PoolKey memory key, bool useZero) internal { +// PositionConfig memory config; +// Currency currencyUnpermissioned; - uint256 liquidity = 1e18; +// uint256 liquidity = 1e18; - if (useZero) { - currencyUnpermissioned = getPermissionedCurrency(key.currency0); - config = PositionConfig({poolKey: key, tickLower: -180, tickUpper: -60}); - } else { - currencyUnpermissioned = getPermissionedCurrency(key.currency1); - config = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 180}); - } +// if (useZero) { +// currencyUnpermissioned = getPermissionedCurrency(key.currency0); +// config = PositionConfig({poolKey: key, tickLower: -180, tickUpper: -60}); +// } else { +// currencyUnpermissioned = getPermissionedCurrency(key.currency1); +// config = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 180}); +// } - // Add some tokens to unauthorized user - MockERC20(Currency.unwrap(currencyUnpermissioned)).mint(unauthorizedUser, 1000e18); +// // Add some tokens to unauthorized user +// MockERC20(Currency.unwrap(currencyUnpermissioned)).mint(unauthorizedUser, 1000e18); - vm.startPrank(unauthorizedUser); +// vm.startPrank(unauthorizedUser); - // Approve tokens for the position manager and permit2 - MockERC20(Currency.unwrap(currencyUnpermissioned)).approve(address(permit2), type(uint256).max); - permit2.approve(Currency.unwrap(currencyUnpermissioned), address(lpm), type(uint160).max, type(uint48).max); +// // Approve tokens for the position manager and permit2 +// MockERC20(Currency.unwrap(currencyUnpermissioned)).approve(address(permit2), type(uint256).max); +// permit2.approve(Currency.unwrap(currencyUnpermissioned), address(lpm), type(uint160).max, type(uint48).max); - // This should revert because the recipient is not in the allowlist - vm.expectRevert(Unauthorized.selector); - mint(config, liquidity, unauthorizedUser, ZERO_BYTES); - vm.stopPrank(); - } +// // This should revert because the recipient is not in the allowlist +// vm.expectRevert(Unauthorized.selector); +// mint(config, liquidity, unauthorizedUser, ZERO_BYTES); +// vm.stopPrank(); +// } - function test_unpermissioned_sided_mint_allowed_user() public { - _test_unpermissioned_sided_mint_allowed_user(key0, false); - _test_unpermissioned_sided_mint_allowed_user(key1, true); - // key2 has no unpermissioned side - } +// function test_unpermissioned_sided_mint_allowed_user() public { +// _test_unpermissioned_sided_mint_allowed_user(key0, false); +// _test_unpermissioned_sided_mint_allowed_user(key1, true); +// // key2 has no unpermissioned side +// } - function _test_unpermissioned_sided_mint_allowed_user(PoolKey memory key, bool useZero) internal { - PositionConfig memory config; - Currency currencyPermissioned; +// function _test_unpermissioned_sided_mint_allowed_user(PoolKey memory key, bool useZero) internal { +// PositionConfig memory config; +// Currency currencyPermissioned; - uint256 liquidity = 1e18; - uint256 tokenId = lpm.nextTokenId(); +// uint256 liquidity = 1e18; +// uint256 tokenId = lpm.nextTokenId(); - if (useZero) { - currencyPermissioned = getPermissionedCurrency(key.currency0); - config = PositionConfig({poolKey: key, tickLower: -180, tickUpper: -60}); - } else { - currencyPermissioned = getPermissionedCurrency(key.currency1); - config = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 180}); - } +// if (useZero) { +// currencyPermissioned = getPermissionedCurrency(key.currency0); +// config = PositionConfig({poolKey: key, tickLower: -180, tickUpper: -60}); +// } else { +// currencyPermissioned = getPermissionedCurrency(key.currency1); +// config = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 180}); +// } - vm.startPrank(alice); +// vm.startPrank(alice); - // Approve tokens for the position manager and permit2 - MockERC20(Currency.unwrap(currencyPermissioned)).approve(address(permit2), type(uint256).max); - permit2.approve(Currency.unwrap(currencyPermissioned), address(lpm), type(uint160).max, type(uint48).max); - - mint(config, liquidity, alice, ZERO_BYTES); - vm.stopPrank(); - - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - } - - function test_permissioned_sided_mint_disallowed_user_reverts() public { - _test_permissioned_sided_mint_disallowed_user_reverts(key0, true); - _test_permissioned_sided_mint_disallowed_user_reverts(key1, false); - _test_permissioned_sided_mint_disallowed_user_reverts(key2, true); - } - - function _test_permissioned_sided_mint_disallowed_user_reverts(PoolKey memory key, bool useZero) internal { - PositionConfig memory config; - Currency currencyPermissioned; - - uint256 liquidity = 1e18; - - if (useZero) { - currencyPermissioned = getPermissionedCurrency(key.currency0); - config = PositionConfig({poolKey: key, tickLower: -180, tickUpper: -60}); - } else { - currencyPermissioned = getPermissionedCurrency(key.currency1); - config = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 180}); - } - - // Add some tokens to unauthorized user - MockPermissionedToken(Currency.unwrap(currencyPermissioned)) - .setAllowlist(unauthorizedUser, PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currencyPermissioned)).mint(unauthorizedUser, 1000e18); - MockPermissionedToken(Currency.unwrap(currencyPermissioned)) - .setAllowlist(unauthorizedUser, PermissionFlags.NONE); - - vm.startPrank(unauthorizedUser); - - // Approve tokens for the position manager and permit2 - MockERC20(Currency.unwrap(currencyPermissioned)).approve(address(permit2), type(uint256).max); - permit2.approve(Currency.unwrap(currencyPermissioned), address(lpm), type(uint160).max, type(uint48).max); - - // This should revert because the recipient is not in the allowlist - vm.expectRevert(Unauthorized.selector); - mint(config, liquidity, unauthorizedUser, ZERO_BYTES); - vm.stopPrank(); - } - - function test_permissioned_single_sided_mint_allowed_user() public { - _test_permissioned_sided_mint_allowed_user(key0, true); - _test_permissioned_sided_mint_allowed_user(key1, false); - _test_permissioned_sided_mint_allowed_user(key2, true); - } - - function _test_permissioned_sided_mint_allowed_user(PoolKey memory key, bool useZero) internal { - PositionConfig memory config; - - uint256 liquidity = 1e18; - uint256 tokenId = lpm.nextTokenId(); - - if (useZero) { - config = PositionConfig({poolKey: key, tickLower: -180, tickUpper: -60}); - } else { - config = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 180}); - } - - vm.prank(alice); - mint(config, liquidity, alice, ZERO_BYTES); - - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - } - - function test_fuzz_burn_emptyPosition(ModifyLiquidityParams memory params) public { - _test_fuzz_burn_emptyPosition(key0, params); - _test_fuzz_burn_emptyPosition(key1, params); - _test_fuzz_burn_emptyPosition(key2, params); - } - - function _test_fuzz_burn_emptyPosition(PoolKey memory key, ModifyLiquidityParams memory params) internal { - uint256 balance0Start = key.currency0.balanceOfSelf(); - uint256 balance1Start = key.currency1.balanceOfSelf(); - uint256 tokenId; - - // create liquidity we can burn - (tokenId, params) = addFuzzyLiquidity(lpm, ActionConstants.MSG_SENDER, key, params, SQRT_PRICE_1_1, ZERO_BYTES); - PositionConfig memory config = - PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - assertEq(tokenId, lpm.nextTokenId() - 1); - assertEq(IERC721(address(lpm)).ownerOf(1), address(this)); - - uint256 liquidity = lpm.getPositionLiquidity(tokenId); - - assertEq(liquidity, uint256(params.liquidityDelta)); - - uint256 balance0BeforeBurn = getPermissionedCurrency(key.currency0).balanceOfSelf(); - uint256 balance1BeforeBurn = getPermissionedCurrency(key.currency1).balanceOfSelf(); - uint256 balance0ManagerBefore = (key.currency0).balanceOf(address(manager)); - uint256 balance1ManagerBefore = (key.currency1).balanceOf(address(manager)); - - // burn liquidity - decreaseLiquidity(tokenId, config, liquidity, ZERO_BYTES); - - uint256 balance0ManagerAfter = (key.currency0).balanceOf(address(manager)); - uint256 balance1ManagerAfter = (key.currency1).balanceOf(address(manager)); - - liquidity = lpm.getPositionLiquidity(tokenId); - - assertEq(liquidity, 0); - assertEq( - getPermissionedCurrency(key.currency0).balanceOfSelf(), - balance0BeforeBurn + balance0ManagerBefore - balance0ManagerAfter - ); - assertEq( - getPermissionedCurrency(key.currency1).balanceOfSelf(), - balance1BeforeBurn + balance1ManagerBefore - balance1ManagerAfter - ); - - IERC721(address(lpm)).ownerOf(lpm.nextTokenId() - 1); - - // see note from position manager test: "no tokens were lost ... fuzzer showing off by 1 sometimes" - assertApproxEqAbs(key.currency0.balanceOfSelf(), balance0Start, 1 wei); - assertApproxEqAbs(key.currency1.balanceOfSelf(), balance1Start, 1 wei); - } - - function test_initialize() public { - _test_initialize(currency0, currency1); - _test_initialize(Currency.wrap(address(permissionsAdapter0)), currency1); - _test_initialize(currency1, Currency.wrap(address(permissionsAdapter2))); - _test_initialize(Currency.wrap(address(permissionsAdapter0)), Currency.wrap(address(permissionsAdapter2))); - } - - function _test_initialize(Currency currency0_, Currency currency1_) internal { - // initialize a new pool - PoolKey memory key = PoolKey({ - currency0: currency0_, currency1: currency1_, fee: 0, tickSpacing: 100, hooks: IHooks(address(0)) - }); - - lpm.initializePool(key, SQRT_PRICE_1_1); - - (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId()); - - assertEq(sqrtPriceX96, SQRT_PRICE_1_1); - assertEq(tick, 0); - assertEq(protocolFee, 0); - assertEq(lpFee, key.fee); - } - - function test_fuzz_initialize(uint160 sqrtPrice, uint24 fee) public { - _test_fuzz_initialize(currency0, currency1, sqrtPrice, fee); - _test_fuzz_initialize(Currency.wrap(address(permissionsAdapter0)), currency1, sqrtPrice, fee); - _test_fuzz_initialize(currency1, Currency.wrap(address(permissionsAdapter2)), sqrtPrice, fee); - _test_fuzz_initialize( - Currency.wrap(address(permissionsAdapter0)), Currency.wrap(address(permissionsAdapter2)), sqrtPrice, fee - ); - } - - function _test_fuzz_initialize(Currency currency0_, Currency currency1_, uint160 sqrtPrice, uint24 fee) internal { - sqrtPrice = - uint160(bound(sqrtPrice, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE_MINUS_MIN_SQRT_PRICE_MINUS_ONE)); - fee = uint24(bound(fee, 0, LPFeeLibrary.MAX_LP_FEE)); - PoolKey memory key = PoolKey({ - currency0: currency0_, currency1: currency1_, fee: fee, tickSpacing: 10, hooks: IHooks(address(0)) - }); - - lpm.initializePool(key, sqrtPrice); - - (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId()); - - assertEq(sqrtPriceX96, sqrtPrice); - assertEq(tick, TickMath.getTickAtSqrtPrice(sqrtPrice)); - assertEq(protocolFee, 0); - assertEq(lpFee, fee); - } - - event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); - - function test_transferFrom_success_seize_by_admin() public { - _testRevert_transferFrom_success_seize_by_admin(key0); - _testRevert_transferFrom_success_seize_by_admin(key1); - } - - function _testRevert_transferFrom_success_seize_by_admin(PoolKey memory poolKey) internal { - uint256 tokenId = lpm.nextTokenId(); - _test_permissioned_mint_allowed_user(poolKey); - - vm.expectEmit(true, true, true, true); - emit Transfer(alice, address(this), tokenId); - // address(this) is admin - IERC721(address(lpm)).transferFrom(alice, address(this), tokenId); - } - - function test_transferFrom_success_seize_by_either_admin() public { - uint256 tokenId0 = lpm.nextTokenId(); - _test_permissioned_mint_allowed_user(key2); - uint256 tokenId1 = lpm.nextTokenId(); - _test_permissioned_mint_allowed_user(key2); - uint256 tokenId2 = lpm.nextTokenId(); - _test_permissioned_mint_allowed_user(key2); - - vm.expectEmit(true, true, true, true); - emit Transfer(alice, address(this), tokenId0); - // address(this) is admin of both permissioned tokens - IERC721(address(lpm)).transferFrom(alice, address(this), tokenId0); - - address owner0 = makeAddr("owner0"); - address owner1 = makeAddr("owner1"); - permissionsAdapter0.transferOwnership(owner0); - vm.prank(owner0); - permissionsAdapter0.acceptOwnership(); - permissionsAdapter2.transferOwnership(owner1); - vm.prank(owner1); - permissionsAdapter2.acceptOwnership(); - - vm.prank(owner0); - vm.expectEmit(true, true, true, true); - emit Transfer(alice, address(this), tokenId1); - // owner0 is admin of permissionsAdapter0 - IERC721(address(lpm)).transferFrom(alice, address(this), tokenId1); - - vm.prank(owner1); - vm.expectEmit(true, true, true, true); - emit Transfer(alice, address(this), tokenId2); - // owner1 is admin of permissionsAdapter2 - IERC721(address(lpm)).transferFrom(alice, address(this), tokenId2); - } - - function test_transferFrom_revert_not_admin(address notAdmin) public { - vm.assume(notAdmin != address(this) && notAdmin != address(0)); - uint256 tokenId0 = lpm.nextTokenId(); - _test_permissioned_mint_allowed_user(key0); - uint256 tokenId1 = lpm.nextTokenId(); - _test_permissioned_mint_allowed_user(key1); - uint256 tokenId2 = lpm.nextTokenId(); - _test_permissioned_mint_allowed_user(key2); - uint256 tokenId3 = lpm.nextTokenId(); - _test_permissioned_mint_allowed_user(key2); - - vm.prank(notAdmin); - vm.expectRevert(Unauthorized.selector); - IERC721(address(lpm)).transferFrom(alice, address(this), tokenId0); - - vm.prank(notAdmin); - vm.expectRevert(Unauthorized.selector); - IERC721(address(lpm)).transferFrom(alice, address(this), tokenId1); - - vm.prank(notAdmin); - vm.expectRevert(Unauthorized.selector); - IERC721(address(lpm)).transferFrom(alice, address(this), tokenId2); - - vm.prank(notAdmin); - vm.expectRevert(Unauthorized.selector); - IERC721(address(lpm)).transferFrom(alice, address(this), tokenId3); - } - - error SafeTransferDisabled(); - - function test_safe_transfer_from_reverts() public { - bytes memory encodedCall = - abi.encodeWithSignature("safeTransferFrom(address,address,uint256)", alice, address(this), 1); - - address target = address(lpm); - - vm.startPrank(alice); - (bool success, bytes memory data) = address(target).call(encodedCall); - - assertEq(success, false); - assertEq(bytes4(data), SafeTransferDisabled.selector); - vm.stopPrank(); - } - - function test_safe_transfer_from_with_bytes_reverts() public { - bytes memory encodedCall = - abi.encodeWithSignature("safeTransferFrom(address,address,uint256,bytes)", alice, address(this), 1, "abcd"); - address target = address(lpm); - - vm.startPrank(alice); - (bool success, bytes memory data) = address(target).call(encodedCall); - - assertEq(success, false); - assertEq(bytes4(data), SafeTransferDisabled.selector); - vm.stopPrank(); - } - - function test_mint_from_contract_balance() public { - _test_mint_from_contract_balance(key0); - _test_mint_from_contract_balance(key1); - _test_mint_from_contract_balance(key2); - } - - /// @dev This function had to be split up and refactored to avoid stack-too-deep errors - function _test_mint_from_contract_balance(PoolKey memory key) internal { - uint256 amount0ToTransfer = 100e18; - uint256 amount1ToTransfer = 100e18; - - // Setup and transfer tokens - setupContractBalance(key, amount0ToTransfer, amount1ToTransfer); - - // Get balances before minting - BalanceInfo memory balanceInfo = getBalanceInfo(key); - - // Create and execute mint plan - bytes memory calls = createMintPlan(key, amount0ToTransfer, amount1ToTransfer); - uint256 tokenId = lpm.nextTokenId(); - lpm.modifyLiquidities(calls, _deadline); - - // Verify results - verifyMintResults(key, balanceInfo, tokenId); - } - - function test_mint_from_contract_balance_disallowed_revert() public { - _test_mint_from_contract_balance_disallowed_revert(keyFake0); - _test_mint_from_contract_balance_disallowed_revert(keyFake1); - _test_mint_from_contract_balance_disallowed_revert(keyFake2); - } - - function _test_mint_from_contract_balance_disallowed_revert(PoolKey memory key) internal { - uint256 amount0ToTransfer = 1e18; - uint256 amount1ToTransfer = 1e18; - - Currency currency0_ = getPermissionedCurrency(key.currency0); - Currency currency1_ = getPermissionedCurrency(key.currency1); - - uint256 balance0Before = currency0_.balanceOf(address(secondaryPosm)); - uint256 balance1Before = currency1_.balanceOf(address(secondaryPosm)); - - // Transfer tokens to the contract - currency0_.transfer(address(secondaryPosm), amount0ToTransfer); - currency1_.transfer(address(secondaryPosm), amount1ToTransfer); - - // Calculate liquidity for the desired amounts - int24 tickLower = -int24(key.tickSpacing); - int24 tickUpper = int24(key.tickSpacing); - uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( - SQRT_PRICE_1_1, - TickMath.getSqrtPriceAtTick(tickLower), - TickMath.getSqrtPriceAtTick(tickUpper), - amount0ToTransfer, - amount1ToTransfer - ); - - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); - - // Verify the contract has the tokens - assertEq(currency0_.balanceOf(address(secondaryPosm)), amount0ToTransfer + balance0Before); - assertEq(currency1_.balanceOf(address(secondaryPosm)), amount1ToTransfer + balance1Before); - - // Create a plan that uses the contract's balance instead of the caller's - Plan memory planner = Planner.init(); - planner.add( - Actions.MINT_POSITION, - abi.encode( - config.poolKey, - config.tickLower, - config.tickUpper, - liquidityToAdd, - MAX_SLIPPAGE_INCREASE, - MAX_SLIPPAGE_INCREASE, - address(this), // recipient - ZERO_BYTES // hookData - ) - ); - - // Add actions to settle from the contract's balance - planner.add(Actions.SETTLE, abi.encode(key.currency0, ActionConstants.OPEN_DELTA, false)); // false = payer is contract - planner.add(Actions.SETTLE, abi.encode(key.currency1, ActionConstants.OPEN_DELTA, false)); // false = payer is contract - - bytes memory calls = planner.finalizeModifyLiquidityWithClose(config.poolKey); - - vm.prank(unauthorizedUser); - vm.expectRevert(); - secondaryPosm.modifyLiquidities(calls, _deadline); - } - - function test_mint_from_contract_balance_uses_contract_balance_settle() public { - _test_mint_from_contract_balance_uses_contract_balance_settle(key0); - _test_mint_from_contract_balance_uses_contract_balance_settle(key1); - _test_mint_from_contract_balance_uses_contract_balance_settle(key2); - } - - function _test_mint_from_contract_balance_uses_contract_balance_settle(PoolKey memory key) internal { - uint256 amount0ToTransfer = 100e18; - uint256 amount1ToTransfer = 100e18; - - // Transfer underlying permissioned tokens to the POSM - setupContractBalance(key, amount0ToTransfer, amount1ToTransfer); - - BalanceInfo memory balanceInfo = getBalanceInfo(key); - - // Create a mint plan that uses CONTRACT_BALANCE instead of OPEN_DELTA - int24 tickLower = -int24(key.tickSpacing); - int24 tickUpper = int24(key.tickSpacing); - uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( - SQRT_PRICE_1_1, - TickMath.getSqrtPriceAtTick(tickLower), - TickMath.getSqrtPriceAtTick(tickUpper), - amount0ToTransfer, - amount1ToTransfer - ); - - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); - - Plan memory planner = Planner.init(); - planner.add( - Actions.MINT_POSITION, - abi.encode( - config.poolKey, - config.tickLower, - config.tickUpper, - liquidityToAdd, - MAX_SLIPPAGE_INCREASE, - MAX_SLIPPAGE_INCREASE, - address(this), - ZERO_BYTES - ) - ); - - // Use CONTRACT_BALANCE instead of OPEN_DELTA — this is the broken path - planner.add(Actions.SETTLE, abi.encode(key.currency0, ActionConstants.CONTRACT_BALANCE, false)); - planner.add(Actions.SETTLE, abi.encode(key.currency1, ActionConstants.CONTRACT_BALANCE, false)); - - bytes memory calls = planner.finalizeModifyLiquidityWithClose(config.poolKey); - uint256 tokenId = lpm.nextTokenId(); - lpm.modifyLiquidities(calls, _deadline); - - verifyMintResults(key, balanceInfo, tokenId); - } - - // ===== PERMISSION FLAG TESTS ===== - - function test_permission_flag_none() public { - // Test that NONE permissions prevent all operations - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.NONE); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.NONE); - - // Should revert when trying to mint with NONE permissions - PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - - vm.prank(alice); - vm.expectRevert(); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - } - - function test_permission_flag_swap_allowed() public { - // Test that SWAP_ALLOWED only allows swaps, not liquidity operations - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); - - // Should revert when trying to mint (liquidity operation) with only SWAP_ALLOWED - PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - - vm.prank(alice); - vm.expectRevert(); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - } - - function test_permission_flag_liquidity_allowed() public { - // Test that LIQUIDITY_ALLOWED allows liquidity operations - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); - - // Should succeed when trying to mint with LIQUIDITY_ALLOWED - PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - uint256 tokenId = lpm.nextTokenId(); - - vm.prank(alice); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - } - - function test_permission_flag_all_allowed() public { - // Test that ALL_ALLOWED allows all operations - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); - - // Should succeed when trying to mint with ALL_ALLOWED - PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - uint256 tokenId = lpm.nextTokenId(); - - vm.prank(alice); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - } - - function test_permission_flag_combinations() public { - // Test various combinations of permissions - PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - - // Test SWAP_ALLOWED + LIQUIDITY_ALLOWED (should work like ALL_ALLOWED) - MockPermissionedToken(Currency.unwrap(currency0)) - .setAllowlist(alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED)); - MockPermissionedToken(Currency.unwrap(currency2)) - .setAllowlist(alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED)); - - uint256 tokenId = lpm.nextTokenId(); - vm.prank(alice); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - } - - function test_permission_flag_partial_permissions() public { - // Test that having permissions on only one token is enough - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.NONE); - - PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - - vm.prank(alice); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - } - - function test_permission_flag_dynamic_changes() public { - // Test that permission changes take effect immediately - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); - - PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - - // Should succeed initially - uint256 tokenId = lpm.nextTokenId(); - vm.prank(alice); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - - // Remove permissions - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.NONE); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.NONE); - - // Should fail on subsequent operations - tokenId = lpm.nextTokenId(); - vm.prank(alice); - vm.expectRevert(); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - } - - function test_permission_flag_edge_cases() public { - // Test edge cases with permission flags - PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - - // Test with zero permissions (should be same as NONE) - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlag.wrap(0x0000)); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlag.wrap(0x0000)); - - vm.prank(alice); - vm.expectRevert(); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - - // Test with maximum permissions (should be same as ALL_ALLOWED) - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlag.wrap(0xFFFF)); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlag.wrap(0xFFFF)); - - uint256 tokenId = lpm.nextTokenId(); - vm.prank(alice); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - } - - function test_permission_flag_all_pools() public { - // Test permission flags work across all pool types - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); - - PositionConfig memory config0 = PositionConfig({poolKey: key0, tickLower: -120, tickUpper: 120}); - PositionConfig memory config1 = PositionConfig({poolKey: key1, tickLower: -120, tickUpper: 120}); - PositionConfig memory config2 = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - - // Test permissioned/normal pool - uint256 tokenId0 = lpm.nextTokenId(); - vm.prank(alice); - mint(config0, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - assertEq(IERC721(address(lpm)).ownerOf(tokenId0), alice); - - // Test normal/permissioned pool - uint256 tokenId1 = lpm.nextTokenId(); - vm.prank(alice); - mint(config1, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - assertEq(IERC721(address(lpm)).ownerOf(tokenId1), alice); - - // Test permissioned/permissioned pool - uint256 tokenId2 = lpm.nextTokenId(); - vm.prank(alice); - mint(config2, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - assertEq(IERC721(address(lpm)).ownerOf(tokenId2), alice); - } - - function test_permission_flag_swap_allowed_unauthorized_reverts() public { - // Test that SWAP_ALLOWED does not allow liquidity operations - MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); - MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); - - // Should revert when trying to mint with SWAP_ALLOWED - PositionConfig memory config = PositionConfig({poolKey: insecureKey, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - - vm.startPrank(alice); - vm.expectRevert(Unauthorized.selector); - mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); - vm.stopPrank(); - } - - function test_permissioned_mint_to_unauthorized_recipient_reverts() public { - // Caller (address(this)) is allowed, but unauthorizedUser is not - // The hook will pass because the caller is authorized, - // but _mint should revert because the recipient lacks LIQUIDITY_ALLOWED - PositionConfig memory config = PositionConfig({poolKey: key0, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - - vm.expectRevert(Unauthorized.selector); - mint(config, liquidity, unauthorizedUser, ZERO_BYTES); - } - - function test_permissioned_mint_to_authorized_recipient_succeeds() public { - // Caller (address(this)) is allowed, alice is also allowed - // Minting to an authorized recipient should succeed - PositionConfig memory config = PositionConfig({poolKey: key0, tickLower: -120, tickUpper: 120}); - uint256 liquidity = 1e18; - uint256 tokenId = lpm.nextTokenId(); - - mint(config, liquidity, alice, ZERO_BYTES); - assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); - } -} +// // Approve tokens for the position manager and permit2 +// MockERC20(Currency.unwrap(currencyPermissioned)).approve(address(permit2), type(uint256).max); +// permit2.approve(Currency.unwrap(currencyPermissioned), address(lpm), type(uint160).max, type(uint48).max); + +// mint(config, liquidity, alice, ZERO_BYTES); +// vm.stopPrank(); + +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// } + +// function test_permissioned_sided_mint_disallowed_user_reverts() public { +// _test_permissioned_sided_mint_disallowed_user_reverts(key0, true); +// _test_permissioned_sided_mint_disallowed_user_reverts(key1, false); +// _test_permissioned_sided_mint_disallowed_user_reverts(key2, true); +// } + +// function _test_permissioned_sided_mint_disallowed_user_reverts(PoolKey memory key, bool useZero) internal { +// PositionConfig memory config; +// Currency currencyPermissioned; + +// uint256 liquidity = 1e18; + +// if (useZero) { +// currencyPermissioned = getPermissionedCurrency(key.currency0); +// config = PositionConfig({poolKey: key, tickLower: -180, tickUpper: -60}); +// } else { +// currencyPermissioned = getPermissionedCurrency(key.currency1); +// config = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 180}); +// } + +// // Add some tokens to unauthorized user +// MockPermissionedToken(Currency.unwrap(currencyPermissioned)) +// .setAllowlist(unauthorizedUser, PermissionFlags.ALL_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currencyPermissioned)).mint(unauthorizedUser, 1000e18); +// MockPermissionedToken(Currency.unwrap(currencyPermissioned)) +// .setAllowlist(unauthorizedUser, PermissionFlags.NONE); + +// vm.startPrank(unauthorizedUser); + +// // Approve tokens for the position manager and permit2 +// MockERC20(Currency.unwrap(currencyPermissioned)).approve(address(permit2), type(uint256).max); +// permit2.approve(Currency.unwrap(currencyPermissioned), address(lpm), type(uint160).max, type(uint48).max); + +// // This should revert because the recipient is not in the allowlist +// vm.expectRevert(Unauthorized.selector); +// mint(config, liquidity, unauthorizedUser, ZERO_BYTES); +// vm.stopPrank(); +// } + +// function test_permissioned_single_sided_mint_allowed_user() public { +// _test_permissioned_sided_mint_allowed_user(key0, true); +// _test_permissioned_sided_mint_allowed_user(key1, false); +// _test_permissioned_sided_mint_allowed_user(key2, true); +// } + +// function _test_permissioned_sided_mint_allowed_user(PoolKey memory key, bool useZero) internal { +// PositionConfig memory config; + +// uint256 liquidity = 1e18; +// uint256 tokenId = lpm.nextTokenId(); + +// if (useZero) { +// config = PositionConfig({poolKey: key, tickLower: -180, tickUpper: -60}); +// } else { +// config = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 180}); +// } + +// vm.prank(alice); +// mint(config, liquidity, alice, ZERO_BYTES); + +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// } + +// function test_fuzz_burn_emptyPosition(ModifyLiquidityParams memory params) public { +// _test_fuzz_burn_emptyPosition(key0, params); +// _test_fuzz_burn_emptyPosition(key1, params); +// _test_fuzz_burn_emptyPosition(key2, params); +// } + +// function _test_fuzz_burn_emptyPosition(PoolKey memory key, ModifyLiquidityParams memory params) internal { +// uint256 balance0Start = key.currency0.balanceOfSelf(); +// uint256 balance1Start = key.currency1.balanceOfSelf(); +// uint256 tokenId; + +// // create liquidity we can burn +// (tokenId, params) = addFuzzyLiquidity(lpm, ActionConstants.MSG_SENDER, key, params, SQRT_PRICE_1_1, ZERO_BYTES); +// PositionConfig memory config = +// PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); +// assertEq(tokenId, lpm.nextTokenId() - 1); +// assertEq(IERC721(address(lpm)).ownerOf(1), address(this)); + +// uint256 liquidity = lpm.getPositionLiquidity(tokenId); + +// assertEq(liquidity, uint256(params.liquidityDelta)); + +// uint256 balance0BeforeBurn = getPermissionedCurrency(key.currency0).balanceOfSelf(); +// uint256 balance1BeforeBurn = getPermissionedCurrency(key.currency1).balanceOfSelf(); +// uint256 balance0ManagerBefore = (key.currency0).balanceOf(address(manager)); +// uint256 balance1ManagerBefore = (key.currency1).balanceOf(address(manager)); + +// // burn liquidity +// decreaseLiquidity(tokenId, config, liquidity, ZERO_BYTES); + +// uint256 balance0ManagerAfter = (key.currency0).balanceOf(address(manager)); +// uint256 balance1ManagerAfter = (key.currency1).balanceOf(address(manager)); + +// liquidity = lpm.getPositionLiquidity(tokenId); + +// assertEq(liquidity, 0); +// assertEq( +// getPermissionedCurrency(key.currency0).balanceOfSelf(), +// balance0BeforeBurn + balance0ManagerBefore - balance0ManagerAfter +// ); +// assertEq( +// getPermissionedCurrency(key.currency1).balanceOfSelf(), +// balance1BeforeBurn + balance1ManagerBefore - balance1ManagerAfter +// ); + +// IERC721(address(lpm)).ownerOf(lpm.nextTokenId() - 1); + +// // see note from position manager test: "no tokens were lost ... fuzzer showing off by 1 sometimes" +// assertApproxEqAbs(key.currency0.balanceOfSelf(), balance0Start, 1 wei); +// assertApproxEqAbs(key.currency1.balanceOfSelf(), balance1Start, 1 wei); +// } + +// function test_initialize() public { +// _test_initialize(currency0, currency1); +// _test_initialize(Currency.wrap(address(permissionsAdapter0)), currency1); +// _test_initialize(currency1, Currency.wrap(address(permissionsAdapter2))); +// _test_initialize(Currency.wrap(address(permissionsAdapter0)), Currency.wrap(address(permissionsAdapter2))); +// } + +// function _test_initialize(Currency currency0_, Currency currency1_) internal { +// // initialize a new pool +// PoolKey memory key = PoolKey({ +// currency0: currency0_, currency1: currency1_, fee: 0, tickSpacing: 100, hooks: IHooks(address(0)) +// }); + +// lpm.initializePool(key, SQRT_PRICE_1_1); + +// (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId()); + +// assertEq(sqrtPriceX96, SQRT_PRICE_1_1); +// assertEq(tick, 0); +// assertEq(protocolFee, 0); +// assertEq(lpFee, key.fee); +// } + +// function test_fuzz_initialize(uint160 sqrtPrice, uint24 fee) public { +// _test_fuzz_initialize(currency0, currency1, sqrtPrice, fee); +// _test_fuzz_initialize(Currency.wrap(address(permissionsAdapter0)), currency1, sqrtPrice, fee); +// _test_fuzz_initialize(currency1, Currency.wrap(address(permissionsAdapter2)), sqrtPrice, fee); +// _test_fuzz_initialize( +// Currency.wrap(address(permissionsAdapter0)), Currency.wrap(address(permissionsAdapter2)), sqrtPrice, fee +// ); +// } + +// function _test_fuzz_initialize(Currency currency0_, Currency currency1_, uint160 sqrtPrice, uint24 fee) internal { +// sqrtPrice = +// uint160(bound(sqrtPrice, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE_MINUS_MIN_SQRT_PRICE_MINUS_ONE)); +// fee = uint24(bound(fee, 0, LPFeeLibrary.MAX_LP_FEE)); +// PoolKey memory key = PoolKey({ +// currency0: currency0_, currency1: currency1_, fee: fee, tickSpacing: 10, hooks: IHooks(address(0)) +// }); + +// lpm.initializePool(key, sqrtPrice); + +// (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId()); + +// assertEq(sqrtPriceX96, sqrtPrice); +// assertEq(tick, TickMath.getTickAtSqrtPrice(sqrtPrice)); +// assertEq(protocolFee, 0); +// assertEq(lpFee, fee); +// } + +// event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + +// function test_transferFrom_success_seize_by_admin() public { +// _testRevert_transferFrom_success_seize_by_admin(key0); +// _testRevert_transferFrom_success_seize_by_admin(key1); +// } + +// function _testRevert_transferFrom_success_seize_by_admin(PoolKey memory poolKey) internal { +// uint256 tokenId = lpm.nextTokenId(); +// _test_permissioned_mint_allowed_user(poolKey); + +// vm.expectEmit(true, true, true, true); +// emit Transfer(alice, address(this), tokenId); +// // address(this) is admin +// IERC721(address(lpm)).transferFrom(alice, address(this), tokenId); +// } + +// function test_transferFrom_success_seize_by_either_admin() public { +// uint256 tokenId0 = lpm.nextTokenId(); +// _test_permissioned_mint_allowed_user(key2); +// uint256 tokenId1 = lpm.nextTokenId(); +// _test_permissioned_mint_allowed_user(key2); +// uint256 tokenId2 = lpm.nextTokenId(); +// _test_permissioned_mint_allowed_user(key2); + +// vm.expectEmit(true, true, true, true); +// emit Transfer(alice, address(this), tokenId0); +// // address(this) is admin of both permissioned tokens +// IERC721(address(lpm)).transferFrom(alice, address(this), tokenId0); + +// address owner0 = makeAddr("owner0"); +// address owner1 = makeAddr("owner1"); +// permissionsAdapter0.transferOwnership(owner0); +// vm.prank(owner0); +// permissionsAdapter0.acceptOwnership(); +// permissionsAdapter2.transferOwnership(owner1); +// vm.prank(owner1); +// permissionsAdapter2.acceptOwnership(); + +// vm.prank(owner0); +// vm.expectEmit(true, true, true, true); +// emit Transfer(alice, address(this), tokenId1); +// // owner0 is admin of permissionsAdapter0 +// IERC721(address(lpm)).transferFrom(alice, address(this), tokenId1); + +// vm.prank(owner1); +// vm.expectEmit(true, true, true, true); +// emit Transfer(alice, address(this), tokenId2); +// // owner1 is admin of permissionsAdapter2 +// IERC721(address(lpm)).transferFrom(alice, address(this), tokenId2); +// } + +// function test_transferFrom_revert_not_admin(address notAdmin) public { +// vm.assume(notAdmin != address(this) && notAdmin != address(0)); +// uint256 tokenId0 = lpm.nextTokenId(); +// _test_permissioned_mint_allowed_user(key0); +// uint256 tokenId1 = lpm.nextTokenId(); +// _test_permissioned_mint_allowed_user(key1); +// uint256 tokenId2 = lpm.nextTokenId(); +// _test_permissioned_mint_allowed_user(key2); +// uint256 tokenId3 = lpm.nextTokenId(); +// _test_permissioned_mint_allowed_user(key2); + +// vm.prank(notAdmin); +// vm.expectRevert(Unauthorized.selector); +// IERC721(address(lpm)).transferFrom(alice, address(this), tokenId0); + +// vm.prank(notAdmin); +// vm.expectRevert(Unauthorized.selector); +// IERC721(address(lpm)).transferFrom(alice, address(this), tokenId1); + +// vm.prank(notAdmin); +// vm.expectRevert(Unauthorized.selector); +// IERC721(address(lpm)).transferFrom(alice, address(this), tokenId2); + +// vm.prank(notAdmin); +// vm.expectRevert(Unauthorized.selector); +// IERC721(address(lpm)).transferFrom(alice, address(this), tokenId3); +// } + +// error SafeTransferDisabled(); + +// function test_safe_transfer_from_reverts() public { +// bytes memory encodedCall = +// abi.encodeWithSignature("safeTransferFrom(address,address,uint256)", alice, address(this), 1); + +// address target = address(lpm); + +// vm.startPrank(alice); +// (bool success, bytes memory data) = address(target).call(encodedCall); + +// assertEq(success, false); +// assertEq(bytes4(data), SafeTransferDisabled.selector); +// vm.stopPrank(); +// } + +// function test_safe_transfer_from_with_bytes_reverts() public { +// bytes memory encodedCall = +// abi.encodeWithSignature("safeTransferFrom(address,address,uint256,bytes)", alice, address(this), 1, "abcd"); +// address target = address(lpm); + +// vm.startPrank(alice); +// (bool success, bytes memory data) = address(target).call(encodedCall); + +// assertEq(success, false); +// assertEq(bytes4(data), SafeTransferDisabled.selector); +// vm.stopPrank(); +// } + +// function test_mint_from_contract_balance() public { +// _test_mint_from_contract_balance(key0); +// _test_mint_from_contract_balance(key1); +// _test_mint_from_contract_balance(key2); +// } + +// /// @dev This function had to be split up and refactored to avoid stack-too-deep errors +// function _test_mint_from_contract_balance(PoolKey memory key) internal { +// uint256 amount0ToTransfer = 100e18; +// uint256 amount1ToTransfer = 100e18; + +// // Setup and transfer tokens +// setupContractBalance(key, amount0ToTransfer, amount1ToTransfer); + +// // Get balances before minting +// BalanceInfo memory balanceInfo = getBalanceInfo(key); + +// // Create and execute mint plan +// bytes memory calls = createMintPlan(key, amount0ToTransfer, amount1ToTransfer); +// uint256 tokenId = lpm.nextTokenId(); +// lpm.modifyLiquidities(calls, _deadline); + +// // Verify results +// verifyMintResults(key, balanceInfo, tokenId); +// } + +// function test_mint_from_contract_balance_disallowed_revert() public { +// _test_mint_from_contract_balance_disallowed_revert(keyFake0); +// _test_mint_from_contract_balance_disallowed_revert(keyFake1); +// _test_mint_from_contract_balance_disallowed_revert(keyFake2); +// } + +// function _test_mint_from_contract_balance_disallowed_revert(PoolKey memory key) internal { +// uint256 amount0ToTransfer = 1e18; +// uint256 amount1ToTransfer = 1e18; + +// Currency currency0_ = getPermissionedCurrency(key.currency0); +// Currency currency1_ = getPermissionedCurrency(key.currency1); + +// uint256 balance0Before = currency0_.balanceOf(address(secondaryPosm)); +// uint256 balance1Before = currency1_.balanceOf(address(secondaryPosm)); + +// // Transfer tokens to the contract +// currency0_.transfer(address(secondaryPosm), amount0ToTransfer); +// currency1_.transfer(address(secondaryPosm), amount1ToTransfer); + +// // Calculate liquidity for the desired amounts +// int24 tickLower = -int24(key.tickSpacing); +// int24 tickUpper = int24(key.tickSpacing); +// uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( +// SQRT_PRICE_1_1, +// TickMath.getSqrtPriceAtTick(tickLower), +// TickMath.getSqrtPriceAtTick(tickUpper), +// amount0ToTransfer, +// amount1ToTransfer +// ); + +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + +// // Verify the contract has the tokens +// assertEq(currency0_.balanceOf(address(secondaryPosm)), amount0ToTransfer + balance0Before); +// assertEq(currency1_.balanceOf(address(secondaryPosm)), amount1ToTransfer + balance1Before); + +// // Create a plan that uses the contract's balance instead of the caller's +// Plan memory planner = Planner.init(); +// planner.add( +// Actions.MINT_POSITION, +// abi.encode( +// config.poolKey, +// config.tickLower, +// config.tickUpper, +// liquidityToAdd, +// MAX_SLIPPAGE_INCREASE, +// MAX_SLIPPAGE_INCREASE, +// address(this), // recipient +// ZERO_BYTES // hookData +// ) +// ); + +// // Add actions to settle from the contract's balance +// planner.add(Actions.SETTLE, abi.encode(key.currency0, ActionConstants.OPEN_DELTA, false)); // false = payer is contract +// planner.add(Actions.SETTLE, abi.encode(key.currency1, ActionConstants.OPEN_DELTA, false)); // false = payer is contract + +// bytes memory calls = planner.finalizeModifyLiquidityWithClose(config.poolKey); + +// vm.prank(unauthorizedUser); +// vm.expectRevert(); +// secondaryPosm.modifyLiquidities(calls, _deadline); +// } + +// function test_mint_from_contract_balance_uses_contract_balance_settle() public { +// _test_mint_from_contract_balance_uses_contract_balance_settle(key0); +// _test_mint_from_contract_balance_uses_contract_balance_settle(key1); +// _test_mint_from_contract_balance_uses_contract_balance_settle(key2); +// } + +// function _test_mint_from_contract_balance_uses_contract_balance_settle(PoolKey memory key) internal { +// uint256 amount0ToTransfer = 100e18; +// uint256 amount1ToTransfer = 100e18; + +// // Transfer underlying permissioned tokens to the POSM +// setupContractBalance(key, amount0ToTransfer, amount1ToTransfer); + +// BalanceInfo memory balanceInfo = getBalanceInfo(key); + +// // Create a mint plan that uses CONTRACT_BALANCE instead of OPEN_DELTA +// int24 tickLower = -int24(key.tickSpacing); +// int24 tickUpper = int24(key.tickSpacing); +// uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( +// SQRT_PRICE_1_1, +// TickMath.getSqrtPriceAtTick(tickLower), +// TickMath.getSqrtPriceAtTick(tickUpper), +// amount0ToTransfer, +// amount1ToTransfer +// ); + +// PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper}); + +// Plan memory planner = Planner.init(); +// planner.add( +// Actions.MINT_POSITION, +// abi.encode( +// config.poolKey, +// config.tickLower, +// config.tickUpper, +// liquidityToAdd, +// MAX_SLIPPAGE_INCREASE, +// MAX_SLIPPAGE_INCREASE, +// address(this), +// ZERO_BYTES +// ) +// ); + +// // Use CONTRACT_BALANCE instead of OPEN_DELTA — this is the broken path +// planner.add(Actions.SETTLE, abi.encode(key.currency0, ActionConstants.CONTRACT_BALANCE, false)); +// planner.add(Actions.SETTLE, abi.encode(key.currency1, ActionConstants.CONTRACT_BALANCE, false)); + +// bytes memory calls = planner.finalizeModifyLiquidityWithClose(config.poolKey); +// uint256 tokenId = lpm.nextTokenId(); +// lpm.modifyLiquidities(calls, _deadline); + +// verifyMintResults(key, balanceInfo, tokenId); +// } + +// // ===== PERMISSION FLAG TESTS ===== + +// function test_permission_flag_none() public { +// // Test that NONE permissions prevent all operations +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.NONE); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.NONE); + +// // Should revert when trying to mint with NONE permissions +// PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); +// uint256 liquidity = 1e18; + +// vm.prank(alice); +// vm.expectRevert(); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// } + +// function test_permission_flag_swap_allowed() public { +// // Test that SWAP_ALLOWED only allows swaps, not liquidity operations +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + +// // Should revert when trying to mint (liquidity operation) with only SWAP_ALLOWED +// PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); +// uint256 liquidity = 1e18; + +// vm.prank(alice); +// vm.expectRevert(); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// } + +// function test_permission_flag_liquidity_allowed() public { +// // Test that LIQUIDITY_ALLOWED allows liquidity operations +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + +// // Should succeed when trying to mint with LIQUIDITY_ALLOWED +// PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); +// uint256 liquidity = 1e18; +// uint256 tokenId = lpm.nextTokenId(); + +// vm.prank(alice); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// } + +// function test_permission_flag_all_allowed() public { +// // Test that ALL_ALLOWED allows all operations +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.ALL_ALLOWED); + +// // Should succeed when trying to mint with ALL_ALLOWED +// PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); +// uint256 liquidity = 1e18; +// uint256 tokenId = lpm.nextTokenId(); + +// vm.prank(alice); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// } + +// function test_permission_flag_combinations() public { +// // Test various combinations of permissions +// PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); +// uint256 liquidity = 1e18; + +// // Test SWAP_ALLOWED + LIQUIDITY_ALLOWED (should work like ALL_ALLOWED) +// MockPermissionedToken(Currency.unwrap(currency0)) +// .setAllowlist(alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED)); +// MockPermissionedToken(Currency.unwrap(currency2)) +// .setAllowlist(alice, (PermissionFlags.SWAP_ALLOWED | PermissionFlags.LIQUIDITY_ALLOWED)); + +// uint256 tokenId = lpm.nextTokenId(); +// vm.prank(alice); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// } + +// function test_permission_flag_partial_permissions() public { +// // Test that having permissions on only one token is enough +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.NONE); + +// PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); +// uint256 liquidity = 1e18; + +// vm.prank(alice); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// } + +// function test_permission_flag_dynamic_changes() public { +// // Test that permission changes take effect immediately +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + +// PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); +// uint256 liquidity = 1e18; + +// // Should succeed initially +// uint256 tokenId = lpm.nextTokenId(); +// vm.prank(alice); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); + +// // Remove permissions +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.NONE); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.NONE); + +// // Should fail on subsequent operations +// tokenId = lpm.nextTokenId(); +// vm.prank(alice); +// vm.expectRevert(); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// } + +// function test_permission_flag_edge_cases() public { +// // Test edge cases with permission flags +// PositionConfig memory config = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); +// uint256 liquidity = 1e18; + +// // Test with zero permissions (should be same as NONE) +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlag.wrap(0x0000)); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlag.wrap(0x0000)); + +// vm.prank(alice); +// vm.expectRevert(); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); + +// // Test with maximum permissions (should be same as ALL_ALLOWED) +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlag.wrap(0xFFFF)); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlag.wrap(0xFFFF)); + +// uint256 tokenId = lpm.nextTokenId(); +// vm.prank(alice); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// } + +// function test_permission_flag_all_pools() public { +// // Test permission flags work across all pool types +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.LIQUIDITY_ALLOWED); + +// PositionConfig memory config0 = PositionConfig({poolKey: key0, tickLower: -120, tickUpper: 120}); +// PositionConfig memory config1 = PositionConfig({poolKey: key1, tickLower: -120, tickUpper: 120}); +// PositionConfig memory config2 = PositionConfig({poolKey: key2, tickLower: -120, tickUpper: 120}); +// uint256 liquidity = 1e18; + +// // Test permissioned/normal pool +// uint256 tokenId0 = lpm.nextTokenId(); +// vm.prank(alice); +// mint(config0, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId0), alice); + +// // Test normal/permissioned pool +// uint256 tokenId1 = lpm.nextTokenId(); +// vm.prank(alice); +// mint(config1, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId1), alice); + +// // Test permissioned/permissioned pool +// uint256 tokenId2 = lpm.nextTokenId(); +// vm.prank(alice); +// mint(config2, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId2), alice); +// } + +// function test_permission_flag_swap_allowed_unauthorized_reverts() public { +// // Test that SWAP_ALLOWED does not allow liquidity operations +// MockPermissionedToken(Currency.unwrap(currency0)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); +// MockPermissionedToken(Currency.unwrap(currency2)).setAllowlist(alice, PermissionFlags.SWAP_ALLOWED); + +// // Should revert when trying to mint with SWAP_ALLOWED +// PositionConfig memory config = PositionConfig({poolKey: insecureKey, tickLower: -120, tickUpper: 120}); +// uint256 liquidity = 1e18; + +// vm.startPrank(alice); +// vm.expectRevert(Unauthorized.selector); +// mint(config, liquidity, ActionConstants.MSG_SENDER, ZERO_BYTES); +// vm.stopPrank(); +// } + +// function test_permissioned_mint_to_unauthorized_recipient_reverts() public { +// // Caller (address(this)) is allowed, but unauthorizedUser is not +// // The hook will pass because the caller is authorized, +// // but _mint should revert because the recipient lacks LIQUIDITY_ALLOWED +// PositionConfig memory config = PositionConfig({poolKey: key0, tickLower: -120, tickUpper: 120}); +// uint256 liquidity = 1e18; + +// vm.expectRevert(Unauthorized.selector); +// mint(config, liquidity, unauthorizedUser, ZERO_BYTES); +// } + +// function test_permissioned_mint_to_authorized_recipient_succeeds() public { +// // Caller (address(this)) is allowed, alice is also allowed +// // Minting to an authorized recipient should succeed +// PositionConfig memory config = PositionConfig({poolKey: key0, tickLower: -120, tickUpper: 120}); +// uint256 liquidity = 1e18; +// uint256 tokenId = lpm.nextTokenId(); + +// mint(config, liquidity, alice, ZERO_BYTES); +// assertEq(IERC721(address(lpm)).ownerOf(tokenId), alice); +// } +// } diff --git a/test/hooks/permissionedPools/PermissionedV4Router.t.sol b/test/hooks/permissionedPools/PermissionedV4Router.t.sol index 4b6bb8828..6d49721f5 100644 --- a/test/hooks/permissionedPools/PermissionedV4Router.t.sol +++ b/test/hooks/permissionedPools/PermissionedV4Router.t.sol @@ -114,7 +114,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -144,7 +144,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey memory adapterKey = PoolKey(currencyA, currencyB, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(currencyA, currencyB, ActionConstants.MSG_SENDER); @@ -173,7 +173,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -218,7 +218,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -238,7 +238,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -264,7 +264,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -303,7 +303,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { getPermissionedCurrency(key1.currency1).transfer(alice, 2 ether); } IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key1, zeroToOne, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(key1, zeroToOne, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, ActionConstants.MSG_SENDER); @@ -339,7 +339,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { } IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key1, zeroToOne, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(key1, zeroToOne, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, ActionConstants.MSG_SENDER); @@ -378,7 +378,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountIn = 100; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key1, zeroToOne, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(key1, zeroToOne, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, ActionConstants.MSG_SENDER); @@ -415,7 +415,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { } IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key1, zeroToOne, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(key1, zeroToOne, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, ActionConstants.MSG_SENDER); @@ -443,7 +443,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -467,7 +467,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, insecureHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -492,7 +492,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -516,7 +516,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -536,7 +536,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -565,7 +565,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -589,7 +589,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -619,7 +619,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -659,7 +659,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountIn = 100; plan = Planner.init(); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(poolKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(poolKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); plan = plan.add(Actions.SETTLE, abi.encode(poolKey.currency0, ActionConstants.CONTRACT_BALANCE, false)); plan = plan.add(Actions.TAKE_ALL, abi.encode(poolKey.currency1, 0)); @@ -683,7 +683,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( - adapterKey, true, uint128(amountOut), uint128(amountInMaximum), bytes("") + adapterKey, true, uint128(amountOut), uint128(amountInMaximum), 0, bytes("") ); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); @@ -718,7 +718,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { }); IV4Router.ExactInputParams memory params = - IV4Router.ExactInputParams(permissionsAdapter1Currency, pathKeys, uint128(amountIn), 0); + IV4Router.ExactInputParams(permissionsAdapter1Currency, pathKeys, new uint256[](0), uint128(amountIn), 0); plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); bytes memory data = @@ -741,7 +741,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { PoolKey(permissionsAdapter1Currency, permissionsAdapter0Currency, 3000, 60, permissionedHooks); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(adapterKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = @@ -759,8 +759,9 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountIn = 1 ether; uint256 expectedAmountOut = 19992; // min amount out of 1 higher than the actual amount out - IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), uint128(expectedAmountOut + 1), bytes("")); + IV4Router.ExactInputSingleParams memory params = IV4Router.ExactInputSingleParams( + key0, true, uint128(amountIn), uint128(expectedAmountOut + 1), 0, bytes("") + ); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, ActionConstants.MSG_SENDER); @@ -776,7 +777,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 expectedAmountOut = 19992; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); (uint256 inputBalanceBefore, uint256 outputBalanceBefore,) = @@ -801,7 +802,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 expectedAmountOut = 19992; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -828,7 +829,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 expectedAmountOut = 19992; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -854,7 +855,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 expectedAmountOut = 949; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); Currency inputCurrency = getPermissionedCurrency(key0.currency0); Currency outputCurrency = getPermissionedCurrency(key0.currency1); @@ -882,7 +883,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bool zeroForOne = false; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, zeroForOne, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, zeroForOne, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -1040,6 +1041,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IV4Router.ExactInputParams memory params = IV4Router.ExactInputParams({ currencyIn: permissionsAdapter0Currency, path: path, + minHopPriceX36: new uint256[](0), amountIn: uint128(amountIn), amountOutMinimum: uint128(expectedAmountOut) }); @@ -1077,7 +1079,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 expectedAmountOut = 19992; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(nativeKey, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(nativeKey, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -1107,7 +1109,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { // native output means we need !zeroForOne IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(nativeKey, zeroForOne, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(nativeKey, zeroForOne, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -1275,7 +1277,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { vm.deal(address(permissionedRouter), amountIn); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(nativeKey, zeroForOne, ActionConstants.OPEN_DELTA, 0, bytes("")); + IV4Router.ExactInputSingleParams(nativeKey, zeroForOne, ActionConstants.OPEN_DELTA, 0, 0, bytes("")); plan = plan.add(Actions.SETTLE, abi.encode(nativeKey.currency0, ActionConstants.CONTRACT_BALANCE, false)); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -1303,8 +1305,9 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 amountOut = 1 ether; uint256 expectedAmountIn = 369070324193623892281288; - IV4Router.ExactOutputSingleParams memory params = - IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn - 1), bytes("")); + IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( + key0, true, uint128(amountOut), uint128(expectedAmountIn - 1), 0, bytes("") + ); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, ActionConstants.MSG_SENDER); @@ -1323,7 +1326,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bool zeroForOne = true; IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( - key0, zeroForOne, uint128(amountOut), uint128(expectedAmountIn + 1), bytes("") + key0, zeroForOne, uint128(amountOut), uint128(expectedAmountIn + 1), 0, bytes("") ); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); @@ -1355,7 +1358,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bool zeroForOne = false; IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( - key0, zeroForOne, uint128(amountOut), uint128(expectedAmountIn + 1), bytes("") + key0, zeroForOne, uint128(amountOut), uint128(expectedAmountIn + 1), 0, bytes("") ); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); @@ -1514,6 +1517,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IV4Router.ExactOutputParams memory params = IV4Router.ExactOutputParams({ currencyOut: currency3, path: path, + minHopPriceX36: new uint256[](0), amountOut: uint128(amountOut), amountInMaximum: uint128(expectedAmountIn) }); @@ -1686,7 +1690,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { uint256 expectedOutput = 900; IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( - key0, true, ActionConstants.OPEN_DELTA, uint128(expectedAmountIn), bytes("") + key0, true, ActionConstants.OPEN_DELTA, uint128(expectedAmountIn), 0, bytes("") ); plan = plan.add(Actions.TAKE, abi.encode(key0.currency1, ActionConstants.ADDRESS_THIS, expectedOutput)); @@ -1742,7 +1746,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { bool zeroForOne = false; IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( - nativeKey, zeroForOne, uint128(amountOut), uint128(expectedAmountIn + 1), bytes("") + nativeKey, zeroForOne, uint128(amountOut), uint128(expectedAmountIn + 1), 0, bytes("") ); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); @@ -1806,7 +1810,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(key2.currency0)).transfer(address(permissionedRouter), amountIn); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key2, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(key2, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SETTLE, abi.encode(key2.currency0, ActionConstants.CONTRACT_BALANCE, false)); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -1835,7 +1839,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { IERC20(Currency.unwrap(getPermissionedCurrency(key0.currency0))).transfer(address(permissionedRouter), amountIn); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SETTLE, abi.encode((key0.currency0), ActionConstants.CONTRACT_BALANCE, false)); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -1872,7 +1876,7 @@ contract PermissionedV4RouterTest is PermissionedRoutingTestHelpers { .setAllowlist(unauthorizedUser, PermissionFlags.NONE); IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); plan = plan.add(Actions.SETTLE, abi.encode((key0.currency0), ActionConstants.CONTRACT_BALANCE, false)); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); diff --git a/test/hooks/permissionedPools/mocks/MockHooks.sol b/test/hooks/permissionedPools/mocks/MockInsecureHooks.sol similarity index 66% rename from test/hooks/permissionedPools/mocks/MockHooks.sol rename to test/hooks/permissionedPools/mocks/MockInsecureHooks.sol index 1331fd9cf..3573effb7 100644 --- a/test/hooks/permissionedPools/mocks/MockHooks.sol +++ b/test/hooks/permissionedPools/mocks/MockInsecureHooks.sol @@ -5,17 +5,6 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import { - IPermissionsAdapterFactory -} from "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; -import {PermissionedHooks} from "../../../../src/hooks/permissionedPools/PermissionedHooks.sol"; - -contract MockHooks is PermissionedHooks { - constructor(IPoolManager manager, IPermissionsAdapterFactory permissionsAdapterFactory) - PermissionedHooks(manager, permissionsAdapterFactory) - {} -} /// @notice This contract is used in the testing of security for the permissioned pool manager contract MockInsecureHooks { diff --git a/test/hooks/permissionedPools/mocks/MockPermissionedHooks.sol b/test/hooks/permissionedPools/mocks/MockPermissionedHooks.sol new file mode 100644 index 000000000..488680035 --- /dev/null +++ b/test/hooks/permissionedPools/mocks/MockPermissionedHooks.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +import {Hooks, IHooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IMsgSender} from "../../../../src/interfaces/IMsgSender.sol"; +import {IPermissionsAdapter} from "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapter.sol"; +import { + IPermissionsAdapterFactory +} from "../../../../src/hooks/permissionedPools/interfaces/IPermissionsAdapterFactory.sol"; +import {PermissionFlags, PermissionFlag} from "../../../../src/hooks/permissionedPools/libraries/PermissionFlags.sol"; + +/// @notice Standalone mock that replicates PermissionedHooks logic without inheriting BaseHook +contract MockPermissionedHooks { + error Unauthorized(); + error SwappingDisabled(); + error NoVerifiedAdapter(); + error HookNotImplemented(); + + IPoolManager public immutable manager; + IPermissionsAdapterFactory public immutable PERMISSIONS_ADAPTER_FACTORY; + + constructor(IPoolManager _manager, IPermissionsAdapterFactory _permissionsAdapterFactory) { + manager = _manager; + PERMISSIONS_ADAPTER_FACTORY = _permissionsAdapterFactory; + } + + function beforeInitialize(address, PoolKey calldata key, uint160) external view returns (bytes4) { + bool currency0Verified = + PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(key.currency0)) != address(0); + bool currency1Verified = + PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(Currency.unwrap(key.currency1)) != address(0); + if (!currency0Verified && !currency1Verified) revert NoVerifiedAdapter(); + return IHooks.beforeInitialize.selector; + } + + function beforeSwap(address sender, PoolKey calldata key, SwapParams calldata, bytes calldata) + external + view + returns (bytes4, BeforeSwapDelta, uint24) + { + _verifyAllowlist(IMsgSender(sender), key, IHooks.beforeSwap.selector); + return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + function beforeAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata) + external + view + returns (bytes4) + { + _verifyAllowlist(IMsgSender(sender), key, IHooks.beforeAddLiquidity.selector); + return IHooks.beforeAddLiquidity.selector; + } + + function afterSwap(address, PoolKey calldata, SwapParams calldata, BalanceDelta, bytes calldata) + external + pure + returns (bytes4, int128) + { + revert HookNotImplemented(); + } + + function afterInitialize(address, PoolKey calldata, uint160, int24) external pure returns (bytes4) { + revert HookNotImplemented(); + } + + function beforeRemoveLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata) + external + pure + returns (bytes4) + { + revert HookNotImplemented(); + } + + function afterRemoveLiquidity( + address, + PoolKey calldata, + ModifyLiquidityParams calldata, + BalanceDelta, + BalanceDelta, + bytes calldata + ) external pure returns (bytes4, BalanceDelta) { + revert HookNotImplemented(); + } + + function afterAddLiquidity( + address, + PoolKey calldata, + ModifyLiquidityParams calldata, + BalanceDelta, + BalanceDelta, + bytes calldata + ) external pure returns (bytes4, BalanceDelta) { + revert HookNotImplemented(); + } + + function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) external pure returns (bytes4) { + revert HookNotImplemented(); + } + + function afterDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) external pure returns (bytes4) { + revert HookNotImplemented(); + } + + function _verifyAllowlist(IMsgSender sender, PoolKey calldata poolKey, bytes4 selector) internal view { + _isAllowed(Currency.unwrap(poolKey.currency0), sender.msgSender(), address(sender), selector); + _isAllowed(Currency.unwrap(poolKey.currency1), sender.msgSender(), address(sender), selector); + } + + function _isAllowed(address permissionsAdapter, address sender, address router, bytes4 selector) internal view { + address permissionedToken = PERMISSIONS_ADAPTER_FACTORY.verifiedPermissionsAdapterOf(permissionsAdapter); + if (permissionedToken == address(0)) return; + + PermissionFlag permission = PermissionFlags.NONE; + if (selector == IHooks.beforeSwap.selector) { + permission = PermissionFlags.SWAP_ALLOWED; + if (!IPermissionsAdapter(permissionsAdapter).swappingEnabled()) revert SwappingDisabled(); + } else if (selector == IHooks.beforeAddLiquidity.selector) { + permission = PermissionFlags.LIQUIDITY_ALLOWED; + } + + if ( + !IPermissionsAdapter(permissionsAdapter).isAllowed(sender, permission) + || !IPermissionsAdapter(permissionsAdapter).allowedWrappers(router) + ) revert Unauthorized(); + } +} diff --git a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol index 7ab26a532..311890109 100644 --- a/test/hooks/permissionedPools/shared/PermissionedDeployers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedDeployers.sol @@ -37,7 +37,8 @@ import { import {MockPermissionedRouter} from "../../../mocks/MockPermissionedRouter.sol"; import {MockPermissionedToken} from "../PermissionedPoolsBase.sol"; import {MockV4Router} from "../../../mocks/MockV4Router.sol"; -import {MockHooks} from "../mocks/MockHooks.sol"; +import {MockInsecureHooks} from "../mocks/MockInsecureHooks.sol"; +import {MockPermissionedHooks} from "../mocks/MockPermissionedHooks.sol"; import {Deploy} from "../../../../test/shared/Deploy.sol"; import {HookMiner} from "../../../../src/utils/HookMiner.sol"; import {IWETH9} from "../../../../src/interfaces/external/IWETH9.sol"; @@ -135,12 +136,12 @@ contract PermissionedDeployers is Test { (address calculatedAddr, bytes32 salt) = HookMiner.find( address(this), flags, - vm.getCode("MockHooks.sol:MockHooks"), + vm.getCode("MockPermissionedHooks.sol:MockPermissionedHooks"), abi.encode(IPoolManager(manager_), IPermissionsAdapterFactory(permissionsAdapterFactory_)) ); address addr = Deploy.create2( abi.encodePacked( - vm.getCode("MockHooks.sol:MockHooks"), + vm.getCode("MockPermissionedHooks.sol:MockPermissionedHooks"), abi.encode(IPoolManager(manager_), IPermissionsAdapterFactory(permissionsAdapterFactory_)) ), salt @@ -152,10 +153,14 @@ contract PermissionedDeployers is Test { function deployInsecureHooks(address manager_) internal returns (address deployedHooksAddr) { uint160 flags = (1 << 11) | (1 << 7); (address calculatedAddr, bytes32 salt) = HookMiner.find( - address(this), flags, vm.getCode("MockHooks.sol:MockInsecureHooks"), abi.encode(IPoolManager(manager_)) + address(this), + flags, + vm.getCode("MockInsecureHooks.sol:MockInsecureHooks"), + abi.encode(IPoolManager(manager_)) ); address addr = Deploy.create2( - abi.encodePacked(vm.getCode("MockHooks.sol:MockInsecureHooks"), abi.encode(IPoolManager(manager_))), salt + abi.encodePacked(vm.getCode("MockInsecureHooks.sol:MockInsecureHooks"), abi.encode(IPoolManager(manager_))), + salt ); assertEq(addr, calculatedAddr); deployedHooksAddr = calculatedAddr; @@ -314,6 +319,7 @@ contract PermissionedDeployers is Test { zeroForOne, // Direction of swap uint128(amountIn), // Amount to swap in 0, // Minimum amount out (0 = no slippage protection) + 0, bytes("") // Hook data ); diff --git a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol index d017793bb..a02804472 100644 --- a/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol +++ b/test/hooks/permissionedPools/shared/PermissionedRoutingTestHelpers.sol @@ -259,6 +259,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 params.currencyIn = _tokenPath[0]; params.path = path; + params.minHopPriceX36 = new uint256[](0); params.amountIn = uint128(amountIn); params.amountOutMinimum = uint128(amountOutMinimum); } @@ -284,6 +285,7 @@ contract PermissionedRoutingTestHelpers is PermissionedDeployers, DeployPermit2 params.currencyOut = _tokenPath[_tokenPath.length - 1]; params.path = path; + params.minHopPriceX36 = new uint256[](0); params.amountOut = uint128(amountOut); params.amountInMaximum = uint128(amountInMaximum); } From e40c0a3fb0953d49d5d031477327f90db72218a0 Mon Sep 17 00:00:00 2001 From: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:35:41 -0400 Subject: [PATCH 33/33] fix(snapshots): fixing unit test snapshots --- snapshots/BaseActionsRouterTest.json | 2 +- snapshots/PaymentsTests.json | 8 +-- snapshots/PermissionedV4RouterTest.json | 2 +- snapshots/PosMGasTest.json | 78 ++++++++++++------------- snapshots/QuoterTest.json | 26 ++++----- snapshots/StateViewTest.json | 20 +++---- snapshots/V4RouterTest.json | 46 +++++++-------- 7 files changed, 91 insertions(+), 91 deletions(-) diff --git a/snapshots/BaseActionsRouterTest.json b/snapshots/BaseActionsRouterTest.json index 8881f0954..d7241dbe2 100644 --- a/snapshots/BaseActionsRouterTest.json +++ b/snapshots/BaseActionsRouterTest.json @@ -1,3 +1,3 @@ { - "BaseActionsRouter_mock10commands": "36124" + "BaseActionsRouter_mock10commands": "63076" } \ No newline at end of file diff --git a/snapshots/PaymentsTests.json b/snapshots/PaymentsTests.json index 1e2bd091c..bebf26d74 100644 --- a/snapshots/PaymentsTests.json +++ b/snapshots/PaymentsTests.json @@ -1,6 +1,6 @@ { - "Payments_swap_settleFromCaller_takeAllToMsgSender": "106843", - "Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "107772", - "Payments_swap_settleWithBalance_takeAllToMsgSender": "98226", - "Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "98136" + "Payments_swap_settleFromCaller_takeAllToMsgSender": "133163", + "Payments_swap_settleFromCaller_takeAllToSpecifiedAddress": "134632", + "Payments_swap_settleWithBalance_takeAllToMsgSender": "127114", + "Payments_swap_settleWithBalance_takeAllToSpecifiedAddress": "127252" } \ No newline at end of file diff --git a/snapshots/PermissionedV4RouterTest.json b/snapshots/PermissionedV4RouterTest.json index 833895aba..7239323da 100644 --- a/snapshots/PermissionedV4RouterTest.json +++ b/snapshots/PermissionedV4RouterTest.json @@ -1,3 +1,3 @@ { - "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "223249" + "PermissionedV4Router_ExactInputSingle_PermissionedTokens": "231245" } \ No newline at end of file diff --git a/snapshots/PosMGasTest.json b/snapshots/PosMGasTest.json index 179915a2c..579fa6642 100644 --- a/snapshots/PosMGasTest.json +++ b/snapshots/PosMGasTest.json @@ -1,43 +1,43 @@ { - "PositionManager_burn_empty": "15837", - "PositionManager_burn_empty_native": "15837", - "PositionManager_burn_nonEmpty_native_withClose": "53574", - "PositionManager_burn_nonEmpty_native_withTakePair": "52825", - "PositionManager_burn_nonEmpty_withClose": "49866", - "PositionManager_burn_nonEmpty_withTakePair": "49117", - "PositionManager_collect_native": "78760", - "PositionManager_collect_sameRange": "75052", - "PositionManager_collect_withClose": "75052", - "PositionManager_collect_withTakePair": "74303", - "PositionManager_decreaseLiquidity_native": "46233", - "PositionManager_decreaseLiquidity_withClose": "42525", - "PositionManager_decreaseLiquidity_withTakePair": "41776", - "PositionManager_decrease_burnEmpty": "52299", - "PositionManager_decrease_burnEmpty_native": "56007", - "PositionManager_decrease_sameRange_allLiquidity": "40841", - "PositionManager_decrease_take_take": "42570", - "PositionManager_increaseLiquidity_erc20_withClose": "51172", - "PositionManager_increaseLiquidity_erc20_withSettlePair": "50387", - "PositionManager_increaseLiquidity_native": "50392", - "PositionManager_increase_autocompoundExactUnclaimedFees": "64807", - "PositionManager_increase_autocompoundExcessFeesCredit": "80601", - "PositionManager_increase_autocompound_clearExcess": "74319", - "PositionManager_mint_native": "341094", - "PositionManager_mint_nativeWithSweep_withClose": "349125", - "PositionManager_mint_nativeWithSweep_withSettlePair": "348168", - "PositionManager_mint_onSameTickLower": "258462", - "PositionManager_mint_onSameTickUpper": "263104", - "PositionManager_mint_sameRange": "163786", - "PositionManager_mint_settleWithBalance_sweep": "386472", - "PositionManager_mint_warmedPool_differentRange": "263680", - "PositionManager_mint_withClose": "394974", - "PositionManager_mint_withSettlePair": "394103", - "PositionManager_multicall_initialize_mint": "428837", - "PositionManager_permit": "51379", - "PositionManager_permit_secondPosition": "29479", - "PositionManager_permit_twice": "7579", - "PositionManager_subscribe": "56828", - "PositionManager_unsubscribe": "27693", + "PositionManager_burn_empty": "51102", + "PositionManager_burn_empty_native": "51102", + "PositionManager_burn_nonEmpty_native_withClose": "127695", + "PositionManager_burn_nonEmpty_native_withTakePair": "127137", + "PositionManager_burn_nonEmpty_withClose": "134600", + "PositionManager_burn_nonEmpty_withTakePair": "134043", + "PositionManager_collect_native": "147732", + "PositionManager_collect_sameRange": "156364", + "PositionManager_collect_withClose": "156364", + "PositionManager_collect_withTakePair": "155679", + "PositionManager_decreaseLiquidity_native": "113681", + "PositionManager_decreaseLiquidity_withClose": "121933", + "PositionManager_decreaseLiquidity_withTakePair": "121248", + "PositionManager_decrease_burnEmpty": "137500", + "PositionManager_decrease_burnEmpty_native": "130595", + "PositionManager_decrease_sameRange_allLiquidity": "134649", + "PositionManager_decrease_take_take": "122490", + "PositionManager_increaseLiquidity_erc20_withClose": "160652", + "PositionManager_increaseLiquidity_erc20_withSettlePair": "159563", + "PositionManager_increaseLiquidity_native": "143532", + "PositionManager_increase_autocompoundExactUnclaimedFees": "138231", + "PositionManager_increase_autocompoundExcessFeesCredit": "179593", + "PositionManager_increase_autocompound_clearExcess": "149511", + "PositionManager_mint_native": "367550", + "PositionManager_mint_nativeWithSweep_withClose": "376153", + "PositionManager_mint_nativeWithSweep_withSettlePair": "375348", + "PositionManager_mint_onSameTickLower": "319298", + "PositionManager_mint_onSameTickUpper": "319940", + "PositionManager_mint_sameRange": "245522", + "PositionManager_mint_settleWithBalance_sweep": "420760", + "PositionManager_mint_warmedPool_differentRange": "325316", + "PositionManager_mint_withClose": "421910", + "PositionManager_mint_withSettlePair": "420939", + "PositionManager_multicall_initialize_mint": "458237", + "PositionManager_permit": "79163", + "PositionManager_permit_secondPosition": "62075", + "PositionManager_permit_twice": "44975", + "PositionManager_subscribe": "87968", + "PositionManager_unsubscribe": "62697", "position manager initcode hash (without constructor params, as uint256)": "81827502601055975118808937107769364319765058198432540518516360048852193272922", "positionManager bytecode size": "23877" } \ No newline at end of file diff --git a/snapshots/QuoterTest.json b/snapshots/QuoterTest.json index 5df24b9ff..3ecbe3350 100644 --- a/snapshots/QuoterTest.json +++ b/snapshots/QuoterTest.json @@ -1,15 +1,15 @@ { - "Quoter_exactInputSingle_oneForZero_multiplePositions": "123658", - "Quoter_exactInputSingle_zeroForOne_multiplePositions": "129861", - "Quoter_exactOutputSingle_oneForZero": "57001", - "Quoter_exactOutputSingle_zeroForOne": "62234", - "Quoter_quoteExactInput_oneHop_1TickLoaded": "100170", - "Quoter_quoteExactInput_oneHop_initializedAfter": "124805", - "Quoter_quoteExactInput_oneHop_startingInitialized": "47592", - "Quoter_quoteExactInput_twoHops": "181404", - "Quoter_quoteExactOutput_oneHop_1TickLoaded": "99666", - "Quoter_quoteExactOutput_oneHop_2TicksLoaded": "130321", - "Quoter_quoteExactOutput_oneHop_initializedAfter": "99693", - "Quoter_quoteExactOutput_oneHop_startingInitialized": "54699", - "Quoter_quoteExactOutput_twoHops": "181132" + "Quoter_exactInputSingle_oneForZero_multiplePositions": "146134", + "Quoter_exactInputSingle_zeroForOne_multiplePositions": "152349", + "Quoter_exactOutputSingle_oneForZero": "79477", + "Quoter_exactOutputSingle_zeroForOne": "84722", + "Quoter_quoteExactInput_oneHop_1TickLoaded": "122938", + "Quoter_quoteExactInput_oneHop_initializedAfter": "147573", + "Quoter_quoteExactInput_oneHop_startingInitialized": "80848", + "Quoter_quoteExactInput_twoHops": "205152", + "Quoter_quoteExactOutput_oneHop_1TickLoaded": "122434", + "Quoter_quoteExactOutput_oneHop_2TicksLoaded": "153089", + "Quoter_quoteExactOutput_oneHop_initializedAfter": "122461", + "Quoter_quoteExactOutput_oneHop_startingInitialized": "98755", + "Quoter_quoteExactOutput_twoHops": "204880" } \ No newline at end of file diff --git a/snapshots/StateViewTest.json b/snapshots/StateViewTest.json index dc141e39f..ab141d8b1 100644 --- a/snapshots/StateViewTest.json +++ b/snapshots/StateViewTest.json @@ -1,12 +1,12 @@ { - "StateView_extsload_getFeeGrowthGlobals": "2203", - "StateView_extsload_getFeeGrowthInside": "7875", - "StateView_extsload_getLiquidity": "1439", - "StateView_extsload_getPositionInfo": "2761", - "StateView_extsload_getPositionLiquidity": "1691", - "StateView_extsload_getSlot0": "1486", - "StateView_extsload_getTickBitmap": "1432", - "StateView_extsload_getTickFeeGrowthOutside": "2490", - "StateView_extsload_getTickInfo": "2693", - "StateView_extsload_getTickLiquidity": "1686" + "StateView_extsload_getFeeGrowthGlobals": "8703", + "StateView_extsload_getFeeGrowthInside": "24375", + "StateView_extsload_getLiquidity": "5939", + "StateView_extsload_getPositionInfo": "11261", + "StateView_extsload_getPositionLiquidity": "6191", + "StateView_extsload_getSlot0": "5986", + "StateView_extsload_getTickBitmap": "5932", + "StateView_extsload_getTickFeeGrowthOutside": "8990", + "StateView_extsload_getTickInfo": "11193", + "StateView_extsload_getTickLiquidity": "6186" } \ No newline at end of file diff --git a/snapshots/V4RouterTest.json b/snapshots/V4RouterTest.json index 9af030053..dcf6ef685 100644 --- a/snapshots/V4RouterTest.json +++ b/snapshots/V4RouterTest.json @@ -1,27 +1,27 @@ { "V4Router_Bytecode": "9950", - "V4Router_ExactIn1Hop_nativeIn": "95778", - "V4Router_ExactIn1Hop_nativeOut": "94244", - "V4Router_ExactIn1Hop_oneForZero": "102636", - "V4Router_ExactIn1Hop_zeroForOne": "109162", - "V4Router_ExactIn2Hops": "158796", - "V4Router_ExactIn2Hops_nativeIn": "151940", - "V4Router_ExactIn3Hops": "208433", - "V4Router_ExactIn3Hops_nativeIn": "201577", - "V4Router_ExactInputSingle": "107666", - "V4Router_ExactInputSingle_nativeIn": "94282", - "V4Router_ExactInputSingle_nativeOut": "92738", - "V4Router_ExactOut1Hop_nativeIn_sweepETH": "101873", - "V4Router_ExactOut1Hop_nativeOut": "95262", - "V4Router_ExactOut1Hop_oneForZero": "103654", - "V4Router_ExactOut1Hop_zeroForOne": "108311", - "V4Router_ExactOut2Hops": "158923", - "V4Router_ExactOut2Hops_nativeIn": "157142", - "V4Router_ExactOut3Hops": "209553", - "V4Router_ExactOut3Hops_nativeIn": "207772", - "V4Router_ExactOut3Hops_nativeOut": "201161", - "V4Router_ExactOutputSingle": "106810", - "V4Router_ExactOutputSingle_nativeIn_sweepETH": "100372", - "V4Router_ExactOutputSingle_nativeOut": "93786", + "V4Router_ExactIn1Hop_nativeIn": "122222", + "V4Router_ExactIn1Hop_nativeOut": "120688", + "V4Router_ExactIn1Hop_oneForZero": "129560", + "V4Router_ExactIn1Hop_zeroForOne": "136086", + "V4Router_ExactIn2Hops": "193444", + "V4Router_ExactIn2Hops_nativeIn": "179580", + "V4Router_ExactIn3Hops": "250829", + "V4Router_ExactIn3Hops_nativeIn": "236965", + "V4Router_ExactInputSingle": "134158", + "V4Router_ExactInputSingle_nativeIn": "120294", + "V4Router_ExactInputSingle_nativeOut": "118738", + "V4Router_ExactOut1Hop_nativeIn_sweepETH": "128509", + "V4Router_ExactOut1Hop_nativeOut": "121898", + "V4Router_ExactOut1Hop_oneForZero": "130770", + "V4Router_ExactOut1Hop_zeroForOne": "135427", + "V4Router_ExactOut2Hops": "191892", + "V4Router_ExactOut2Hops_nativeIn": "184974", + "V4Router_ExactOut3Hops": "248399", + "V4Router_ExactOut3Hops_nativeIn": "241481", + "V4Router_ExactOut3Hops_nativeOut": "225554", + "V4Router_ExactOutputSingle": "133494", + "V4Router_ExactOutputSingle_nativeIn_sweepETH": "126576", + "V4Router_ExactOutputSingle_nativeOut": "119978", "router initcode hash (without constructor params, as uint256)": "19267777467363212309328806064985146558623592810537866695556902731584515865783" } \ No newline at end of file