原文

pERC20: Private Token Standard — Cyimon (2026-06-15)

著者: Cyimon (@Cyimon) · ステータス: ドラフト · タイプ: 標準トラック · カテゴリ: ERC · EIP: 8287 · 作成日: 2026-06-09

概要: EVM向けの、デフォルトでプライベートなファンジブルトークン標準

関連議論: EIP-8287-Draft · Ethereum Magicians #28702 · ethresear.ch #25089 · 実装: PERC20Labs/pERC20_


以前公開されたpERC20プロトコル設計 (Ethereum Magicians #28702) を拡張します。この改訂版の主な追加点は、ZIP-32サブアカウントを介した**ERC-20承認済み支出** — approveallowancetransferFrom — です。更新された標準はERC-20機能的に完全 (capability-complete) ですが、バイト互換性はありません(異なるABI、公開残高なし)。

ERC-20pERC20レイヤー
name / symbol / decimals / totalSupplyはい — 同じ公開ビューオンチェーン
balanceOfはい — ホルダーのみのスキャンオフチェーン
transferはい — プライベートな当事者と金額オンチェーン
approve / allowance / transferFrom新規 — ZIP-32サブアカウントを介したEOA支出者向けの承認済み支出; オンチェーン = 転送(コントラクト支出者は非対応)オフチェーン
mint / burnはい — 一般的な拡張オンチェーン
Transfer / Approval events省略(プライバシーのため)

以下は最新の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はプライベートアセットの発行を定義します。

仕様

キーワードMUSTMUST NOTSHOULDMAYはRFC 2119に従って解釈されます。Solidity構文は0.8.20以上です。

基盤となるプロトコル

価値はアカウント残高ではなく、シールドノートに保持されます。ノート形式、ナリファイアコミットメントツリー、ノート暗号化、およびアクション/バンドル構造は、Zcashプロトコル仕様の**Orchardシールドプール**に従い、ここではGroth16で検証されるアセットごとのEVMコントラクトとして採用されています。以下に繰り返されないフィールドレベルの形式は、参照実装で規範的です。

pERC20インターフェース

このセクションでは、すべてのpERC20インターフェースを一箇所にまとめ、それぞれがERC-20標準インターフェースに対応するかどうかを示します。ERC-20: yes = ERC-20標準; extension = 一般的な拡張(発行/焼却); no = pERC20固有。レイヤー: on-chain = コントラクトABI; off-chain = ウォレット/SDK(コントラクトメソッドなし)。

pERC20インターフェースERC-20レイヤー公開度説明
name() / symbol() / decimals()yesオンチェーンpublicERC-20と同一
totalSupply()yesオンチェーンpublic公開カウンター(発行 − 焼却)
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 (関係は隠蔽)支出者がサブアカウントから支払い; オンチェーンではtransfer(PrivacyCall)として提出
mint(amount, PrivacyCall)extensionオンチェーン金額はpublic; 受取人はprivate発行者のみ; Orchardアクション + 総供給量増加
burn(amount, PrivacyCall)extensionオンチェーン金額はpublic; 焼却者はprivateホルダーが自身のノートを焼却; Orchardアクション + 総供給量減少
issuer()noオンチェーンpublicトークン発行者アドレス
cmxFrozenRoot() / setFrozenRoot()noオンチェーンpublic root; 管理者書き込みコンプライアンス凍結ノートルート
cmxRoot() / isValidAnchor() / isSpent() / treeSize()noオンチェーンpublicOrchardコミットメントツリーの状態

イベント

pERC20イベントERC-20レイヤー説明
Transfer(from, to, value)yesオフチェーン(省略)発行されない; 当事者と金額はプライベート
Approval(owner, spender, value)yesオフチェーン(省略)発行されない; 所有者 ↔ 支出者をリンクしてしまうため
NoteAdded / NoteConfirmed転送を置き換えるオンチェーンノートごとの可視性
Mint / Burnextensionオンチェーン公開金額のみ
Perc20Created / FrozenRootUpdated / BundleExecutednoオンチェーンデプロイ、コンプライアンス、バンドルメタデータ

オンチェーンでの区別不能性。 transferapproveの資金提供ステップ、transferFrom、および取り消し・回収はすべて同じオンチェーン呼び出しです: transfer(PrivacyCall)。オブザーバーはどのERC-20操作が実行されているかを判別できません。

ネイティブにはサポートされない。 ERC-20approve(contractAddress, amount) — コントラクトが自律的にtransferFromを呼び出す — にはネイティブな同等物がありません: 支出には秘密鍵が必要ですが、コントラクトはそれを保持できません。理由を参照してください。

コントラクトインターフェース

pERC20は1つのオンチェーンABIIPERC20; 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);
}

