newsence

草案 ERC:VOSA-20 — 具備隱私保護功能的包裝 ERC-20 代幣標準

Ethereum Magicians·大約 1 個月前

本 ERC 定義了 VOSA-20,這是一個隱私保護包裝 ERC-20 代幣的標準介面,透過 Poseidon 哈希承諾隱藏交易金額與餘額,並利用鏈上零知識證明驗證合法性,同時保持交易圖譜可審計以符合監管需求。

欄位

標題
VOSA-20 - 隱私保護型包裝 ERC-20 代幣標準

狀態
草案 (Draft)

類型
標準軌道 (Standards Track)

類別
ERC

依賴需求
ERC-20, EIP-712, ERC-5564

摘要 (Abstract)

本 ERC 定義了 VOSA-20,這是一個用於隱私保護型包裝 ERC-20 代幣的標準介面。VOSA-20 實現了機密轉帳,其中交易金額和餘額使用 Poseidon 哈希承諾(Commitment)進行隱藏,而有效性則透過鏈上零知識證明(Groth16)進行驗證。

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

隱私模型:VOSA-20 提供 選擇性隱私 —— 金額、餘額和 VOSA 持有者的真實身份(透過隱身地址)是被隱藏的,但 VOSA 到 VOSA 的轉帳圖譜保持公開可審計。這種刻意的設計選擇在保護財務隱私的同時,也支持監管合規。

核心特性

  • 任意轉帳金額(非固定面額)

  • 鏈上 ZK 證明驗證(完全去中心化)

  • 標準 20 位元組地址 + ECDSA 簽名(EVM 原生,相容 MetaMask)

  • 具備可配置週期清理功能的 O(1) 支出追蹤(限制狀態增長)

動機 (Motivation)

私有代幣轉帳的需求

目前的 ERC-20 代幣會公開所有交易細節 —— 發送者/接收者地址、轉帳金額以及完整的交易歷史。這對薪資發放、商業交易和個人財務產生了隱私疑慮。

隱私對比

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

設計理念

VOSA-20 專注於 餘額隱私,而非 匿名混幣

  • 企業財務隱私(對競爭對手隱藏金額)

  • 個人餘額隱私(隱藏錢包資產持有量)

  • 合規的私密交易(滿足 KYC/AML)

  • 完全匿名需求(轉帳圖譜是可追蹤的)

規範 (Specification)

本文件中的關鍵詞「必須」(MUST)、「不得」(MUST NOT)、「要求」(REQUIRED)、「應當」(SHALL)、「不應當」(SHALL NOT)、「應該」(SHOULD)、「不應該」(SHOULD NOT)、「推薦」(RECOMMENDED)、「不推薦」(NOT RECOMMENDED)、「可以」(MAY) 和「可選」(OPTIONAL) 應按照 RFC 2119 和 RFC 8174 中的描述進行解釋。

概述

ERC-20 (公開) ──deposit()──▶ VOSA-20 (私有) ──withdraw()──▶ ERC-20 (公開)

├── transfer() (私有 → 私有,金額隱藏)
├── consolidate() (合併 N 個 VOSA → 1 個)
├── createPolicy() (循環授權)
└── createPermit() (一次性授權)

鏈上狀態為單一映射:

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

依賴項

  • ERC-20: 底層代幣標準

  • EIP-712: 類型化結構數據簽名

  • ERC-5564: 隱身地址標準

常量

/// @notice 已支出標記前綴 (高 128 位元)
bytes16 public constant SPENT_PREFIX = 0xDEADDEADDEADDEADDEADDEADDEADDEAD;

/// @notice 每筆交易的最大輸入數量 (DoS 保護)
uint256 public constant MAX_INPUTS = 10;

/// @notice 每筆交易的最大輸出數量
uint256 public constant MAX_OUTPUTS = 10;

核心介面

元數據 (Metadata)

interface IVOSA20Metadata {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function underlying() external view returns (IERC20);
function totalSupply() external view returns (uint256);
function DOMAIN_SEPARATOR() external view returns (bytes32);
}

VOSA 狀態管理

interface IVOSA20VOSA {
/// @return 如果未使用則返回 bytes32(0),如果已支出則返回 SPENT_MARKER,如果活躍則返回承諾值
function balanceCommitmentHash(address vosa) external view returns (bytes32);
function hasBalance(address vosa) external view returns (bool);
function isEverUsed(address vosa) external view returns (bool);
function batchGetCommitmentHash(address[] calldata vosas) external view returns (bytes32[] memory);
/// @return 0=普通, 1=Policy, 2=Permit
function getAddressType(address vosa) external view returns (uint8);
}

核心操作

