newsence
來源篩選

Draft ERC: pERC-20 — Privacy-Preserving Native ERC-20 Token Standard

Ethereum Magicians

This ERC introduces pERC-20, a standard for natively private tokens that use Zero-Knowledge proofs and the VOSA model to hide balances and transaction amounts while maintaining an auditable transfer graph.

newsence

草案 ERC:pERC-20 — 具備隱私保護功能的原生 ERC-20 代幣標準

Ethereum Magicians
大約 5 小時前

AI 生成摘要

本 ERC 定義了 pERC-20,這是一種原生私有代幣的標準介面,利用零知識證明與 VOSA 模型來隱藏餘額與交易金額,同時保持轉帳圖譜的可審計性以符合監管需求。

摘要

本 ERC 定義了 pERC-20,這是一種具有內建金額隱私的代幣標準介面。與 (包裝現有的 ERC-20 代幣)不同,pERC-20 用於原生隱私的代幣——直接鑄造到隱藏餘額中,從不暴露在公共帳本上。

該標準利用了 模型——每個隱私餘額都存在於一個標準的 20 位元組 EVM 地址中,且僅使用一次(UTXO 語義),並透過標準 ECDSA 簽名證明所有權(無需自定義錢包)。

隱私模型:pERC-20 提供金額隱私——餘額和轉帳金額使用 Poseidon 哈希承諾(Commitment)進行隱藏,並透過鏈上 Groth16 零知識證明(ZK Proof)進行驗證。VOSA 到 VOSA 的轉帳圖譜保持公開可審計。這種刻意的設計選擇在保護財務隱私的同時,也實現了監管合規性。

核心特性

  • 原生隱私:代幣直接鑄造為 Poseidon 承諾
  • 任意轉帳金額(非固定面額)
  • 鏈上 ZK 證明驗證(完全去中心化)
  • 標準 20 位元組地址 + ECDSA 簽名(EVM 原生,相容 MetaMask)
  • O(1) 已支出追蹤與可配置的週期清理(有界的狀態增長)
  • 子類別友好:核心操作為外部虛擬函數(external virtual),並配有內部的 _execute* 函數以便安全覆寫

動機

ERC-20 的隱私缺口

目前的 ERC-20 代幣會暴露所有財務資訊:

balanceOf(0x1234...) → 1,000,000 USDC // 任何人都能看到
Transfer(from, to, 50000) // 金額是公開的

這產生了隱私問題:

  • 薪資透明化:任何人都能看到薪酬
  • 商業暴露:競爭對手可以看到交易量和國庫規模
  • 個人理財:錢包持倉是公開的

為什麼需要原生標準?

VOSA-20 透過包裝為現有代幣增加隱私。但如果你想發行一個預設隱私新代幣呢?

| 方案 | 用例 | 存款/提款 | 鑄造/銷毀 |
| :--- | :--- | :--- | :--- |
| VOSA-20 (包裝型) | 為現有代幣(USDC, WETH)增加隱私 | 是 | 否 |
| pERC-20 (原生型) | 發行具有內建隱私的新代幣 | 否 | 是 |

特別需要 pERC-20 的用例:

  • RWA 代幣:證券、債券、房地產代幣,其中餘額隱私是監管要求
  • 企業代幣:持倉不應公開的內部治理/效用代幣
  • 穩定幣:由受監管實體發行的隱私保護穩定幣
  • 合規准入代幣:每項操作都需要合規證明的代幣

隱私對比

| 解決方案 | 金額隱私 | VOSA 持有者身份 | 轉帳圖譜 | 合規性 |
| :--- | :---: | :---: | :---: | :--- |
| ERC-20 | ❌ | ❌ | 公開 | 透明 |
| pERC-20 | ✅ | ✅ | ✅ (設計使然) | 可審計 |
| Tornado/Railgun | ✅ | ✅ | ❌ | 困難 |

設計理念

pERC-20 針對的是餘額隱私,而非匿名混幣

  • ✅ 企業財務隱私(向競爭對手隱藏金額)
  • ✅ 個人餘額隱私(隱藏錢包持倉)
  • ✅ 合規的隱私交易(滿足 KYC/AML)
  • ❌ 完全匿名要求(轉帳圖譜是可追蹤的)

規範