適合性:

  • 成功したtransfertrueMUST返します。
  • コアバンドル実行パスは公開呼び出し可能でMUST NOTありません(供給不変条件; 後述)。
  • 実装はSolidityを複数のコントラクトに分割してもMAY構いません(例: IPERC20 + 検証者ベース)が、観測可能なABIとイベントは上記の統一インターフェースと一致してMUSTください。
  • cmxRoot()は最新のコミットメントツリールートです; isValidAnchor(root)rootがこれまでアクティブであった場合にのみtrueを返します; isSpent(nf)ナリファイアセットを公開します; treeSize()は挿入されたコミットメントの数です。

呼び出し形式

すべての値変更操作は、1つ以上の**Orchardアクション** (Zcashプロトコル仕様) をエンコードするPrivacyCallを提出します:

  • actions = abi.encode(BundleAction[])
  • bindingSig = 値の保存を証明するSchnorrバインディング署名[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ランダム化された支出認証キーX
[5]rk_yランダム化された支出認証キーY
[6]cmx出力ノートコミットメント
[7]rt_frozenコンプライアンス凍結ルートバインディング

pubFields[i]< FrMUSTあり、Frは検証者が使用するSNARK曲線のスカラーフィールドモジュラス(参照実装ではBN254)です; そうでない場合はリバートします(PubFieldOutOfRange)。実装は、ActionPubHash(Poseidonスポンジ)を介して8つのフィールドを1つのGroth16公開シグナルにハッシュ化し、回路のPubHashAction()と一致させてMUSTください。

calldataフィールドへのバインディング。 証明の公開入力はアクションのトップレベルフィールドと一致してMUSTください; 以下のいずれかが失敗した場合、実装はリバートしてMUSTください:

チェックリバート
pubFields[0] == anchor and isValidAnchor(anchor)BadAnchor
pubFields[3] == nfOldMUSTリバート(参照実装: NullifierSpent)
pubFields[6] == cmx and cmx != 0InvalidProof / 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バイト(受信者キー下のOrchardノートプレーンテキスト + AEADタグ)でMUSTください。outCiphertextは80バイト(OVK下の送信者自己回復)でSHOULDください。ノート暗号化レイアウトを変更する実装は、このERCの別のバリアントを公開してMUSTください。

バンドルレベルのbindingSigは、状態変異の前に、すべてのアクションナリファイア、コミットメント、および操作のvalueBalanceに対して検証されてMUSTください(エンコーディングについてはメソッドセマンティクスを参照)。

ノート暗号化とキー導出は、Zcashプロトコル仕様Orchardノート形式に従います; 正確なエンコーディングは参照実装にあります。

メソッドセマンティクス

name / symbol / decimals / totalSupply

ERC-20と同一: 公開オンチェーンビュー。

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 += amountonlyIssuer); amountは公開、受信者はプライベート。発行はtransferと同じアンカー / ナリファイア / 支出認証パスを使用MUSTください(出力のみのブランチなし)。回路は消費された入力をv = 0に制約し、アクションが純流入を表すようにMUSTください; コントラクトはノート値を直接読み取りません。
  • burn: totalSupply -= amount; どのホルダーも自身のノートを焼却MAYできます; amountは公開、焼却者はプライベート。
