作者: Cyimon ( @Cyimon ) ·状态:草案 ·类型:标准跟踪 (ERC) · EIP: 8287 (PR) ·创建日期: 2026-06-09
描述: EVM 的可互换代币标准,默认情况下是私有的。
讨论: ethresear.ch #25089 · Ethereum Magicians #28702 ·实现: PERC20Labs/pERC20_
我们扩展了之前发布的 pERC20 协议设计( Ethereum Magicians #28702 / ethresear.ch #25089 )。此次修订的主要新增功能是通过ZIP-32子账户实现 ERC-20 认可的支出——包括approve 、 allowance和transferFrom 。更新后的标准在功能上与ERC-20完全兼容,但在字节上不兼容(ABI 不同,且不支持公开余额)。
| ERC-20 | pERC20 | 层 |
|---|---|---|
name / symbol / decimals / totalSupply | 是的——公众观点相同 | 链上 |
balanceOf | 是的——仅限持卡人扫描 | 链下 |
transfer | 是的——私人聚会及金额 | 链上 |
approve / allowance / transferFrom | 新增——已批准EOA支出者通过ZIP-32子账户进行支出;链上= transfer (不支持合约支出者) | 链下 |
mint / burn | 是的——常用扩展 | 链上 |
Transfer / Approval事件 | 省略(隐私) | — |
以下是最新的 pERC20 标准:私有代币标准。
抽象的
pERC20是 EVM 的默认私有同质化代币标准,是ERC-20的隐私版本。其底层采用Zcash协议规范中的 Orchard 屏蔽池模型。它保留了 ERC-20 的所有方法,但部分方法是私有的(仅限持有者访问,链下),而非公开的链上读取,并且transfer / approve / transferFrom在链上显示为相同的transfer操作。请参阅下方的pERC20 接口。
动机
以太坊的公共账本使所有ERC-20余额、转账和授权永久可见。随着支付、工资、国库和链上金融迁移到 L1 层,用户和发行方需要的是私有的同质化代币,而不仅仅是围绕公共余额的私密信息交流。
隐私问题越来越多地在协议层得到解决。例如, EIP-8182定义了一个协议层面的保护池:用户可以存入公开的ETH或兼容的 ERC-20 代币,在共享池内进行私密转移,然后再提取回公开形式。该模型保护了现有的公共资产;但它并未定义如何发行从创建之初就具有私密性的代币。
pERC20填补了后一个空白。它是一个应用层代币标准,用于原生私有同质化代币:从一开始就以私有票据的形式通过approve / transferFrom进行铸造、持有、转移和使用,没有公开的balanceOf阶段,也不存入共享的屏蔽池。它定义了ERC-20的私有对应物——相同的方法界面,不同的开放程度——因此发行方可以立即发行私有资产,同时协议级隐私(例如 EIP-8182)也在同步发展。两者是互补的,而非竞争的:EIP-8182 将公共资产私有化; pERC20定义了私有资产的发行。
规格
关键词MUST 、 MUST NOT 、 SHOULD 、 MAY的解释遵循 RFC 2119。Solidity 语法版本为0.8.20或更高版本。
底层协议
价值以屏蔽票据的形式存在,而非账户余额。票据格式、作废符、承诺树、票据加密以及操作/捆绑结构均遵循Zcash协议规范中的Orchard 屏蔽池,并在此处改编为基于资产的 EVM 合约,且已通过 Groth16 验证。下文未重复的字段级格式在参考实现部分中具有规范性。
pERC20接口
本节将所有 pERC20 接口集中列出,并标记每个接口是否对应于ERC-20标准接口。ERC -20 : yes = ERC-20 标准; extension = 通用扩展(铸造/销毁); no = pERC20 特有。层级: on-chain = 合约 ABI; off-chain = 钱包/SDK(无合约方法)。
| pERC20接口 | ERC-20 | 层 | 开放性 | 描述 |
|---|---|---|---|---|
name() / symbol() / decimals() | 是的 | 链上 | 民众 | 与 ERC-20 相同 |
totalSupply() | 是的 | 链上 | 民众 | 公共计数器( mint - burn ) |
balanceOf(addr) | 是的 | 链下 | 私人的 | 使用查看密钥扫描Orchard纸币;仅限持卡人使用 |
transfer(PrivacyCall) | 是的 | 链上 | 私人(参与方 + 金额) | 果园行动包 |
approve(spender, N) | 是的 | 链下 | 私密(关系已隐藏) | ZIP-32子账户;资金 + 交付密钥;链上提交为transfer(PrivacyCall) |
allowance(owner, spender) | 是的 | 链下 | 私人的 | 扫描子账户剩余余额 |
transferFrom(from, to, amount) | 是的 | 链下 | 私密(关系已隐藏) | 支出者在子账户中支出;链上提交为transfer(PrivacyCall) |
mint(amount, PrivacyCall) | 扩大 | 链上 | 金额公开;收款人私密 | 仅限发行方;果园行动 + totalSupply量增加 |
burn(amount, PrivacyCall) | 扩大 | 链上 | 公开金额;私人燃烧器 | 持票人烧毁自己的票据;果园行动 + totalSupply减少 |
issuer() | 不 | 链上 | 民众 | 代币发行者地址 |
cmxFrozenRoot() / setFrozenRoot() | 不 | 链上 | 公共根目录;管理员写入 | 合规冻结记录根 |
cmxRoot() / isValidAnchor() / isSpent() / treeSize() | 不 | 链上 | 民众 | 果园承诺树状态 |
活动
| pERC20事件 | ERC-20 | 层 | 描述 |
|---|---|---|---|
Transfer(from, to, value) | 是的 | 链下(省略) | 未公开;参与方和金额均属私人信息。 |
Approval(owner, spender, value) | 是的 | 链下(省略) | 未发行;将所有者与消费者关联 |
NoteAdded / NoteConfirmed | 取代Transfer | 链上 | 单张票据可观察性 |
Mint / Burn | 扩大 | 链上 | 公开金额 |
Perc20Created / FrozenRootUpdated / BundleExecuted | 不 | 链上 | 部署、合规性、捆绑元数据 |
链上操作不可区分。 transfer 、 approve 、 transferFrom和撤销操作的资金步骤在链上都是同一个调用: transfer(PrivacyCall) 。观察者无法分辨正在执行的是哪个 ERC-20 操作。
原生不支持。ERC -20 的 ` approve(contractAddress, amount) `(合约自主调用transferFrom )没有原生等效项:支出需要私钥,而合约无法持有私钥。参见原理。
合同接口
pERC20公开了一个链上 ABI( IPERC20 ;参见上文的pERC20 接口)。该表中标记为链下的方法没有合约入口点;其行为在下文“方法语义”中指定。
interface IPERC20 {struct PrivacyCall { bytes actions; uint256[3] bindingSig; }struct BundleAction {bytes32 cmx;bytes encCiphertext;bytes outCiphertext;bytes32 epk;bytes32 nfOld; // nullifier of the consumed (or dummy) input notebytes32 anchor; // historical root of the consumed (or dummy) input notebytes proof;uint256[8] pubFields;uint256[3] spendAuthSig;}// ERC-20-aligned public viewsfunction name() external view returns (string memory);function symbol() external view returns (string memory);function decimals() external view returns (uint8);function totalSupply() external view returns (uint256);function issuer() external view returns (address);// Value-changing operations (private parties; see Method Semantics)function transfer(PrivacyCall calldata call) external returns (bool success);function mint(uint256 amount, PrivacyCall calldata call) external;function burn(uint256 amount, PrivacyCall calldata call) external;// Compliancefunction cmxFrozenRoot() external view returns (uint256);function setFrozenRoot(uint256 newRoot) external; // onlyAdmin// Note state machine (Orchard commitment tree)function cmxRoot() external view returns (bytes32);function isValidAnchor(bytes32 root) external view returns (bool);function isSpent(bytes32 nf) external view returns (bool);function treeSize() external view returns (uint256);event Mint(address indexed issuer, uint256 amount);event Burn(uint256 amount);event FrozenRootUpdated(uint256 oldRoot, uint256 newRoot);event Perc20Created(address indexed pool, address indexed issuer,string name, string symbol, uint8 decimals);event NoteAdded(bytes32 indexed cmx, bytes encCiphertext, bytes outCiphertext,bytes32 epk, bytes32 nfOld, bytes32 cvNetX);event NoteConfirmed(bytes32 indexed cmx, bytes32 newRoot, uint256 position);event BundleExecuted(uint256 valueBalance, uint256 amount, bytes32 recipientMeta);}符合性:
-
transfer成功必须返回true。 - 核心包执行路径绝对不能公开调用(供应不变式;见下文)。
- 实现可以将 Solidity 拆分为多个合约(例如
IPERC20+ 验证器基础),但可观察的 ABI 和事件必须与上述统一接口匹配。 -
cmxRoot()是最新的承诺树根;isValidAnchor(root)如果root曾经是活跃的则返回 true;isSpent(nf)公开空值集;treeSize()是插入的承诺数。
呼叫格式
每次更改值的操作都会提交一个PrivacyCall编码一个或多个Orchard 操作( Zcash协议规范):
-
actions=abi.encode(BundleAction[]). -
bindingSig= Schnorr 绑定签名[Rx, Ry, s]证明值守恒。
每个BundleAction都是pubFields适用于 EVM 验证的 Orchard 操作:一份输出票据承诺 ( cmx ) 以及其(真实或虚拟)输入的证明材料。pubFields 必须按以下顺序排列(Orchard 操作的主要输入):
| 指数 | 场地 | 角色 |
|---|---|---|
[0] | anchor | 消耗输入的默克尔根 |
[1] | cv_net_x | 净值承诺 X(具有约束力的签名) |
[2] | cv_net_y | 净值承诺 Y(具有约束力的签名) |
[3] | nf_old | 消耗输入的无效化 |
[4] | rk_x | 随机消费授权密钥 X |
[5] | rk_y | 随机消费授权密钥 Y |
[6] | cmx | 输出说明承诺 |
[7] | rt_frozen | 合规性冻结根绑定 |
每个pubFields[i]必须< Fr ,其中Fr是验证器(参考实现中为 BN254)使用的 SNARK 曲线的标量场模;否则,将返回错误( PubFieldOutOfRange )。实现必须通过ActionPubHash (Poseidon sponge)将这八个字段哈希到一个 Groth16 公共信号中,使其与电路的PubHashAction()相匹配。
绑定到调用数据字段。证明公共输入必须与操作的顶级字段匹配;如果出现以下任何情况,实现必须回滚:
| 查看 | 恢复 |
|---|---|
pubFields[0] == anchor且isValidAnchor(anchor) | BadAnchor |
pubFields[3] == nfOld | 必须还原(参考实现: NullifierSpent ) |
pubFields[6] == cmx且cmx != 0 | InvalidProof / ZeroCommitment |
pubFields[7] == cmxFrozenRoot() | BadFrozenRoot |
spendAuthSig验证pubFields[4]和pubFields[5]是否在(nfOld, cmx, epk, encCiphertext, outCiphertext)范围内。 | BadSpendAuthSig |
如果没有pubFields ↔ calldata 相等性检查,有效的证明可以用不同的nfOld或cmx重放,绕过空值集或插入未经证明的承诺。
encCiphertext必须为 580 字节(Orchard 笔记outCiphertext + 收件人密钥下的 AEAD 标签)。outCiphertext 应为 80 字节(OVK 下的发送方自恢复)。更改笔记加密布局的实现必须发布此 ERC 的单独变体。
在进行任何状态变更之前,必须对所有操作无效符、承诺和操作的valueBalance验证捆绑包级别的bindingSig (有关编码,请参阅方法语义)。
注意加密和密钥派生遵循Zcash协议规范中的 Orchard 笔记格式;确切的编码在参考实现库中。
方法语义
name / symbol / decimals / totalSupply
与 ERC-20 相同:公开的链上视图。
transfer(PrivacyCall) → bool
使用Orchard票据作为输入,并创建一个价值守恒的操作包( valueBalance == 0 )。发送者、接收者和金额必须保持私密。成功时返回true ;发出NoteAdded / NoteConfirmed ,而不是Transfer(from,to,value) 。
mint(amount, PrivacyCall) / burn(amount, PrivacyCall)
与transfer相同的Orchard操作验证路径,并公开totalSupply统计:
-
mint:totalSupply += amount(onlyIssuer);amount公开,接收方私有。铸币必须使用与transfer相同的锚点/无效符/支出授权路径(无仅输出分支)。电路必须将消耗的输入限制为v = 0,以便该操作代表净流入;合约不直接读取票据价值。 -
burn:totalSupply -= amount;任何持有人均可销毁自己的票据;amount公开,销毁者私密。
| 手术 | valueBalance编码 |
|---|---|
transfer | 0 |
burn | bit255 = 0,低位 = 金额 |
mint | bit255 = 1,低位 = 金额 |
balanceOf (链下,私有)
只有持有者才能通过扫描NoteAdded事件、使用查看密钥试解密Orchard笔记并排除已花费的无效符来计算余额。链上没有balanceOf ,也无法查询第三方的余额。
approve / allowance / transferFrom (链下语义;链上 = transfer )
已批准的支出( approve / allowance / transferFrom )基于ZIP-32分层账户:每个EOA 支出者都会获得一个专用的子账户( account_S ),该子账户拥有自己的支出密钥和查看密钥,并与所有者的主账户以及所有其他支出者进行加密隔离。ZIP -32定义了密钥派生;此 ERC 将 ERC-20 的approve / transferFrom映射到 ZIP-32,具体如下:
-
approve(spender, N)— 所有者创建一个未使用的 ZIP-32 子账户,通过transfer(PrivacyCall)向其充值N,并将该子账户的支出密钥交付给 EOA 支出者(链下加密)。链上操作:一次transfer。 -
allowance(owner, spender)— 该子账户的剩余余额,使用子账户查看密钥扫描。无链上映射。 -
transferFrom(owner, to, amount)— 支出者从子账户支出to目标账户;找零返回到子账户。链上:一次transfer。 -
approve(spender, 0)/ revoke — 所有者通过transfer将子账户退回。
限额由子账户的实际票据余额决定,而非链上计数器。钱包区分“自有”资产和“限额”资产的方式,是依据解密票据的查看密钥,而非链上标记。
执行要求
链上状态机遵循 Orchard 屏蔽池( Zcash协议规范)。实现必须:
- 维护一个无效化符集;同一个无效
nf不能使用两次。 - 维护一个仅追加的承诺树;可通过
isValidAnchor查询历史根节点。 - 验证 Groth16 证明、支出认证和绑定签名,以及所有 pubFields 绑定检查(不仅仅是
pubFields[7])。 - 拒绝重复或零承诺;拒绝空操作数组;限制每次调用的操作数(
maxActions,一个有限的可配置正界限)。 - 仅通过
mint/burn/transfer公开价值变更(没有公共捆绑包入口点)。 - 在部署时发出
Perc20Created一次(建议进行工厂部署,但并非必须)。
合规性。cmxFrozenRoot cmxFrozenRoot()是链下黑名单 SMT 的根;该电路用于证明已花费的笔记不属于该黑名单setFrozenRoot仅限admin使用;初始根0表示黑名单为空。实现可以在更新后的短暂宽限期内接受紧邻的前一个根,以避免正在进行的证明被搁置。
理由
- Orchard ZK-UTXO 模型。注释、无效化符和承诺树遵循Zcash协议规范;此 ERC 定义了私有代币接口和在 EVM 上的每个资产的部署。
- 私有 ERC-20 代币,并非不同的资产。方法表面相同;隐私改变的是开放性(公开查看 vs 私密查询 vs 不可区分的传输),而不是用户意图。
- 所有资金转移只需一次链上操作。合并
transfer/approve/transferFrom会移除 ERC-20 不可避免的授权元数据泄露。 - 通过ZIP-32子账户批准支出。每个 EOA 支出者都会获得一个独立的层级账户,而不是链上
allowance映射;参见方法语义。 - 无需
approve(contract)。合约没有私钥;将支出密钥放在链上会使其暴露给所有人。可编程的私有支出(谓词授权的票据、MPC托管)是未来的工作,不属于本次ERC的范围。 - 钱包行为不在链上ABI的范围内。子账户布局、加密密钥交付和票据扫描属于钱包/SDK的职责范围;请参阅参考实现。
向后兼容性
pERC20具备ERC-20 的所有功能,但与字节不兼容:没有公开的balanceOf属性,没有链上allowance ,没有approve / transferFrom ABI,也没有Transfer / Approval事件。现有的 ERC-20 索引器和可组合合约在没有隐私保护钱包/SDK 的情况下无法驱动 pERC20。
可选的bridgeOut到公共 ERC-20 孪生服务器可能会在出口处终止隐私;本提案不要求这样做。
测试用例
参考实现库包含:
- 铸造单元测试(
test/PERC20Test.t.sol):构造函数保护、铸造/销毁/转移会计、供应不变量。 - 端到端测试(
test/PERC20E2E.t.sol,e2e/):针对已部署的PERC20的真实 Groth16 证明,涵盖铸造、转移、销毁和approve/transferFrom流程。
参考实现
代码
参考实现: PERC20Labs/pERC20_ 。
- 规范性资产合约:
contracts/ptoken/PERC20.sol(IPERC20)。 - 加密和钱包格式(密钥派生、
perc1地址、笔记加密、无效化器、approve打包):参考库和 SDK 在同一个存储库中。
相关标准和协议
- EIP-20 :代币标准
pERC20映射到的公共可互换代币接口。 - Zcash协议规范:Orchard 屏蔽池 — 注意承诺、无效化器、笔记加密和操作结构,此处针对 EVM Groth16 验证进行了调整。
- ZIP-32 :受保护的分层确定性钱包——用于在
approve/transferFrom中按消费者子账户的分层账户派生。
安全考量
- 双花保护:空值设置 + 正确的
nf推导。每个pubFields[i]必须< Fr(否则nf + Fr会重用具有不同isSpent键的证明);pubFields[0]、[3]、[6]必须分别等于anchor、nfOld、cmx(否则有效的证明可以绑定到不同的 calldata)。 - 供应不变性:价值仅通过
mint/burn/transfer改变;核心执行路径不得公开调用。 - 价值守恒:在状态突变之前验证结合特征。
- 重放保护:sighash 绑定
chainId、合约地址和所有nf/cmx。 - 子账户支出密钥:用于
approve密钥必须仅以密文形式出现;绝不能存储在合约中。 - 合规权限:
setFrozenRoot是一个高信任度的管理员角色;应该使用多重签名/时间锁。
隐私考量
- 操作应该通过中继器提交,以隐藏提交者的EOA。
-
mint/burnamount和totalSupply是公开的;转账金额和approve关系是私密的。 -
approve/transferFrom在链上与transfer无法区分(相同的transfer(PrivacyCall)选择器);mint/burn是具有公开数量的独立函数。 - 试解密是收款的信任边界:仅凭
NoteAdded事件无法证明付款;电路不会验证encCiphertext是否与cmx匹配。 -
setFrozenRoot允许管理员通过链下黑名单冻结已识别的笔记;这是对完全不信任的一种明确的合规性权衡。
版权
通过CC0放弃版权及相关权利。
请引用此文档:Cyimon,“ERC-8287:私有代币标准”,以太坊改进提案,2026 年 6 月。