本文件中的關鍵詞「MUST」、「MUST NOT」、「REQUIRED」、「SHALL」、「SHALL NOT」、「SHOULD」、「SHOULD NOT」、「RECOMMENDED」、「NOT RECOMMENDED」、「MAY」和「OPTIONAL」應按照 RFC 2119 和 RFC 8174 中的描述進行解釋。

概述

text
┌─────────────────────────────────────────────────────────────────────────────────┐│                           pERC-20 架構                                          │├─────────────────────────────────────────────────────────────────────────────────┤│                                                                                 ││  存儲:                                                                         ││  ┌────────────────────────────────────────────────────────────────────────┐    ││  │  mapping(address => bytes32) balanceCommitment  // 隱藏餘額            │    ││  │  mapping(address => PolicyMeta) policies        // 授權                │    ││  │  mapping(address => PermitMeta) permits         // 一次性授權          │    ││  │  uint256 totalSupply                            // 公開                │    ││  └────────────────────────────────────────────────────────────────────────┘    ││                                                                                 ││  操作:                                                                         ││  ┌────────────────────────────────────────────────────────────────────────┐    ││  │  mint(amount, recipient, commitment, proof)     // 發行代幣            │    ││  │  burn(input, amount, proof, signature)          // 銷毀代幣            │    ││  │  transfer(inputs[], outputs[], proof, sigs)     // 隱私轉帳            │    ││  │  consolidate(inputs[], output, proof, sigs)     // 合併 VOSA           │    ││  │  createPolicy(spender, commitment, proof, sig)  // 循環授權            │    ││  │  createPermit(spender, commitment, proof, sig)  // 一次性授權          │    ││  └────────────────────────────────────────────────────────────────────────┘    ││                                                                                 │└─────────────────────────────────────────────────────────────────────────────────┘

鏈上狀態是一個單一映射:

solidity
mapping(address => bytes32) balanceCommitment;// bytes32(0)                     → 從未使用// Poseidon(amount, blinder, ts)  → 擁有餘額(承諾隱藏了金額)// SPENT_PREFIX | block.number    → 已支出(在可配置的窗口後可刪除)

依賴項

  • EIP-712:類型化結構化數據簽名(所有操作)
  • ERC-5564:隱身地址標準(VOSA 地址推導)

ERC-20 與 pERC-20 對比

| ERC-20 介面 | pERC-20 介面 | 備註 |
| :--- | :--- | :--- |
| name() | name() | 相同 |
| symbol() | symbol() | 相同 |
| decimals() | decimals() | 相同 |
| totalSupply() | totalSupply() | 相同 |
| balanceOf(addr) | hasBalance(addr) + balanceCommitment(addr) | 金額隱藏 |
| transfer(to, amount) | transfer(inputs[], outputs[], proof, ...) | 需要 ZK 證明 |
| approve(spender, amount) | createPolicy(spender, commitment, proof, ...) | 金額隱藏 |
| allowance(owner, spender) | getPolicy(policyAddr) | 金額隱藏 |
| transferFrom(from, to, amount) | 基於策略的 transfer() | 支出者簽名 |
| N/A | mint(amount, recipient, commitment, proof, ...) | 代幣發行 |
| N/A | burn(input, amount, proof, signature, ...) | 代幣銷毀 |
| N/A | consolidate(inputs[], output, proof, sigs, ...) | 合併 VOSA |

常量

solidity
/// @notice 已支出標記前綴(高 128 位元)bytes16 public constant SPENT_PREFIX = 0xDEADDEADDEADDEADDEADDEADDEADDEAD;/// @notice 每筆交易的最大輸入數(防止 DoS)uint256 public constant MAX_INPUTS = 10;/// @notice 每筆交易的最大輸出數uint256 public constant MAX_OUTPUTS = 10;/// @notice 時間戳驗證窗口(±2 小時)uint256 public constant TIMESTAMP_WINDOW = 2 hours;/// @notice 預設清理窗口(以 12 秒/區塊計,約 1 個月)uint256 public constant DEFAULT_CLEANUP_WINDOW = 216_000;

核心介面

數據結構