操作valueBalanceエンコーディング
transfer0
burnbit255 = 0, low bits = amount
mintbit255 = 1, low bits = amount

balanceOf (オフチェーン、プライベート)

ホルダーのみが、NoteAddedイベントをスキャンし、ビューイングキーでOrchardノートを試行復号し、消費されたナリファイアを除外することで、残高を計算できます。オンチェーンのbalanceOfはなく、第三者の残高を照会する方法もありません。

approve / allowance / transferFrom (オフチェーンセマンティクス; オンチェーン = transfer)

承認済み支出(approve / allowance / transferFrom)はZIP-32階層型アカウントに基づいて構築されています: 各支出者は、所有者のメインアカウントや他のすべての支出者から暗号的に隔離された、独自の支出キーとビューイングキーを持つ専用のサブアカウント(account_S)を受け取ります。ZIP-32はキー導出を定義します; このERCERC-20approve / transferFromを以下のようにマッピングします:

  1. approve(spender, N) — 所有者は未使用のZIP-32サブアカウントを導出し、transfer(PrivacyCall)を介してNを資金提供し、そのサブアカウントの支出キーを支出者に(オフチェーンで暗号化して)配布します。オンチェーン: 1つのtransfer
  2. allowance(owner, spender) — そのサブアカウントの残高を、サブアカウントのビューイングキーでスキャンします。オンチェーンマッピングはありません。
  3. transferFrom(owner, to, amount) — 支出者がサブアカウントからtoに支払うために支出します; お釣りはサブアカウントに戻ります。オンチェーン: 1つのtransfer
  4. approve(spender, 0) / 取り消し — 所有者はtransferを介してサブアカウントを回収します。

許容量の上限は、オンチェーンカウンターではなく、サブアカウントの実際のノート残高によって強制されます。ウォレットは、どのビューイングキーがノートを復号したかによって「自身の」アセットと「許容量」アセットを区別します — オンチェーンマーカーはありません。

実行要件

オンチェーン状態マシンはOrchardシールドプール (Zcashプロトコル仕様) に従います。実装はMUST以下を実行します:

  • ナリファイアセットを維持します; 同じnfが二度消費されてMUST NOTください。
  • 追加専用のコミットメントツリーを維持します; 履歴ルートはisValidAnchorを介して照会可能です。
  • Groth16証明、支出認証およびバインディング署名、およびすべてのpubFieldsバインディングチェック(pubFields[7]だけでなく)を検証します。
  • 重複またはゼロのコミットメントを拒否します; 空のアクション配列を拒否します; 呼び出しごとのアクション数を制限します(maxActions、有限で設定可能な正の境界)。
  • 値の変更はmint / burn / transferを介してのみ公開します(公開バンドルエントリポイントなし)。
  • デプロイ時に一度Perc20Createdを発行します(ファクトリデプロイがRECOMMENDEDですが必須ではありません)。

コンプライアンス。 cmxFrozenRoot()はオフチェーンブラックリストSMTのルートです; 回路は消費されたノートがメンバーではないことを証明します。setFrozenRootadminのみです; 初期ルート0は空のブラックリストを示します。実装は、更新後の一時的な猶予期間中に直前のルートを受け入れてもMAY構いません。これにより、処理中の証明が立ち往生するのを防ぎます。

理由

  • Orchard ZK-UTXOモデル ノート、ナリファイア、およびコミットメントツリーZcashプロトコル仕様に従います; このERCは、EVM上でのプライベートトークンインターフェースとアセットごとのデプロイを定義します。
  • プライベートERC-20であり、異なるアセットではない。 同じメソッドサーフェス; プライバシーは公開度(公開ビュー vs プライベートクエリ vs 区別不能な転送)を変更しますが、ユーザーの意図は変更しません。
  • すべての移動に対して1つのオンチェーン操作。 transfer / approve / transferFromを統合することで、ERC-20が不可避的に漏洩する承認メタデータが削除されます。
  • ZIP-32サブアカウントを介した承認済み支出。 各支出者は、オンチェーンのallowanceマッピングの代わりに、隔離された階層型アカウントを取得します; メソッドセマンティクスを参照してください。
  • approve(contract)なし。 コントラクトは秘密鍵を持っていません; 支出キーをオンチェーンに置くと、誰にでも公開されてしまいます。プログラム可能なプライベート支出(述語承認ノート、MPCカストディ)は将来の作業であり、このERCの一部ではありません。
  • ウォレットの動作はオンチェーンABIの範囲外。 サブアカウントのレイアウト、暗号化されたキーの配布、およびノートのスキャンはウォレット/SDKの責任です; 準拠した参照は参照実装で提供されています。

