Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
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": "227931"
}
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 @@ -509,7 +509,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);
}
}
80 changes: 80 additions & 0 deletions src/hooks/permissionedPools/PermissionedHooks.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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();

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.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 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();
}
}
138 changes: 138 additions & 0 deletions src/hooks/permissionedPools/PermissionedPositionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// 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";

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();
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);
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)) {
// @audit is it necessary to check the allowlist here?
if (!permissionsAdapter.isAllowed(msgSender(), PermissionFlags.LIQUIDITY_ALLOWED)) {
revert Unauthorized();
}
Currency.wrap(permissionedToken).transfer(address(permissionsAdapter), amount);
permissionsAdapter.wrapToPoolManager(amount);
} else {
// 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));
}

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();
}
}
Loading
Loading