interface IVOSA20Core {
/// @notice 將 ERC-20 包裝進私有的 VOSA 餘額
function deposit(
uint256 amount, address recipient, bytes32 commitment,
uint256 outputTimestamp, bytes calldata ephemeralPubKey,
bytes calldata proof, bytes calldata memo
) 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 解除包裝回 ERC-20 (可選擇將找零轉至新的 VOSA)
function withdraw(
    address input, bytes32 inputCommitment, uint256 amount,
    address recipient, address changeAddress, bytes32 changeCommitment,
    uint256 changeTimestamp, bytes calldata changeEphemeralPubKey,
    bytes calldata signature, bytes calldata proof,
    uint256 deadline, bool policyChangeToChange
) 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,但針對一次性地址。所有者授權一個支出者;當支出者進行轉帳時,Policy 會自動遷移到找零的 VOSA。

interface IVOSA20Policy {
struct PolicyMeta {
address owner;
address spender;
uint256 expiry; // 0 = 永不過期
bool revoked;
}

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 且 Permit 被消耗。不進行遷移。

interface IVOSA20Permit {
struct PermitMeta {
address owner;
address spender;
uint256 expiry;
bool revoked;
bool used;
}

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
);

}

事件 (Events)

interface IVOSA20Events {
event Deposit(address indexed depositor, address indexed recipient, uint256 amount,
bytes32 commitment, bytes ephemeralPubKey, bytes memo);
event Transfer(address[] inputs, address[] outputs, bytes32[] outputCommitments,
bytes[] ephemeralPubKeys, bytes memo);
event Withdraw(address indexed input, address indexed recipient, uint256 amount,
address changeAddress, bytes32 changeCommitment, bytes changeEphemeralPubKey);
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 PolicyRevoked(address indexed policyAddress);
event PolicyMigrated(address indexed oldPolicy, address indexed newPolicy);
event PermitCreated(address indexed permitAddress, address indexed owner,
address indexed spender, uint256 expiry, bytes32 permitCommitment,
address changeAddress, bytes32 changeCommitment, bytes[] ephemeralPubKeys, bytes memo);
event PermitRevoked(address indexed permitAddress);
event PermitUsed(address indexed permitAddress);
}

EIP-712 類型定義

bytes32 constant TRANSFER_TYPEHASH = keccak256(
"Transfer(bytes32 inputsHash,bytes32 inputCommitmentsHash,bytes32 outputsHash,bytes32 outputCommitmentsHash,uint256 deadline)"
);

bytes32 constant WITHDRAW_TYPEHASH = keccak256(
"Withdraw(address input,bytes32 inputCommitment,uint256 amount,address recipient,address changeAddress,bytes32 changeCommitment,uint256 deadline)"
);

bytes32 constant CONSOLIDATE_TYPEHASH = keccak256(
"Consolidate(bytes32 inputsHash,bytes32 inputCommitmentsHash,address output,bytes32 outputCommitment,uint256 deadline)"
);

ZK 電路規範

承諾格式

commitment = Poseidon(amount, blinder, timestamp)

其中:

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

轉帳電路 (TransferCircuit)

用於轉帳和合併 —— 在不洩露金額的情況下驗證餘額守恆:

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

約束條件:

  1. 每個 inputCommitment == Poseidon(inputAmount, inputBlinder, ...)
  2. 每個 outputCommitment == Poseidon(outputAmount, outputBlinder, outputTimestamp)
  3. sum(inputAmounts) == sum(outputAmounts)
  4. 所有金額在 [0, 2^96) 範圍內
  5. 所有 blinder ≠ 0

約束數量:~1,985 (2→2), ~2,886 (5→1)

金額電路 (AmountCircuit)

用於存款和取款 —— 驗證承諾與公開金額匹配:

公開輸入:txHash, inputCommitment, outputCommitment, absAmount, isWithdraw, outputTimestamp
私有輸入:inputAmount, inputBlinder, outputAmount, outputBlinder

約束條件:
存款 (Deposit):absAmount == outputAmount,驗證輸出承諾
取款 (Withdraw):inputAmount == absAmount + outputAmount,驗證兩個承諾

約束數量:~782 (存款), ~1,240 (帶找零的取款)

Poseidon 參數

哈希:Poseidon
寬度:t = 3 (2 個輸入 + 1 個容量)
輪數:RF = 8 全輪, RP = 57 部分輪
域:BN254 Fr

授權邏輯

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

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:所有者即為地址本身

}

可選擴展

審計支持

用戶可以註冊審計員公鑰。交易中的加密備註(Memo)允許授權審計員解密金額。

interface IVOSA20Auditing {
function registerAuditor(address account, bytes calldata auditorPubKey, bytes calldata ownerSig) external;
function removeAuditor(address account, bytes calldata ownerSig) external;
function getAuditor(address account) external view returns (bytes memory);
function setGlobalAuditor(bytes calldata auditorPubKey) external; // 僅限所有者
function globalAuditorKey() external view returns (bytes memory);
}

胖代幣模式 (Fat Token Mode)

單個合約同時具備公開 ERC-20 和私有 VOSA 層。用戶可以將公開餘額 shield()(屏蔽)入私有層,或 unshield()(解除屏蔽)回來。適用於希望將隱私作為可選功能的代幣。