後方互換性

pERC20ERC-20機能的に完全 (capability-complete) ですが、バイト互換性はありません: 公開balanceOfなし、オンチェーンallowanceなし、approve/transferFromABIなし、Transfer/Approvalイベントなし。既存のERC-20インデクサーや構成可能なコントラクトは、プライバシー対応のウォレット/SDKなしではこれを駆動できません。

公開ERC-20ツインへのオプションのbridgeOutは、出口でプライバシーを終了させるMAYことができます; この提案では必須ではありません。

テストケース

参照実装リポジトリには以下が含まれます:

  • Foundryユニットテスト(test/PERC20Test.t.sol): コンストラクタガード、発行/焼却/転送の会計処理、供給不変条件。
  • エンドツーエンドテスト(test/PERC20E2E.t.sole2e/): デプロイされたPERC20に対する実際のGroth16証明。発行、転送、焼却、およびapprove / transferFromフローをカバーしています。

参照実装

コード

参照実装: PERC20Labs/pERC20_

  • 規範的なアセットコントラクト: contracts/ptoken/PERC20.sol (IPERC20)。
  • 暗号およびウォレット形式(キー導出、perc1アドレス、ノート暗号化、ナリファイアapproveパッケージング): 同じリポジトリ内の参照ライブラリおよびSDK。

関連標準およびプロトコル

セキュリティに関する考慮事項

  • 二重支払い保護: ナリファイアセット + 正しいnf導出。各pubFields[i]< FrMUSTください(そうでない場合、nf + Frは異なるisSpentキーで証明を再利用します); pubFields[0][3][6]はそれぞれanchornfOldcmxと等しくMUSTください(そうでない場合、有効な証明が異なるcalldataにバインドされる可能性があります)。
  • 供給不変条件: 値の変更はmint/burn/transferを介してのみ行われます; コア実行パスは公開呼び出し可能でMUST NOTください。
  • 価値保存: 状態変異の前にバインディング署名が検証されます。
  • リプレイ保護: sighashはchainId、コントラクトアドレス、およびすべてのnf/cmxをバインドします。
  • サブアカウント支出キー: approveのために配布されるキーは、暗号文としてのみ現れてMUSTください; コントラクトストレージには決して現れてMUST NOTください。
  • コンプライアンス権限: setFrozenRootは高信頼の管理者ロールです; マルチシグ/タイムロックを使用SHOULDください。

プライバシーに関する考慮事項

  • 操作はリレイヤーを介して提出され、提出者EOAを隠すSHOULDください。
  • mint/burnamounttotalSupplyは公開です; 転送金額とapproveの関係はプライベートです。
  • approve/transferFromtransferとオンチェーンで区別不能です(同じtransfer(PrivacyCall)セレクター); mint/burnは公開金額を持つ別々の関数です。
  • 試行復号は受信の信頼境界です: NoteAddedイベントだけでは支払いを証明しません; 回路はencCiphertextcmxと一致することを検証しません。
  • setFrozenRootは、管理者がオフチェーンブラックリストを介して識別されたノートを凍結することを可能にします; これは完全なトラストレス性に対する明示的なコンプライアンス上のトレードオフです。

著作権

著作権および関連する権利はCC0により放棄されます。

この文書を引用する際は、Cyimon、「ERC-7605: プライベートトークン標準」、イーサリアム改善提案、2026年6月、と記述してください。

1投稿 - 1参加者

トピック全文を読む