Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1554604
Initial commit: Permissioned Pools
gretzke Jun 18, 2025
92fd04a
Hook implementation
gretzke Jun 21, 2025
8249788
fix infinite loop
gretzke Jun 24, 2025
af9ff57
Wrapped token tests (#477)
gretzke Jun 25, 2025
c6b5931
Permissioned router tests, permissioned posm tests (#479)
ericneil-sanc Jul 9, 2025
8482d41
expect specific errors (#486)
ericneil-sanc Jul 15, 2025
8affcc9
Permissioned Pools: Transfers by admins (#483)
gretzke Jul 16, 2025
a8ae205
add granular allow list and multiple hooks and fix revert tests (#484)
ericneil-sanc Jul 16, 2025
f596e82
permissioned-pools: match universalrouter interface (#488)
ericneil-sanc Jul 22, 2025
3fc39d1
Permissioned Pools: Ability to pause swapping (#485)
gretzke Jul 22, 2025
5ad7877
Rename to permissions adapter (#489)
gretzke Jul 23, 2025
334a58c
emit event when hooks are allowed/disallowed (#498)
gretzke Sep 22, 2025
fbf665b
spelling fix
gretzke Sep 23, 2025
1154a7a
Fix typo
gretzke Nov 3, 2025
12bd7ce
Merge branch 'main' into permissioned-pools
gretzke Nov 3, 2025
1210b70
fix(permissioned-hooks): enforce permission-flags in both payer modes…
SocksNFlops Apr 13, 2026
13a4e4f
tests(permissioned-hooks): adding unit tests to validate enforcement …
SocksNFlops Apr 13, 2026
9c48968
update gas snapshot for PermissionedV4Router
SocksNFlops Apr 14, 2026
08cdb30
fixing snapshots and unused variable nit
SocksNFlops Apr 14, 2026
6886991
fixing snapshots one more time
SocksNFlops Apr 14, 2026
5cc4b17
fix: validate mint recipient has LIQUIDITY_ALLOWED in PermissionedPos…
SocksNFlops Apr 14, 2026
fabaf71
fix(permissioned-position-manager): add _mapSettleAmount override for…
SocksNFlops Apr 14, 2026
de71913
refactor(permissioned-routing): strip PermissionedV4Router to abstrac…
SocksNFlops Apr 14, 2026
147e768
Validating Permission-Flags in PermissionedV4Router and PermissionedP…
SocksNFlops Apr 14, 2026
129db45
Fix(PermissionedPositionManager): Adding Recipient checks to mint fun…
SocksNFlops Apr 14, 2026
f8b8f7d
Fix(PermissionedPositionManager): Overriding _mapSettlementAmount to …
SocksNFlops Apr 14, 2026
246249e
feat(permissioned-hooks): require verified adapter in beforeInitialize
SocksNFlops Apr 15, 2026
12501a6
merge(permissioned-hooks): incorporate beforeInitialize fix from eco-194
SocksNFlops Apr 15, 2026
ca50713
fix(ci): format MockPermissionedRouter
SocksNFlops Apr 15, 2026
6b19fb5
fix(ci): format MockPermissionedRouter and fix snapshot
SocksNFlops Apr 15, 2026
c5d705b
lint: fixing lint issue in tests
SocksNFlops Apr 15, 2026
ea8d4d8
fix(tests): fixing snapshots
SocksNFlops Apr 15, 2026
32ccd3c
Merge remote-tracking branch 'origin/permissioned-pools' into socksnf…
SocksNFlops Apr 15, 2026
bf146c3
Merge branch 'socksnflops/eco-194-sc-m-03-positions-can-be-opened-bef…
SocksNFlops Apr 15, 2026
3744a11
Merge remote-tracking branch 'origin/main' into socksnflops/eco-206-s…
SocksNFlops Apr 15, 2026
3405d01
feat(permissioned-hooks): require verified adapter in beforeInitializ…
SocksNFlops Apr 15, 2026
15daba8
fix(migration): migrating out PermissionedPoolHooks dependency
SocksNFlops Apr 15, 2026
f3fe55f
fix: uncommenting out permissioned-posm tests
SocksNFlops Apr 15, 2026
e40c0a3
fix(snapshots): fixing unit test snapshots
SocksNFlops Apr 15, 2026
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
3 changes: 3 additions & 0 deletions snapshots/PermissionedV4RouterTest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"PermissionedV4Router_ExactInputSingle_PermissionedTokens": "231245"
}
4 changes: 2 additions & 2 deletions src/PositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,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 @@ -519,7 +519,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
2 changes: 1 addition & 1 deletion src/base/DeltaResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
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, 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 (PermissionFlag);

function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return interfaceId == type(IAllowlistChecker).interfaceId || super.supportsInterface(interfaceId);
}
}
162 changes: 162 additions & 0 deletions src/hooks/permissionedPools/PermissionedPositionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {
PositionManager,
PoolKey,
IPoolManager,
IAllowanceTransfer,
IPositionDescriptor,
IWETH9,
Currency
} from "../../PositionManager.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";
import {ActionConstants} from "../../libraries/ActionConstants.sol";

contract PermissionedPositionManager is PositionManager {
IPermissionsAdapterFactory public immutable PERMISSIONS_ADAPTER_FACTORY;

mapping(Currency currency => mapping(IHooks hooks => bool)) public isAllowedHooks;

event AllowedHooksUpdated(Currency currency, IHooks hooks, bool allowed);

error InvalidHook();
error SafeTransferDisabled();
error NotPermissionsAdapterAdmin();

/// @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,
uint256 _unsubscribeGasLimit,
IPositionDescriptor _tokenDescriptor,
IWETH9 _weth9,
IPermissionsAdapterFactory _permissionsAdapterFactory
) PositionManager(_poolManager, _permit2, _unsubscribeGasLimit, _tokenDescriptor, _weth9) {
PERMISSIONS_ADAPTER_FACTORY = _permissionsAdapterFactory;
}

/// @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 permissions adapter
function setAllowedHook(Currency currency, IHooks hooks, bool allowed) external {
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
/// @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 SafeTransferDisabled();
}

function safeTransferFrom(address, address, uint256, bytes calldata) public pure override {
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.
function _mint(
PoolKey calldata poolKey,
int24 tickLower,
int24 tickUpper,
uint256 liquidity,
uint128 amount0Max,
uint128 amount1Max,
address owner,
bytes calldata hookData
) 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);
}

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);
if (permissionedToken == address(0)) {
// token is not a permissioned token, use the default implementation
super._pay(currency, payer, amount);
return;
}
// token is permissioned, wrap the token and transfer it to the pool manager
IPermissionsAdapter permissionsAdapter = IPermissionsAdapter(Currency.unwrap(currency));
if (payer == address(this)) {
// 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);
}
}

function _verifiedPermissionedTokenOf(Currency currency) internal view returns (address) {
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);
if (permissionedToken == address(0)) return address(0);
return IPermissionsAdapter(permissionsAdapter).owner();
}
}
67 changes: 67 additions & 0 deletions src/hooks/permissionedPools/PermissionedV4Router.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {ActionConstants} from "../../libraries/ActionConstants.sol";
import {V4Router, IPoolManager, Currency} from "../../V4Router.sol";
import {IPermissionsAdapter} from "./interfaces/IPermissionsAdapter.sol";
import {IPermissionsAdapterFactory} from "./interfaces/IPermissionsAdapterFactory.sol";
import {PermissionFlags} from "./libraries/PermissionFlags.sol";

/// @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;

error Unauthorized();

constructor(IPoolManager poolManager_, IPermissionsAdapterFactory permissionsAdapterFactory)
V4Router(poolManager_)
{
PERMISSIONS_ADAPTER_FACTORY = permissionsAdapterFactory;
}

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)) {
_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)) {
Currency.wrap(permissionedToken).transfer(address(permissionsAdapter), amount);
permissionsAdapter.wrapToPoolManager(amount);
} else {
_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 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();
}
}
Loading
Loading