Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/PositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions src/hooks/permissionedPools/BaseAllowListChecker.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
74 changes: 74 additions & 0 deletions src/hooks/permissionedPools/PermissionedPositionManager.sol
Original file line number Diff line number Diff line change
@@ -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());
Copy link

@ericneil-sanc ericneil-sanc Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question here: Given we are dealing with a permissioned/wrapped binary, why are we casting the permissioned token to an interface for the wrapped token? Shouldn't this be on poolKey.currency0 (and poolKey.currency1 for line 56)? Unless we are expecting the permissioned tokens to have the same function? I apologize if I am missing something, which definitely may be the case 😄 .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks!!

}
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);
}
}
53 changes: 53 additions & 0 deletions src/hooks/permissionedPools/PermissionedV4Router.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
104 changes: 104 additions & 0 deletions src/hooks/permissionedPools/WrappedPermissionedToken.sol
Original file line number Diff line number Diff line change
@@ -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());
}
}
42 changes: 42 additions & 0 deletions src/hooks/permissionedPools/WrappedPermissionedTokenFactory.sol
Original file line number Diff line number Diff line change
@@ -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));
}
}
8 changes: 8 additions & 0 deletions src/hooks/permissionedPools/interfaces/IAllowlistChecker.sol
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Loading