solidity
struct PolicyMeta {    address owner;      // 授權創建者    address spender;    // 被授權的支出者    uint256 expiry;     // 0 = 永不過期    bool revoked;}struct PermitMeta {    address owner;    address spender;    uint256 expiry;    bool revoked;    bool used;          // 單次使用}struct CreatePolicyParams {    address input;    bytes32 inputCommitment;    address policyAddress;    bytes32 policyCommitment;    uint256 policyTimestamp;    address changeAddress;    bytes32 changeCommitment;    uint256 changeTimestamp;    address spender;    uint256 expiry;    uint256 deadline;}struct CreatePermitParams {    address input;    bytes32 inputCommitment;    address permitAddress;    bytes32 permitCommitment;    uint256 permitTimestamp;    address changeAddress;    bytes32 changeCommitment;    uint256 changeTimestamp;    address spender;    uint256 expiry;    uint256 deadline;}

元數據

solidity
interface IPrivateERC20Metadata {    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 DOMAIN_SEPARATOR() external view returns (bytes32);}

VOSA 狀態管理

solidity
interface IPrivateERC20VOSA {    /// @return bytes32(0) 若未使用, SPENT_MARKER 若已支出, commitment 若活躍    function balanceCommitment(address vosa) external view returns (bytes32);    function hasBalance(address vosa) external view returns (bool);    function isEverUsed(address vosa) external view returns (bool);    function isSpent(address vosa) external view returns (bool);    function getSpentBlock(address vosa) external view returns (uint256);    function batchHasBalance(address[] calldata vosas) external view returns (bool[] memory);    function batchGetCommitment(address[] calldata vosas) external view returns (bytes32[] memory);    /// @return 0=普通, 1=策略(Policy), 2=許可(Permit)    function getAddressType(address vosa) external view returns (uint8);}

核心操作

solidity
interface IPrivateERC20Core {    /// @notice 鑄造代幣 — 金額在發行時公開,此後隱藏在承諾中    function mint(        uint256 amount, address recipient, bytes32 commitment,        uint256 outputTimestamp, bytes calldata ephemeralPubKey,        bytes calldata proof, bytes calldata memo    ) external returns (bool);    /// @notice 銷毀代幣 — 金額變為公開,可選找零輸出    function burn(        address input, bytes32 inputCommitment, uint256 amount,        address changeAddress, bytes32 changeCommitment,        uint256 changeTimestamp, bytes calldata changeEphemeralPubKey,        bytes calldata signature, bytes calldata proof, uint256 deadline    ) external returns (bool);    /// @notice 隱私轉帳 (N 個輸入 → M 個輸出,金額隱藏)    function transfer(        address[] calldata inputs, bytes32[] calldata inputCommitments,        address[] calldata outputs, bytes32[] calldata outputCommitments,        uint256[] calldata outputTimestamps, bytes[] calldata ephemeralPubKeys,        bytes[] calldata signatures, bytes calldata proof,        uint256 deadline, int256[] calldata policyChangeIndices,        bytes calldata memo    ) external returns (bool);    /// @notice 將多個 VOSA 合併為一個 (減少碎片化,需為同一所有者)    function consolidate(        address[] calldata inputs, bytes32[] calldata inputCommitments,        address output, bytes32 outputCommitment,        uint256 outputTimestamp, bytes calldata ephemeralPubKey,        bytes[] calldata signatures, bytes calldata proof, uint256 deadline    ) external returns (bool);}

授權介面

策略 (Policy,循環授權)

類似於 ERC-20 的 approve,但針對一次性地址。所有者授權支出者;當支出者進行轉帳時,策略會自動遷移到找零 VOSA。策略在多次轉帳中持續存在,直到被撤銷或過期。

solidity
interface IPrivateERC20Policy {    function createPolicy(        CreatePolicyParams calldata params, bytes[] calldata ephemeralPubKeys,        bytes calldata ownerSignature, bytes calldata proof, bytes calldata memo    ) external returns (bool);    function revokePolicy(address policyAddress) external;    function getPolicy(address policyAddress) external view returns (        address owner, address spender, uint256 expiry, bool revoked, bool isActive    );}

許可 (Permit,一次性授權)

單次使用的授權。使用後,used = true 且許可被消耗。不進行遷移。

solidity
interface IPrivateERC20Permit {    function createPermit(        CreatePermitParams calldata params, bytes[] calldata ephemeralPubKeys,        bytes calldata ownerSignature, bytes calldata proof, bytes calldata memo    ) external returns (bool);    function revokePermit(address permitAddress) external;    function getPermit(address permitAddress) external view returns (        address owner, address spender, uint256 expiry, bool revoked, bool used, bool isActive    );}

事件

solidity
interface IPrivateERC20Events {    event Mint(address indexed minter, address indexed recipient, uint256 amount,        bytes32 commitment, bytes ephemeralPubKey, bytes memo);    event Burn(address indexed input, uint256 amount, address changeAddress,        bytes32 changeCommitment, bytes changeEphemeralPubKey);    event Transfer(address[] inputs, address[] outputs, bytes32[] outputCommitments,        bytes[] ephemeralPubKeys, bytes memo);    event Consolidate(address[] inputs, address indexed output,        bytes32 outputCommitment, bytes ephemeralPubKey);    event PolicyCreated(address indexed policyAddress, address indexed owner,        address indexed spender, uint256 expiry, bytes32 policyCommitment,        address changeAddress, bytes32 changeCommitment, bytes[] ephemeralPubKeys, bytes memo);    event PolicyMigrated(address indexed oldAddress, address indexed newAddress);    event PolicyRevoked(address indexed policyAddress);    event PermitCreated(address indexed permitAddress, address indexed owner,        address indexed spender, uint256 expiry, bytes32 permitCommitment,        address changeAddress, bytes32 changeCommitment, bytes[] ephemeralPubKeys, bytes memo);    event PermitUsed(address indexed permitAddress);    event PermitRevoked(address indexed permitAddress);    event MinterUpdated(address indexed previousMinter, address indexed newMinter);    event VerifierUpdated(string indexed verifierType, address newVerifier);    event AddressCleaned(address indexed addr, uint256 spentBlock);    event CleanupWindowUpdated(uint256 oldWindow, uint256 newWindow);}

EIP-712 類型定義

所有可變參數都包含在簽名中,以防止搶先交易(frontrunning)替換。

solidity
bytes32 constant TRANSFER_TYPEHASH = keccak256(    "Transfer(bytes32 inputsHash,bytes32 inputCommitmentsHash,bytes32 outputsHash,"    "bytes32 outputCommitmentsHash,bytes32 policyIndicesHash,bytes32 ephemeralKeysHash,uint256 deadline)");bytes32 constant BURN_TYPEHASH = keccak256(    "Burn(address input,bytes32 inputCommitment,uint256 amount,"    "address changeAddress,bytes32 changeCommitment,uint256 deadline)");bytes32 constant CONSOLIDATE_TYPEHASH = keccak256(    "Consolidate(bytes32 inputsHash,bytes32 inputCommitmentsHash,"    "address output,bytes32 outputCommitment,bytes32 ephemeralKeyHash,uint256 deadline)");bytes32 constant CREATE_POLICY_TYPEHASH = keccak256(    "CreatePolicy(address input,bytes32 inputCommitment,address policyAddress,"    "bytes32 policyCommitment,address changeAddress,bytes32 changeCommitment,"    "address spender,uint256 expiry,bytes32 ephemeralKeysHash,uint256 deadline)");bytes32 constant CREATE_PERMIT_TYPEHASH = keccak256(    "CreatePermit(address input,bytes32 inputCommitment,address permitAddress,"    "bytes32 permitCommitment,address changeAddress,bytes32 changeCommitment,"    "address spender,uint256 expiry,bytes32 ephemeralKeysHash,uint256 deadline)");

ZK 電路規範

pERC-20 重用了與 VOSA-20 (erc20-wrapped) 相同的電路。

承諾格式

commitment = Poseidon(amount, blinder, timestamp)

其中:

