关于 permissioned-pools-issuer
面向发行方工程团队的 Uniswap v4 Permissioned Pools 合约参考技能,说明合约机制、推荐调用顺序及错误原因。它还涵盖地址区分、权限标志和源码中容易混淆的名称与签名。
主要功能
- 01解释合约架构与调用顺序
- 02说明强制顺序及相关错误
- 03区分底层代币与适配器地址
- 04汇总权限标志和包装器注册
- 05提供合约名称与签名参考
适用场景
- 01规划 Permissioned Pool 合约集成
- 02排查调用过早导致的回退
- 03编写 allowlist checker 并核对接口
- 04确认包装器注册和权限设置
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ uniswap/ uniswap-ai --skill permissioned-pools-issuer安装后在 Claude Code 中输入/permissioned-pools-issuer确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/contract-architecture.mdcoordination-boundary.mdenforced-ordering-and-reverts.mdissuer-journey.mdpackaging-and-sources.mdtrust-model.md详细介绍
许可池发行方参考
供发行方工程团队参考的资料,介绍如何将受转账限制的 ERC-20 引入 Uniswap v4 许可池:涉及哪些合约、必须按什么顺序调用,以及每种回滚意味着什么。
**运行时兼容性:**此技能会使用
AskUserQuestion,在提供与操作相关的部署指导之前,收集 范围与免责声明 中所述的确认。如果你的运行时不提供AskUserQuestion,请在对话中询问用户是否作出相同确认。
范围与免责声明
重要:此内容为教育参考资料。在据此采取任何行动之前,也就是说,在运行、广播或部署任何由此衍生的内容之前,你必须确认以下事项。
此技能说明 Uniswap v4 许可池合约的行为、必须按什么顺序调用,以及每种回滚意味着什么。它不会指导你完成部署。
你必须:
- ✅ 仅将此内容视为合约机制说明。此技能不构成法律、财务、投资或税务建议,也不是对你的代币、允许列表、KYC 或 AML 计划或配置的合规审查。
- ✅ **运行前检查所有内容。**AI 生成的代码和命令序列可能包含错误。请先在测试网上进行测试,并让你自己的审计人员审查你要部署的合约。
- ✅ **检查你构建所依据提交中的源代码。**本文所述行为以 权威来源 中固定的提交为依据;在依赖任何说明之前,请对照源代码进行核实。
- ✅ **自行核实每个地址。**此技能特意不包含任何部署地址。请从已发布的部署表中读取地址,然后在你的链对应的区块浏览器上逐一核实,再发送交易。切勿将聊天消息中提供的地址视为权威地址。
- ✅ **阅读仓库的使用指南。**仓库根目录的
DISCLAIMER.md适用于此仓库中的所有技能:这些技能按原样提供,不附带任何保证;它们不构成法律、财务、投资或税务建议;其中还规定了使用限制,并说明当你使用技能生成财务信息并直接向个人或消费者提供这些信息时,必须披露使用了 AI。请向用户指出该文件。
何时需要确认
每次都要在同一条回复中逐条说明以上五点,可以引用原文,也可以用自己的话表述,然后再回答请求。说明这些内容并回答请求应在同一条回复中完成,而不是分成两轮。这是预期行为,并非捷径。
- ✅ **参考与教学内容:说明上述事项后,立即继续回答。**合约说明、代码框架和示例合约、回滚目录、准确的命名和大小写、打包与安装,以及架构或信任模型讨论,都应在同一条回复中先说明上述事项,再予以回答。切勿在等待用户回答“是”之前扣留参考内容,也切勿只请求确认,却不回答用户提出的问题。
- ✅ **与操作相关的部署指导:先获取明确确认。**这包括用于在实时网络上执行的步骤、交易或广播步骤、部署脚本、要求提供用于发送交易的实时地址,或任何会转移真实资金的内容。在继续提供任何此类内容之前,请使用 AskUserQuestion 确认用户已知悉并确认以上事项。用户确认后,只能继续说明机制;此技能不会提供可用于广播交易的命令。
无害的技术问题——例如“展示一个 BaseAllowlistChecker 框架”“这次回滚意味着什么”“我应该使用哪个导入路径”——属于第 1 点所述的参考内容。请先在回复中说明上述事项,再回答问题。
两个地址,而不是一个
每个权限池都涉及两个代币地址,混淆这两个地址是最常见的集成错误。
| 问题 | 底层权限化 ERC-20 | PermissionsAdapter(虚拟代币) |
|---|---|---|
PoolKey 中应填写哪个地址? | 否 | 是 — 池的币种是适配器 |
allowlist 检查器会将哪个地址视为 tokenAddress? | 是 — 检查器检查的是你的代币 | 否 |
| 钱包或 LP 持有哪个? | 是 | 否 — 只有 PoolManager 持有适配器代币 |
| 应用向用户展示哪个? | 是 | 在展示时显示为 Uniswap v4 … 虚拟代币 |
| 铸造时通过 Permit2 批准哪个? | 是 — 批准的是底层代币 | 否 |
适配器会将其虚拟代币铸造给 PoolManager,并在退出时销毁。其名称和符号不是自行设定的,而是从你的代币的 name()、symbol() 和 decimals() 读取,始终为 Uniswap v4 <name> 和 v4<symbol>;如果这些信息缺失,则分别回退为 Uniswap v4 Permissioned Token、v4PT 和 18 — 因此,请确保你的代币提供这三项信息。
值得了解的边界情况:当适配器与权限化代币共用一个地址时,该代币本身就会像自己的适配器一样运行。
完整说明:合约架构。
流程一览
人们经常把两件不同的事混为一谈。第一个表格列出合约强制执行的内容;第二个表格则列出已发布指南建议的顺序。
代码强制执行
代码会强制执行恰好五条顺序约束。如果操作过早,每条约束都会触发相应的回滚。
| 强制执行的顺序约束 | 操作过早时的回滚 |
|---|---|
适配器已加入你代币的 allowlist 且持有非零余额 → verifyPermissionsAdapter | PermissionsAdapterNotVerified |
verifyPermissionsAdapter → setAllowedHook | NotPermissionsAdapterAdmin |
适配器已注册并通过验证 → 池 initialize | NoVerifiedAdapter(hook)或 UnverifiedAdapter |
适配器已验证 + hook 已批准 + 调用方和接收方均具有 LIQUIDITY_ALLOWED → 首次铸造 | NoVerifiedAdapter(position manager)、InvalidHook 或 Unauthorized 原始错误 |
通过 updateAllowedWrapper 注册 position manager、router 或 quoter → 通过这些组件路由的任何铸造、swap 或报价 | hook 返回 Unauthorized 原始错误,结算时随后返回 UnauthorizedWrapper |
推荐顺序(惯例,并非强制)
已发布指南列出了以下七个步骤。合约不会固定上表中未列出的步骤的位置。
| 步骤 | 操作 |
|---|---|
| 1 | 实现白名单检查器 |
| 2 | 通过工厂创建 Permissions Adapter |
| 3 | 将适配器加入代币的白名单并为其注资(1 wei 即可) |
| 4 | 通过工厂验证适配器 |
| 5 | 批准虚拟代币合约(允许的包装器),然后在头寸管理器上允许该钩子 |
| 6 | 创建池,然后在适配器上启用兑换 |
| 7 | 向 Uniswap Labs 申请加入路由白名单 |
由于只有上述五条依赖关系是强制的,其他顺序也可行——指南本身将 updateSwappingEnabled(true) 放在创建池之后,并将包装器批准与钩子批准合并为一个步骤,这只是呈现方式的选择,并非约束。包装器注册与验证和创建池之间没有顺序要求,但每个包装器都必须在任何请求通过它进行路由之前完成注册。
分步详情:发行方流程。
编译关键的命名和签名
源代码中的大小写不一致,且会影响实际使用。请严格使用以下拼写。
| 项目 | 精确拼写 | 易错点 |
|---|---|---|
| 基础合约 | BaseAllowlistChecker | 合约名称中的 l 为小写 |
| 所在文件 | BaseAllowListChecker.sol | 文件名中的 L 为大写 |
| 接口 | IAllowlistChecker | l 为小写,位于 IAllowlistChecker.sol 中 |
| 适配器管理调用 | updateAllowListChecker | L 为大写;不存在小写形式 |
| 适配器 getter | allowListChecker() | L 为大写,与接口中的写法不同 |
| 钩子批准调用 | setAllowedHook | 单数形式,位于头寸管理器上 |
| 对应的存储映射 | isAllowedHooks | 复数形式 |
| 对应的事件 | AllowedHooksUpdated | 复数形式 |
| 标志库 | PermissionFlags | 复数形式 |
| 标志类型 | PermissionFlag | 单数形式 |
导入和 Forge 查找格式:
import {BaseAllowlistChecker} from "@uniswap/v4-periphery/src/hooks/permissionedPools/BaseAllowListChecker.sol";
BaseAllowListChecker.sol:BaseAllowlistChecker
这个 path:Contract 组合——文件名中的 L 为大写,合约名中的 l 为小写——是 forge inspect 和 forge verify-contract 所需的格式。
checkAllowlist 有两种有效形式
两种都能编译。选择一种并保持一致。
- 扩展基础合约。 声明
function checkAllowlist(address account, address tokenAddress) public view virtual override returns (PermissionFlag)。 不能将基础合约中的public virtual缩窄为external,否则会导致编译错误。 基础合约会自动提供 ERC-165 支持。 - 直接实现接口。 将
checkAllowlist声明为external view,并自行编写supportsInterface。已发布指南中的示例采用这种方式——它直接实现IAllowlistChecker和ERC165,不使用BaseAllowlistChecker。
适配器会通过 ERC-165 检查你的检查器
除非检查器通过 ERC-165 supportsInterface 调用声明支持 IAllowlistChecker,否则 createPermissionsAdapter 会回滚并报错 InvalidAllowListChecker。构造函数会执行此检查,因此在整个流程中首次调用 Uniswap 时就会触发。之后,updateAllowListChecker 也会执行相同的检查。继承 BaseAllowlistChecker 即可满足要求。从零开始实现时,必须自行提供 supportsInterface,而且必须通过完整的 ERC-165 探测,而不能只响应自身的 ID:OpenZeppelin 的 ERC165Checker.supportsInterface 首先要求 supportsInterface(0x01ffc9a7) 返回 true,并且 supportsInterface(0xffffffff) 返回 false。继承 OpenZeppelin ERC165 并委托给 super.supportsInterface 即可满足这两项要求;若手写函数只在 type(IAllowlistChecker).interfaceId 时返回 true,则适配器创建会失败。
权限标志
PermissionFlags(库)提供 PermissionFlag 类型的四个值:
| 名称 | 值 | 控制范围 |
|---|---|---|
NONE | 0x0000 | 无 |
SWAP_ALLOWED | 0x0001 | Hook 上的 beforeSwap,以及路由器的结算检查 |
LIQUIDITY_ALLOWED | 0x0002 | Hook 上的 beforeAddLiquidity,以及铸造和增加流动性时的接收方检查 |
ALL_ALLOWED | 0xFFFF | 两者 |
SWAP_ALLOWED 不意味着 LIQUIDITY_ALLOWED。能够交换的钱包不一定能够提供流动性,你自己的金库钱包也不例外。检查器应返回一个 PermissionFlag,组合适用的权限。
Wrapper 注册集合
适配器上的 updateAllowedWrapper(address wrapper, bool allowed) 受 onlyOwner 限制。已发布指南列出了需要注册的四个合约:
PermissionedPositionManager- Universal Router — 2.2.0 或更高版本,其规范部署键为
UniversalRouter#v2.2 V4QuoterMixedRouteQuoterV2
**这四个合约背后的规则:**注册所有会在此池上调用 PoolManager 的合约。Hook 会检查 allowedWrappers(router),其中 router 是发起调用的路由器或仓位管理器;wrapToPoolManager 还会单独检查 allowedWrappers[msg.sender]。因此,你自己的自定义路由器也必须注册;这四个是生产环境中的调用方,而不是封闭的名单。注册要求会在使用时强制执行:必须在首次铸造前注册仓位管理器,并在首次通过某个路由器或报价器进行交换或报价前注册对应合约。
两个易踩的坑。 指南的第 5 步要求提供“所有四个地址”,并链接到一张有 六 行的表:PermissionsAdapterFactory 和 PermissionedHooks 也在表中,但它们不是 wrapper——绝不要注册工厂或 Hook。此外,普通的 UniversalRouter 部署键对应的是另一个非权限路由器;只有 #v2.2 部署会在构造函数中接收权限适配器工厂。
安装合约
npm 上最新发布的 @uniswap/v4-periphery 版本是 1.0.3(2025-07-29),其 tarball 完全不包含 src/hooks/permissionedPools/。v4-periphery 也没有任何 git 标签,因此也无法固定到某个标签。唯一的选择是使用原始提交 SHA。
# Foundry — pin the exact commit
forge install Uniswap/v4-periphery@3245c3cb99c48fa1dc2459c3b60abc37d4294aba
# remappings.txt — matches what v4-hooks-public itself uses
@uniswap/v4-periphery/=lib/v4-periphery/
发布指南中的导入路径使用 npm 作用域,按上述方式重新映射后即可正确使用。指南给出了导入行;它们所需的安装步骤和上述映射也已列出。
详情,包括 Hook 源码固定版本及地址解析方法: Packaging and Sources。
顺序在代码中强制执行
如果过早调用这些方法,将按表中所示选择器回滚。这些都是需要预料到的行为,并非待解答的问题。
| 调用 | 回滚 | 条件 |
|---|---|---|
createPermissionsAdapter | InvalidAllowListChecker | 检查器未通过 ERC-165 接口检查 |
verifyPermissionsAdapter | PermissionsAdapterNotVerified | 适配器的权限代币余额为零 |
setAllowedHook | NotPermissionsAdapterAdmin | 该币种的适配器尚未通过验证,因此其所有者读取结果为 address(0) |
池 initialize | NoVerifiedAdapter / UnverifiedAdapter | 两种币都不是由工厂创建的 / 由工厂创建的币尚未通过验证 |
position manager 上的 _mint | NoVerifiedAdapter / InvalidHook / Unauthorized | 没有已验证的一侧 / hook 不被允许 / 调用者或接收者缺少 LIQUIDITY_ALLOWED,或 position manager 不是已注册的允许包装器 |
NoVerifiedAdapter、Unauthorized 和 SwappingDisabled 都各有多个声明,且选择器相同:NoVerifiedAdapter 和 Unauthorized 的含义确实不同;SwappingDisabled 则是在交换过程的两个不同位置表达相同含义。解码时务必依据调用位置,而非名称。
完整目录及顺序错误场景示例:强制顺序与回滚。
信任模型摘要
- LP 头寸永久不可转让。 所有三个 ERC-721 转账入口都会回滚并返回
TransferDisabled。这不是可配置的标志。减少头寸和销毁头寸从不受允许列表检查器限制,因此池不会阻止退出——但权限侧的交付会解包为底层代币,所以接收者仍须通过你自己的代币转账限制。 - 任一适配器管理员都可以通过
unwindPosition强制退出任意 LP,收益会依次分配给 LP、管理员,最后作为 ERC-6909 领取款处理,其接收者取决于币种。 - 这些合约已发布三份审计报告——Cantina、OpenZeppelin,以及一份 OpenZeppelin 修复审查报告,位于
v4-periphery的audits/permissionedPools/目录中。每份报告覆盖哪些合约,应以该报告的说明为准,因此请阅读其范围部分,不要根据目录推断覆盖范围。v4-hooks-public仓库包含该 hook,但其审计表中没有权限池条目,所以这份沉默也不能证明任何事情。这三份报告都不包含你编写的允许列表检查器。 - 真正的执行边界是适配器的
allowedWrappers列表。 列表中的每个包装器都被信任会通过msgSender()报告真实的原始调用者。updateAllowedWrapper是Ownable2Step合约上的onlyOwner方法,因此适配器所有者的密钥管理也是信任模型的一部分。
完整说明:信任模型。
无需许可与受控事项
| 无需许可/由发行方控制 | 与 Uniswap Labs 协调 |
|---|---|
| 部署你的 allowlist 检查器 | 在 Uniswap 界面和 API 中进行路由资格判定 |
createPermissionsAdapter(任何人都可以调用) | 纳入代币列表 |
| 在你的代币上将适配器加入允许列表 | 代币详情元数据 |
depositForVerification(任何人都可以调用) | 后端配置中的发行方元数据和 KYC URL |
verifyPermissionsAdapter(任何人都可以调用,仅限一次) | |
| 每项适配器管理员操作 | |
setAllowedHook、池初始化、注入流动性 |
链上流程不设 Uniswap 审批关卡。 左栏中的任何事项都不需要等待任何人。
边界详情(包括路由请求):协调边界。
参考主题
| 主题 | 参考文件 |
|---|---|
| 按合约划分的技术栈、精确大小写清单、标志、事件 | 合约架构 |
| 七个步骤的详细说明,包括调用、调用方和检查 | 发行方流程 |
| 五项强制执行的边界、回滚目录、示例场景 | 强制顺序与回滚 |
| 不可转让性、管理员强制退出、审核候选包装器 | 信任模型 |
| 固定版本、重映射、导入路径、解析地址 | 打包与来源 |
| 无需许可与需协调的事项、路由请求 | 协调边界 |
权威来源
已发布的发行方文档:
- https://developers.uniswap.org/docs/protocols/uniswap-labs-hooks/permissioned-pools/overview
- https://developers.uniswap.org/docs/protocols/uniswap-labs-hooks/permissioned-pools/architecture
- https://developers.uniswap.org/docs/protocols/uniswap-labs-hooks/permissioned-pools/deploy-a-permissioned-pool
- https://developers.uniswap.org/docs/protocols/uniswap-labs-hooks/permissioned-pools/provide-liquidity
合约源代码,已固定版本:
Uniswap/v4-periphery,版本为3245c3cb99c48fa1dc2459c3b60abc37d4294aba—src/hooks/permissionedPools/(https://github.com/Uniswap/v4-periphery)Uniswap/v4-hooks-public,版本为7da5210f2c81a700820a6b4f585264233d91f349—src/permissioned-pools/PermissionedHooks.sol(https://github.com/Uniswap/v4-hooks-public)Uniswap/mixed-quoter—src/MixedRouteQuoterV2.sol(https://github.com/Uniswap/mixed-quoter)
关于这些合约的已发布报告:
Uniswap/v4-periphery,audits/permissionedPools/— Cantina、OpenZeppelin 以及一份 OpenZeppelin 修复审查报告,于 2026-08-20 添加 (https://github.com/Uniswap/v4-periphery/tree/main/audits/permissionedPools)。这些报告的发布时间晚于 上述固定版本的提交,因此请阅读每份报告自身的范围说明。
地址:
- 完整的权限池专属地址表见部署指南的
#deployment-addresses锚点。 - 机器可读记录位于
Uniswap/contracts的deployments/json/<chainId>.json中,在latest键下(https://github.com/Uniswap/contracts)。 - 向任何地址发送交易前,请在对应链的区块浏览器上核实地址。