原文
# pERC20: Private Token Standard (Draft) — JiangXb-son (2026-06-15)
著者: 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と機能的に完全互換 (capability-complete) ですが、バイト互換性はありません (異なるABI、公開残高なし)。
| ERC-20 | pERC20 | レイヤー |
|---|---|---|
| name / symbol / decimals / totalSupply | はい — 同じ公開ビュー | オンチェーン |
| balanceOf | はい — ホルダーのみのスキャン | オフチェーン |
| transfer | はい — プライベートな当事者と金額 | オンチェーン |
| approve / allowance / transferFrom | 新規 — ZIP-32 サブアカウントを介したEOAスペンダー向けの承認済み支出。オンチェーン = transfer (コントラクトスペンダーはサポートされません) | オフチェーン |
| mint / burn | はい — 一般的な拡張 | オンチェーン |
| Transfer / Approval イベント | 省略 (プライバシー) | — |
以下は最新のpERC20標準: プライベートトークン標準です。
概要
pERC20 は、EVM向けのデフォルトでプライベートなファンジブルトークン標準です。これはERC-20のプライバシーバージョンです。内部的には、Zcashプロトコル仕様のOrchardシールドプールモデルを使用しています。ERC-20の全メソッドインターフェース (method surface) を維持していますが、いくつかのメソッドは公開オンチェーン読み取りではなくプライベート (ホルダーのみ、オフチェーン) であり、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シールドプールに従い、ここではGroth16で検証されるアセットごとのEVMコントラクトとして適応されています。以下に繰り返されないフィールドレベルの形式は、参照実装セクションで規範的です。
pERC20インターフェース
このセクションでは、すべてのpERC20インターフェースを一箇所にリストし、それぞれがERC-20標準インターフェースに対応するかどうかを示します。ERC-20: yes = ERC-20標準; extension = 一般的な拡張 (mint/burn); no = pERC20固有。レイヤー: on-chain = コントラクトABI; off-chain = ウォレット/SDK (コントラクトメソッドなし)。
| pERC20インターフェース | ERC-20 | レイヤー | 公開性 | 説明 |
|---|---|---|---|---|
| name() / symbol() / decimals() | yes | オンチェーン | public | ERC-20と同一 |
| totalSupply() | yes | オンチェーン | public | 公開カウンター (mint − burn) |
| balanceOf(addr) | yes | オフチェーン | private | ビューイングキーでOrchardノートをスキャン; ホルダーのみ |
| transfer(PrivacyCall) | yes | オンチェーン | private (当事者 + 金額) | Orchardアクションバンドル |
| approve(spender, N) | yes | オフチェーン | private (関係は隠蔽) | ZIP-32 サブアカウント; キーを資金提供 + 配信; オンチェーンではtransfer(PrivacyCall)として提出 |
| allowance(owner, spender) | yes | オフチェーン | private | サブアカウントの残り残高をスキャン |
| transferFrom(from, to, amount) | yes | オフチェーン | private (関係は隠蔽) | スペンダーがサブアカウントから to に支払う; お釣りはサブアカウントに戻る。オンチェーンではtransfer(PrivacyCall)として提出 |
| mint(amount, PrivacyCall) | extension | オンチェーン | 金額は公開; 受取人はプライベート | 発行者のみ; Orchardアクション + totalSupply増加 |
| burn(amount, PrivacyCall) | extension | オンチェーン | 金額は公開; バーナーはプライベート | ホルダーが自身のノートをバーン; Orchardアクション + totalSupply減少 |
| issuer() | no | オンチェーン | public | トークン発行者アドレス |
| cmxFrozenRoot|cmxFrozenRoot] / setFrozenRoot|setFrozenRoot] | no | オンチェーン | 公開ルート; 管理者書き込み | コンプライアンス凍結ノートルート |
| cmxRoot|cmxRoot] / isValidAnchor|isValidAnchor] / isSpent|isSpent] / treeSize|treeSize] | no | オンチェーン | public | Orchardコミットメントツリーの状態 |
イベント
| pERC20イベント | ERC-20 | レイヤー | 説明 |
|---|---|---|---|
| Transfer(from, to, value) | yes | オフチェーン (省略) | 発行されません; 当事者と金額はプライベートです |
| Approval(owner, spender, value) | yes | オフチェーン (省略) | 発行されません; owner ↔ spenderをリンクすることになります |
| NoteAdded / NoteConfirmed | Transferを置き換え | オンチェーン | ノートごとの可視性 |
| Mint / Burn | extension | オンチェーン | 公開金額のみ |
| Perc20Created / FrozenRootUpdated / BundleExecuted | no | オンチェーン | デプロイメント、コンプライアンス、バンドルメタデータ |
オンチェーンでの区別不能性 (On-chain indistinguishability)。 transfer、approve の資金提供ステップ、transferFrom、および取り消し・回収はすべて同じオンチェーン呼び出しです: transfer([[glossary/PrivacyCall|PrivacyCall]])。オブザーバーはどのERC-20操作が実行されているかを判別できません。
ネイティブにはサポートされない。 ERC-20の approve(contractAddress, amount) — コントラクトが自律的に transferFrom を呼び出す — にはネイティブな同等物がありません。支出にはプライベートキーが必要であり、コントラクトはそれを保持できません。理由 (Rationale) を参照してください。
コントラクトインターフェース
pERC20 は1つのオンチェーン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 note
bytes32 anchor; // historical root of the consumed (or dummy) input note
bytes proof;
uint256[8] pubFields;
uint256[3] spendAuthSig;
}
// ERC-20-aligned public views
function 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;
// Compliance
function 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をMUST返します。 - コアのバンドル実行パスは公開呼び出し可能でMUST NOTありません (供給不変条件; 以下を参照)。
- 実装はSolidityを複数のコントラクトに分割してもMAY構いません (例:
IPERC20+ 検証者ベース) が、観測可能なABIとイベントは上記の統一されたインターフェースとMUST一致します。 cmxRoot()は最新のコミットメントツリーのルートです;isValidAnchor(root)はrootが過去にアクティブであった場合にのみtrueを返します;isSpent(nf)はナリファイアセットを公開します;treeSize()は挿入されたコミットメントの数です。
呼び出し形式
すべての値を変更する操作は、1つ以上のOrchardアクション (Zcashプロトコル仕様) をエンコードするPrivacyCallを提出します:
actions=abi.encode(BundleAction[])。bindingSig= 価値保存を証明するシュノアバインディング署名[Rx, Ry, s]。
各BundleActionは、EVM検証用に適応されたOrchardアクションです: 1つの出力ノートコミットメント (cmx) と、その (実またはダミーの) 入力に対する証明マテリアル。pubFieldsは以下の順序でMUST並べられます (Orchardアクションの主要入力):
| インデックス | フィールド | 役割 |
|---|---|---|
| [0] | anchor | 消費された入力のマークルルート |
| [1] | cv_net_x | 正味価値コミットメントX (バインディング署名) |
| [2] | cv_net_y | 正味価値コミットメントY (バインディング署名) |
| [3] | nf_old | 消費された入力のナリファイア |
| [4] | rk_x | ランダム化されたspend-authキーX |
| [5] | rk_y | ランダム化されたspend-authキーY |
| [6] | cmx | 出力ノートコミットメント |
| [7] | rt_frozen | コンプライアンス凍結ルートバインディング |
各 pubFields[i] は < Fr でMUSTなければなりません。ここで Fr は検証者が使用するSNARK曲線のスカラー体モジュラスです (参照実装ではBN254)。そうでない場合はリバートします (PubFieldOutOfRange)。実装は、8つのフィールドを ActionPubHash (ポセイドンスポンジ) を介して1つのGroth16公開シグナルにハッシュ化し、回路の PubHashAction() と一致させるMUST必要があります。
calldataフィールドへのバインディング。 証明の公開入力はアクションのトップレベルフィールドとMUST一致します。実装は、以下のいずれかが失敗した場合にMUSTリバートします:
| チェック | リバート |
|---|---|
| pubFields[0] == anchor and isValidAnchor(anchor) | BadAnchor |
| pubFields[3] == nfOld | MUSTリバート (参照実装: NullifierSpent) |
| pubFields[6] == cmx and cmx != 0 | InvalidProof / ZeroCommitment |
| pubFields[7] == cmxFrozenRoot() | BadFrozenRoot |
| spendAuthSig verifies under pubFields[4], pubFields[5] over (nfOld, cmx, epk, encCiphertext, outCiphertext) | BadSpendAuthSig |
pubFields ↔ calldataの等価性チェックがないと、有効な証明が異なる nfOld または cmx でリプレイされ、ナリファイアセットをバイパスしたり、証明されていないコミットメントを挿入したりする可能性があります。
encCiphertextは580バイト (MUST Orchardノートプレーンテキスト + 受取人キー下のAEADタグ)。outCiphertextは80バイト (SHOULD 送信者自己回復のためのOVK下)。ノート暗号化レイアウトを変更する実装は、このERCの別のバリアントを公開MUSTする必要があります。
バンドルレベルのbindingSigは、状態変更の前に、すべてのアクションナリファイア、コミットメント、および操作のvalueBalanceに対して検証されるMUST必要があります (エンコーディングについてはメソッドセマンティクスを参照)。
ノート暗号化とキー導出は、Zcashプロトコル仕様のOrchardノート形式に従います。正確なエンコーディングは参照実装リポジトリにあります。
メソッドセマンティクス
name / symbol / decimals / totalSupply
transfer(PrivacyCall) → bool
入力Orchardノートを消費し、価値保存アクションバンドル (valueBalance == 0) で出力ノートを作成します。送信者、受取人、および金額はプライベートのままMUSTです。成功時に true を返します; Transfer(from,to,value) ではなく、NoteAdded / NoteConfirmed を発行します。
mint(amount, PrivacyCall) / burn(amount, PrivacyCall)
transfer と同じOrchardアクション検証パスで、公開 totalSupply 会計が行われます:
mint:totalSupply += amount(onlyIssuer);amountは公開、受取人はプライベート。Mintはtransferと同じアンカー / ナリファイア / spend-auth パスをMUST使用します (出力のみのブランチはありません)。回路は消費された入力をv = 0に制約し、アクションが純流入を表すようにMUSTします。コントラクトはノートの値を直接読み取りません。burn:totalSupply -= amount; 任意のホルダーは自身のノートをバーンMAYできます;amountは公開、バーナーはプライベート。
| 操作 | valueBalanceエンコーディング |
|---|---|
| transfer | 0 |
| burn | bit255 = 0, 下位ビット = amount |
| mint | bit255 = 1, 下位ビット = amount |
balanceOf (オフチェーン、プライベート)
ホルダーのみが、NoteAddedイベントをスキャンし、ビューイングキーでOrchardノートを試行復号 (trial-decrypt) し、消費されたナリファイアを除外することで、自身の残高を計算できます。オンチェーンの balanceOf はなく、第三者の残高をクエリする方法もありません。
approve / allowance / transferFrom (オフチェーンセマンティクス; オンチェーン = transfer)
承認済み支出 (approve / allowance / transferFrom) はZIP-32階層型アカウントに基づいて構築されています。各EOAスペンダーは、独自のスペンディングキーとビューイングキーを持つ専用のサブアカウント (account_S) を受け取ります。これは、所有者のメインアカウントや他のすべてのスペンダーから暗号的に隔離されています。ZIP-32はキー導出を定義し、このERCはERC-20の approve / transferFrom を以下のようにマッピングします:
approve(spender, N)— 所有者は未使用のZIP-32サブアカウントを導出し、transfer([[glossary/PrivacyCall|PrivacyCall]])を介してNを資金提供し、そのサブアカウントのスペンディングキーをEOAスペンダーに (オフチェーンで暗号化して) 配信します。オンチェーン: 1回のtransfer。allowance(owner, spender)— そのサブアカウントの残り残高を、サブアカウントのビューイングキーでスキャンします。オンチェーンマッピングはありません。transferFrom(owner, to, amount)— スペンダーはサブアカウントからtoに支払うために支出します; お釣りはサブアカウントに戻ります。オンチェーン: 1回のtransfer。approve(spender, 0)/ 取り消し — 所有者はtransferを介してサブアカウントを回収します。
アローワンスの上限は、オンチェーンカウンターではなく、サブアカウントの実際のノート残高によって強制されます。ウォレットは、どのビューイングキーがノートを復号したかによって「自身の」資産と「アローワンス」資産を区別します — オンチェーンマーカーはありません。
実行要件
オンチェーン状態マシンはOrchardシールドプール (Zcashプロトコル仕様) に従います。実装はMUST以下を行います:
- ナリファイアセットを維持します; 同じ
nfは2回消費されてはMUST NOTなりません。 - 追記専用のコミットメントツリーを維持します; 履歴ルートは
isValidAnchorを介してクエリ可能です。 - Groth16証明、spend-authおよびbindingSig、およびすべてのpubFieldsバインディングチェック (
pubFields[7]のみではない) を検証します。 - 重複またはゼロのコミットメントを拒否します; 空のアクション配列を拒否します; 呼び出しごとのアクション数を上限設定します (
maxActions、有限で設定可能な正の境界)。 - 値の変更を
mint/burn/transferを介してのみ公開します (公開バンドルエントリポイントはありません)。 - デプロイ時に一度だけPerc20Createdを発行します (ファクトリデプロイメントがRECOMMENDEDですが、必須ではありません)。
コンプライアンス。 cmxFrozenRoot|cmxFrozenRoot] はオフチェーンのブラックリストSMTのルートです; 回路は消費されたノートの非メンバーシップを証明します。setFrozenRootは admin のみです; 初期ルート 0 は空のブラックリストを示します。実装は、更新後短い猶予期間中に直前のルートを受け入れるMAYことができ、処理中の証明が立ち往生しないようにします。
理由 (Rationale)
- Orchard ZK-UTXOモデル。 ノート、ナリファイア、およびコミットメントツリーはZcashプロトコル仕様に従います; このERCはEVM上でのプライベートトークンインターフェースとアセットごとのデプロイメントを定義します。
- プライベートERC-20であり、異なる資産ではない。 同じメソッドインターフェース; プライバシーは公開性 (公開ビュー vs プライベートクエリ vs 区別不能な転送) を変更しますが、ユーザーの意図は変更しません。
- すべての移動に対して1つのオンチェーン操作。
transfer/approve/transferFromを統合することで、ERC-20が不可避的に漏洩する承認メタデータが削除されます。 - ZIP-32サブアカウントを介した承認済み支出。 各EOAスペンダーは、オンチェーンの
allowanceマッピングの代わりに、隔離された階層型アカウントを取得します; メソッドセマンティクスを参照してください。 approve(contract)はなし。 コントラクトはプライベートキーを持っていません; スペンディングキーをオンチェーンに置くと、誰にでも公開されてしまいます。プログラム可能なプライベート支出 (述語承認ノート、MPCカストディ) は将来の作業であり、このERCの一部ではありません。- ウォレットの動作はオンチェーンABIの範囲外。 サブアカウントのレイアウト、暗号化されたキー配信、およびノートスキャンはウォレット/SDKの責任です; 参照実装を参照してください。
後方互換性
pERC20 はERC-20と機能的に完全互換 (capability-complete) ですが、バイト互換性はありません。公開 balanceOf、オンチェーン allowance、approve/transferFrom ABI、Transfer/Approval イベントはありません。既存のERC-20インデクサーや構成可能なコントラクトは、プライバシー対応のウォレット/SDKなしではこれを駆動できません。
公開ERC-20ツインへのオプションの bridgeOut は、出口でプライバシーを終了MAYする可能性がありますが、この提案では必須ではありません。
テストケース
参照実装リポジトリには以下が含まれます:
- Foundryユニットテスト (
test/PERC20Test.t.sol): コンストラクタガード、mint/burn/transfer会計、供給不変条件。 - エンドツーエンドテスト (
test/PERC20E2E.t.sol,e2e/): デプロイされたPERC20に対する実際のGroth16証明。mint、transfer、burn、およびapprove/transferFromフローをカバーします。
参照実装
コード
参照実装: PERC20Labs/pERC20_.
- 規範的なアセットコントラクト:
contracts/ptoken/PERC20.sol(IPERC20)。 - 暗号化およびウォレット形式 (キー導出、
perc1アドレス、ノート暗号化、ナリファイア、approveパッケージング): 同じリポジトリ内の参照ライブラリおよびSDK。
関連する標準およびプロトコル
- EIP-20 (https://eips.ethereum.org/EIPS/eip-20): トークン標準 —
pERC20がマッピングする公開ファンジブルトークンインターフェース。 - Zcashプロトコル仕様: Orchardシールドプール — ここでEVMGroth16検証用に適応されたノートコミットメント、ナリファイア、ノート暗号化、およびアクション構造。
- ZIP-32 (https://zips.z.cash/zip-0032): シールド階層型決定性ウォレット —
approve/transferFromにおけるスペンダーごとのサブアカウントに使用される階層型アカウント導出。
セキュリティに関する考慮事項
- 二重支払い保護: ナリファイアセット + 正しい
nf導出。各pubFields[i]は< FrでMUSTなければなりません (そうでない場合、nf + Frは異なるisSpentキーで証明を再利用します);pubFields[0]、[3]、[6]はそれぞれanchor、nfOld、cmxとMUST等しくなければなりません (そうでない場合、有効な証明が異なるcalldataにバインドされる可能性があります)。 - 供給不変条件: 値の変更は
mint/burn/transferを介してのみ行われます; コア実行パスは公開呼び出し可能でMUST NOTありません。 - 価値保存: 状態変更の前にbindingSigが検証されます。
- リプレイ保護: sighashは
chainId、コントラクトアドレス、およびすべてのnf/cmxをバインドします。 - サブアカウントスペンディングキー:
approveのために配信されるキーは、暗号文としてのみ現れるMUST必要があります; コントラクトストレージには決して現れません。 - コンプライアンス権限: setFrozenRootは高信頼の管理者ロールです; マルチシグ/タイムロックをSHOULD使用します。
プライバシーに関する考慮事項
- 操作はリレイヤーを介して提出され、提出者のEOAを隠すSHOULD必要があります。
mint/burnのamountとtotalSupplyは公開です; 転送金額とapproveの関係はプライベートです。approve/transferFromはtransferとオンチェーンで区別不能です (同じtransfer([[glossary/PrivacyCall|PrivacyCall]])セレクター);mint/burnは公開金額を持つ別々の関数です。- 試行復号 (trial-decryption) は受領の信頼境界です: NoteAddedイベントだけでは支払いを証明しません; 回路は
encCiphertextがcmxと一致することを検証しません。 - setFrozenRootは、管理者がオフチェーンのブラックリストを介して識別されたノートを凍結することを可能にします; これは完全なトラストレス性とのコンプライアンス上の明示的なトレードオフです。
著作権
著作権および関連する権利はCC0により放棄されます。
この文書を引用する際は、Cyimon, “ERC-8287: Private Token Standard,” Ethereum Improvement Proposals, June 2026. としてください。
1投稿 - 1参加者