interface IFatVOSA20 is IERC20 {
function shield(uint256 amount, address output, bytes32 commitment, ...) external;
function unshield(address input, bytes32 inputCommitment, uint256 amount, ...) external;
function publicBalance(address account) external view returns (uint256);
}

週期清理

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

interface IVOSA20Cleanup {
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; // 僅限所有者
}

原理闡述 (Rationale)

為什麼選擇 Poseidon 哈希?

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

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

為什麼需要兩個驗證器?

分離電路可以針對每種用例進行優化:

  • AmountCircuit: 帶有公開金額的簡單存/取款 (~782–1,240 個約束)

  • TransferCircuit: 帶有隱藏金額的複雜多輸入/輸出轉帳 (~1,985–2,886 個約束)

為什麼使用 SPENT_MARKER 而非 Nullifiers?

維度NullifierSPENT_MARKER
轉帳圖譜隱藏公開
電路複雜度較高 (默克爾成員證明)較低
狀態查詢O(log n)O(1)
狀態增長無限制有限制 (週期清理)
合規性困難容易

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

轉帳圖譜公開意味著觀察者可以看到哪些 VOSA 被同時消耗 —— 但看不到金額。對於大多數用例(薪資隱私、商業交易、個人財務),隱藏金額和餘額才是核心需求。且這使得合規變得簡單直接。

向後相容性

  • ERC-20: deposit() 使用標準 transferFrom;withdraw() 使用標準 transfer。底層代幣保持不變。

  • EIP-712: 所有簽名均使用類型化結構數據。支持 EIP-712 的錢包(MetaMask 等)可直接使用。

  • ERC-5564: VOSA 地址遵循隱身地址推導模式。

  • 基礎設施: 標準 eth_call 用於查詢,基於事件的索引,相容區塊瀏覽器(金額隱藏在承諾中)。

安全性考量

可信設置 (Trusted Setup)

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

搶跑保護 (Front-Running Protection)

  • EIP-712 簽名綁定到特定參數(輸入、輸出、承諾、截止時間)

  • 截止時間 (Deadline) 防止過期交易

  • ZK 證明需要私有輸入 (blinder) —— 無法從公開數據偽造

雙重支出 (Double-Spending)

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

金額安全

  • 範圍證明強制執行 0 ≤ amount < 2^96(防止負數金額 / 溢出)

  • ZK 電路中強制執行餘額守恆:sum(inputs) == sum(outputs)

  • 電路中強制執行 blinder ≠ 0(防止承諾變成金額的簡單哈希)

地址安全

  • 地址碰撞概率:~2^-80(對於 20 位元組地址可忽略不計)

  • 地址搶佔:攻擊者在不知道私鑰的情況下無法預先聲明 VOSA

  • 重放保護:DOMAIN_SEPARATOR 包含 chainId + 合約地址;每個 VOSA 僅能支出一次

時間戳驗證

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

參考實現

代碼庫:[GitHub 連結]

contracts/
├── WrappedVOSA20.sol # 主合約 (EIP-712, Pausable, ReentrancyGuard, Ownable)
├── interfaces/
│ └── IVOSA20.sol # 完整介面
├── libraries/
│ └── Poseidon.sol # Poseidon 哈希 (BN254 Fr)
└── verifiers/
├── TransferVerifier.sol # 用於轉帳的 Groth16 驗證器
└── AmountVerifier.sol # 用於存/取款的 Groth16 驗證器

circuits/
├── configs/
│ ├── amount_0_1.circom # 存款 (0→1)
│ ├── amount_1_0.circom # 全額取款 (1→0)
│ ├── amount_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)
│ ├── transfer_10_1.circom # 大規模合併 (10→1)
│ └── ... # 其他配置
└── lib/
├── chunked_poseidon.circom # 分塊 Poseidon 哈希 (用於可變輸入)
├── hash_commitment.circom # 承諾 + 範圍證明 (amount < 2^96)
└── sum.circom # 餘額守恆檢查

測試用例

要求覆蓋:

  • 存款 (Deposit):正確的承諾、ZK 驗證、拒絕帶有轉帳手續費的代幣

  • 轉帳 (Transfer):多輸入/輸出 (1→2, 2→2, 5→1)、餘額守恆、重複/重疊檢查

  • 取款 (Withdraw):帶找零的全額和部分取款

  • 合併 (Consolidate):多個 VOSA 合併為一個,強制執行相同所有者

  • 策略 (Policy):創建、使用(支出者)、撤銷(所有者)、過期、自動遷移至找零地址

  • 許可 (Permit):創建、使用(一次性)、撤銷、過期

  • 清理 (Cleanup):週期窗口、批量清理、無法清理活躍的 VOSA

  • 安全 (Security):重放保護、雙重支出、搶跑、溢出

版權

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

        1 則貼文 - 1 位參與者

        [閱讀完整主題](https://ethereum-magicians.org/t/draft-erc-vosa-20-privacy-preserving-wrapped-erc-20-token-standard/27832)
https://ethereum-magicians.org/t/draft-erc-vosa-20-privacy-preserving-wrapped-erc-20-token-standard/27832