  • amount: uint256, 必須 < 2^96
  • blinder: 域元素 (Field element), 必須不為 0 (由 CSPRNG 生成)
  • timestamp: uint256, 用於重放保護 (在 ±2 小時窗口內驗證)

金額電路 (AmountCircuit)

用於鑄造和銷毀 —— 驗證承諾與公開金額匹配:

公開輸入:inputCommitments[], outputCommitments[], absAmount, isWithdraw, txHash, outputTimestamps[]
私密輸入:inputAmounts[], inputBlinders[], inputTimestamps[], outputAmounts[], outputBlinders[]

約束條件:

  • 鑄造 (mint):absAmount == outputAmount,輸出承諾正確
  • 銷毀 (burn):inputAmount == absAmount + changeAmount,兩個承諾均正確
  • 範圍 (Range):所有金額在 [0, 2^96) 區間內
  • 盲因子 (Blinder):所有盲因子 ≠ 0

| 變體 | 公開信號 | 約束數量 |
| :--- | :---: | :---: |
| AmountCircuit(0,1) — 鑄造 | 5 | ~782 |
| AmountCircuit(1,0) — 全額銷毀 | 4 | ~782 |
| AmountCircuit(1,1) — 部分銷毀 | 6 | ~1,240 |

轉帳電路 (TransferCircuit)

用於轉帳、合併、創建策略、創建許可 —— 在不洩露金額的情況下驗證餘額守恆:

公開輸入:inputCommitments[], outputCommitments[], txHash, outputTimestamps[]
私密輸入:inputAmounts[], inputBlinders[], inputTimestamps[], outputAmounts[], outputBlinders[]

約束條件:

  1. 每個 inputCommitment == Poseidon(inputAmount, inputBlinder, inputTimestamp)
  2. 每個 outputCommitment == Poseidon(outputAmount, outputBlinder, outputTimestamp)
  3. sum(inputAmounts) == sum(outputAmounts)
  4. 所有金額在 [0, 2^96) 區間內
  5. 所有盲因子 ≠ 0
  6. txHash == Poseidon(inputCommitments || outputCommitments)

| 變體 | 公開信號 | 約束數量 |
| :--- | :---: | :---: |
| TransferCircuit(1,1) — 簡單發送 | 4 | ~700 |
| TransferCircuit(1,2) — 拆分 | 6 | ~1,100 |
| TransferCircuit(2,1) — 合併 (2→1) | 5 | ~1,100 |
| TransferCircuit(2,2) — 標準轉帳 | 7 | ~1,500 |
| TransferCircuit(5,1) — 合併 (5→1) | 8 | ~2,900 |

註:合約允許最多 MAX_INPUTS=10,但目前的電路/驗證器部署支持最多 5 個輸入。TransferCircuit(10,1) 及更大的變體可以從同一模板編譯,並根據需要添加到驗證器包裝器中。

Poseidon 參數

哈希:Poseidon
寬度:承諾使用 t = 3 (2 個輸入 + 1 個容量);txHasht 值視情況而定
輪數:RF = 8 全輪,RP = 57 部分輪
域:BN254 Fr

授權邏輯

在任何時刻,每個 VOSA 都有且僅有一個授權簽名者 —— 零併發衝突:

solidity
function _getAuthorizedSigner(address input) internal view returns (address) {    PolicyMeta memory pMeta = _policyMeta[input];    PermitMeta memory tMeta = _permitMeta[input];    if (pMeta.owner != address(0)) {        bool isActive = !pMeta.revoked && (pMeta.expiry == 0 || block.timestamp < pMeta.expiry);        return isActive ? pMeta.spender : pMeta.owner;    } else if (tMeta.owner != address(0)) {        bool isActive = !tMeta.used && !tMeta.revoked && (tMeta.expiry == 0 || block.timestamp < tMeta.expiry);        return isActive ? tMeta.spender : tMeta.owner;    }    return input; // 普通 VOSA:所有者即地址本身}

對於銷毀操作,只有所有者可以簽名(代理人不行),防止策略/許可支出者贖回資產:

solidity
function _getBurnAuthorizedSigner(address input) internal view returns (address) {    PolicyMeta memory pMeta = _policyMeta[input];    if (pMeta.owner != address(0)) return pMeta.owner;    PermitMeta memory tMeta = _permitMeta[input];    if (tMeta.owner != address(0)) return tMeta.owner;    return input;}

週期清理

SPENT 標記編碼了支出時的區塊號:SPENT_PREFIX | uint128(block.number)。在可配置的窗口(預設約 216,000 個區塊 / 約 1 個月)之後,任何人可以調用 cleanup() 來刪除過期的條目並回收存儲 Gas。

solidity
interface IPrivateERC20Cleanup {    function cleanupWindow() external view returns (uint256);    function canCleanup(address addr) external view returns (bool);    function cleanup(address[] calldata addrs) external returns (uint256 cleaned);    function setCleanupWindow(uint256 window) external; // 僅限所有者}

清理還會移除已支出地址上任何殘留的 _policyMeta_permitMeta

CompatibleERC20 變體 (可選擴展)

為了與現有的 DeFi(CEX、Uniswap 等)向後相容,一個可選CompatibleERC20 擴展了 PrivateERC20,增加了:

  • 標準 ERC-20 公開餘額(balanceOf, publicTransfer, approve, transferFrom)
  • conceal(amount):公開餘額 → 私密 VOSA
  • reveal(vosa, amount, recipient, proof):私密 VOSA → 公開餘額
solidity
interface ICompatibleERC20 is IPrivateERC20 {    // 標準 ERC-20 (註:使用 publicTransfer 而非 transfer 以避免與私密轉帳衝突)    function balanceOf(address account) external view returns (uint256);    function publicTransfer(address to, uint256 amount) external returns (bool);    function approve(address spender, uint256 amount) external returns (bool);    function allowance(address owner, address spender) external view returns (uint256);    function transferFrom(address from, address to, uint256 amount) external returns (bool);    // 模式切換    function conceal(uint256 amount, address newVosa, bytes32 newCommitment,        uint256 timestamp, bytes calldata ephemeralPubKey,        bytes calldata proof) external returns (bool);    function reveal(address vosa, bytes32 inputCommitment, uint256 amount,        address recipient, address changeAddress, bytes32 changeCommitment,        uint256 changeTimestamp, address changeVosa,        bytes calldata changeEphemeralPubKey, bytes calldata signature,        bytes calldata proof, uint256 deadline) external returns (bool);    function publicSupply() external view returns (uint256);    function privateSupply() external view returns (uint256);}

CompatibleERC20 增加了其專屬的 EIP-712 類型和事件:

solidity
bytes32 constant REVEAL_TYPEHASH = keccak256(    "Reveal(address vosa,bytes32 inputCommitment,uint256 amount,address recipient,"    "address changeAddress,bytes32 changeCommitment,uint256 deadline)");event PublicTransfer(address indexed from, address indexed to, uint256 value);event Approval(address indexed owner, address indexed spender, uint256 value);event Revealed(address indexed vosa, address indexed recipient, uint256 amount);event Concealed(address indexed from, address indexed vosa, uint256 amount);

原理

為什麼選擇 Poseidon 哈希?

| 哈希算法 | R1CS 約束 (每個承諾) | 證明時間 (存款) |
| :--- | :---: | :---: |
| SHA256 | ~30,000 | ~3s |
| Pedersen | ~3,400 | ~500ms |
| Poseidon | ~350 | ~92ms |

Poseidon 對 ZK 極其友好(無需曲線操作),從而產生顯著更小的電路和更快的證明速度。

為什麼需要兩個驗證器?

將電路分開可以針對每個用例進行優化:

  • AmountVerifier:簡單的公開金額鑄造/銷毀(~782–1,240 個約束)
  • TransferVerifier:複雜的隱藏金額多輸入/輸出(~700–5,500 個約束)

兩者都使用適配器模式(IGroth16Verifier 配合動態 uint256[] publicInputs),根據輸入數量路由到相應的電路變體。

為什麼使用 SPENT_MARKER 而非 Nullifiers?

| 維度 | Nullifier (Tornado/Railgun) | SPENT_MARKER (VOSA) |
| :--- | :--- | :--- |
| 轉帳圖譜 | 隱藏 | 公開 |
| 電路複雜度 | 較高 (默克爾成員證明) | 較低 |
| 狀態查找 | O(log n) | O(1) |
| 狀態增長 | 無界 (Nullifier 永久存在) | 有界 (週期清理) |
| 合規性 | 困難 | 容易 |
| 10 年存儲 (100 億筆交易) | ~320 GB | ~2.7 GB |

pERC-20 的設計選擇:合規性 + 簡單性 > 完全匿名性

為什麼分開鑄造/銷毀(而非存款/提款)?

pERC-20 用於原生代幣 —— 沒有底層的 ERC-20 需要包裝/拆解。供應量管理使用 mint(授權鑄造者創建新代幣)和 burn(持有者銷毀代幣)。這類似於 OpenZeppelin 中的 ERC20._mintERC20._burn,但具備隱私性。

為什麼為子類別化使用虛擬函數?

所有六個核心操作(mint, burn, transfer, consolidate, createPolicy, createPermit)都是外部虛擬的,允許子類別覆寫。mintburntransfer 另外暴露了內部的 _executeMint/_executeBurn/_executeTransfer,其中包含核心邏輯,使子類別能直接調用它們而無需進行外部自我調用(這會破壞 nonReentrant 並浪費 Gas)。如果需要,同樣的 _execute* 模式也可以應用於合併、創建策略和創建許可。

性能

在 Apple M2 上使用 snarkjs WASM 證明器測量。Gas 在 XLayer 測試網上使用模擬驗證器測量(在 L2 上進行真實 Groth16 驗證需增加約 20 萬 Gas)。

Gas (PrivateERC20, 模擬驗證器)

| 操作 | Gas | 備註 |
| :--- | :--- | :--- |
| mint | 78,169 | 鑄造者發行私密代幣 |
| burn (帶找零) | 105,316 | 所有者銷毀,餘額進入找零 VOSA |
| transfer (1→2) | 145,237 | 1 個輸入,2 個輸出 |
| consolidate (2→1) | 137,673 | 合併 2 個 VOSA |

Gas (CompatibleERC20, 公開模式)

| 操作 | Gas | 對比標準 ERC-20 |
| :--- | :--- | :--- |
| mintPublic | 39,709 | -20% |
| publicTransfer | 56,385 | +8% |
| approve | 46,046 | ~0% |
| conceal (公開→私密) | 78,962 | N/A |
| reveal (私密→公開) | 86,225 | N/A |

證明生成時間

| 操作 | snarkjs WASM | rapidsnark (C++) |
| :--- | :--- | :--- |
| mint (amount_0_1) | ~90 ms | ~65 ms |
| burn with change (amount_1_1) | ~130 ms | ~85 ms |
| transfer 1→2 (transfer_1_2) | ~125 ms | ~70 ms |
| transfer 2→2 (transfer_2_2) | ~150 ms | ~90 ms |
| consolidate 5→1 (transfer_5_1) | ~210 ms | ~105 ms |

向後相容性

  • pERC-20 實現 ERC-20 介面(balanceOf, transfer(address,uint256) 等)。它是原生隱私代幣的獨立標準。
  • 對於需要同時具備公開和私密模式的代幣,請使用 CompatibleERC20,它在實現私密操作的同時也實現了完整的 ERC-20 介面。
  • EIP-712:所有簽名均使用類型化結構化數據。支持 EIP-712 的錢包(MetaMask 等)可直接使用。
  • ERC-5564:VOSA 地址遵循隱身地址推導模式。

安全考量

可信設置 (Trusted Setup)

Groth16 需要可信設置儀式。推薦:使用有 100+ 參與者並發佈成績單的 Powers of Tau。未來的實現可以考慮 PLONK 以實現通用設置。

搶先交易保護

  • EIP-712 簽名綁定到所有可變參數,包括 policyChangeIndicesephemeralPubKeys
  • ephemeralKeysHash 對每個密鑰使用 keccak256,然後打包為 bytes32[],以避免在變長 bytes[] 上發生 abi.encodePacked 衝突。
  • 截止日期 (Deadline) 防止過期交易。
  • ZK 證明需要私密輸入(盲因子)—— 無法從公開數據偽造。

雙重支出 (Double-Spending)

每個 VOSA 僅能支出一次。SPENT_MARKERReentrancyGuard 原子化設置。承諾檢查 balanceCommitment[vosa] == inputCommitment 會隱式拒絕已支出地址(前綴與任何有效的 Poseidon 哈希不匹配)和未使用地址(bytes32(0) 不匹配)。

金額安全

  • 範圍證明強制執行 0 ≤ amount < 2^96(防止負數金額/溢出)。
  • ZK 電路中強制執行餘額守恆:sum(inputs) == sum(outputs)
  • 電路中強制執行 Blinder ≠ 0(防止承諾變成金額的簡單哈希)。

銷毀授權

對於策略/許可授權的 VOSA,銷毀始終需要所有者的簽名(而非代理人)。這防止了代理人贖回/銷毀他們僅被授權轉帳的資產。

元數據清理

當 VOSA 被支出時,策略和許可元數據會被清理:

  • burn:立即刪除輸入地址上的 _policyMeta_permitMeta
  • createPolicy / createPermit:立即刪除被消耗的輸入地址上的兩者。
  • transfer_policyMeta 透過 _handlePolicyMigration 刪除(遷移到找零輸出或直接刪除);_permitMeta 被標記為 used = true(實際刪除發生在週期清理期間)。
  • 週期清理:刪除超過清理窗口的已支出地址上所有殘留的 _policyMeta_permitMeta

這防止了陳舊的授權數據持續存在,並避免地址在清理後被重複使用時可能繼承這些數據。

地址安全

  • 地址碰撞概率:~2^-80(對於 20 位元組地址可忽略不計)。
  • 重放保護:DOMAIN_SEPARATOR 包含 chainId + 合約地址;每個 VOSA 僅支出一次。

時間戳驗證

輸出時間戳必須在 [block.timestamp - TIMESTAMP_WINDOW, block.timestamp] 範圍內。這防止了舊交易的重放,同時允許合理的時鐘偏差。

合規性 (子類別)

需要合規准入的子類別可以覆寫外部入口點,並在調用內部 _execute* 函數之前執行額外的檢查(例如 ZK 合規證明)。

參考實現

代碼庫:[GitHub 連結]

text
erc20-native/├── contracts/│   ├── src/│   │   ├── PrivateERC20.sol          # 核心 pERC-20 (EIP-712, Pausable, ReentrancyGuard, Ownable)│   │   ├── CompatibleERC20.sol       # 雙模式:ERC-20 + pERC-20│   │   ├── interfaces/│   │   │   ├── IPrivateERC20.sol     # 完整介面 (364 行)│   │   │   ├── ICompatibleERC20.sol│   │   │   ├── IGroth16Verifier.sol│   │   │   └── IPoseidon.sol│   │   └── mocks/│   │       ├── MockGroth16Verifier.sol│   │       └── MockPoseidon.sol│   └── test/│       ├── PrivateERC20.test.ts      # 33 個測試│       └── CompatibleERC20.test.ts   # 30 個測試 (共 63 個)├── circuits/                          # 重用 erc20-wrapped 電路│   ├── configs/│   │   ├── amount_0_1.circom         # 鑄造 (0→1)│   │   ├── amount_1_0.circom         # 全額銷毀 (1→0)│   │   ├── amount_1_1.circom         # 部分銷毀 (1→1)│   │   ├── transfer_1_1.circom       # 簡單發送 (1→1)│   │   ├── transfer_1_2.circom       # 拆分 (1→2)│   │   ├── transfer_2_1.circom       # 合併 (2→1)│   │   ├── transfer_2_2.circom       # 標準轉帳 (2→2)│   │   └── transfer_5_1.circom       # 合併 (5→1)│   └── lib/│       ├── hash_commitment.circom     # 承諾 + 範圍證明│       ├── chunked_poseidon.circom    # 變量輸入 Poseidon│       └── sum.circom                 # 餘額守恆└── sdk/                               # 用於證明生成的 TypeScript SDK

測試用例

要求的覆蓋範圍:

  • 鑄造 (Mint):正確的承諾、ZK 驗證、鑄造者授權、拒絕零金額。
  • 銷毀 (Burn):帶找零的全額和部分銷毀、授權 VOSA 的僅限所有者簽名、元數據清理。
  • 轉帳 (Transfer):多輸入/輸出 (1→1, 1→2, 2→2, 5→1)、餘額守恆、重複/重疊檢查、簽名中的 policyChangeIndices
  • 合併 (Consolidate):多個 VOSA 合併為一、強制同一所有者、禁止策略/許可地址。
  • 策略 (Policy):創建、使用(支出者)、撤銷(所有者)、過期、自動遷移至找零、支出時元數據清理。
  • 許可 (Permit):創建、使用(單次)、撤銷、過期、支出時元數據清理。
  • 清理 (Cleanup):週期窗口、批量清理、元數據清理、無法清理活躍 VOSA。
  • 安全 (Security):重放防止、雙重支出、搶先交易(ephemeralPubKeys 替換)、溢出、重入。
  • CompatibleERC20:公開轉帳、隱藏、顯現、供應量追蹤一致性。
  • 子類別化 (Subclassing):覆寫外部入口點、內部調用 _execute*、驗證合規鉤子(hooks)正常工作。

版權

透過 放棄版權及相關權利。