newsence
來源篩選

Draft ERC: VOSA-20 — Privacy-Preserving Wrapped ERC-20 Token Standard

Ethereum Magicians

Field Value Title VOSA-20 - Privacy-Preserving Wrapped ERC-20 Token Standard Status Draft Type Standards Track Category ERC Requires ERC-20, EIP-712, ERC-5564 Abstract This ERC defines VOSA-20, a standard interface for privacy-preserving wrapped ERC-20 tokens. VOSA-20 enables confidential transfers where transaction amounts and balances are hidden using Poseidon hash commitments, while validity is verified through on-chain zero-knowledge proofs (Groth16). The standard leverages the VOSA (Virtual One-time Sub-Account) model — each private balance lives at a standard 20-byte EVM address that is used exactly once (UTXO semantics), with ownership proven via standard ECDSA signatures (no custom wallet needed). Privacy Model : VOSA-20 provides Selective Privacy — amounts, balances, and real-world identity of VOSA holders (via stealth addresses) are hidden, but the VOSA-to-VOSA transfer graph remains publicly auditable. This deliberate design choice enables regulatory compliance while protecting financial privacy. Key Features : Arbitrary transfer amounts (not fixed denominations) On-chain ZK proof verification (fully decentralized) Standard 20-byte addresses + ECDSA signatures (EVM-native, MetaMask compatible) O(1) spent-tracking with configurable epoch cleanup (bounded state growth) Motivation The Need for Private Token Transfers Current ERC-20 tokens expose all transaction details — sender/receiver addresses, transfer amounts, and complete transaction history. This creates privacy concerns for salary payments, business transactions, and personal finances. Privacy Comparison Solution Amount Privacy VOSA Holder Identity Transfer Graph Compliance ERC-20 Transparent VOSA-20 (by design) Auditable Tornado/Railgun Difficult Design Philosophy VOSA-20 targets Balance Privacy , not Anonymous Mixing : Corporate financial privacy (hide amounts from competitors) Personal balance privacy (hide wallet holdings) Compliant private transactions (satisfy KYC/AML) Full anonymity requirements (transfer graph is traceable) Specification The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174. Overview ERC-20 (public) ──deposit()──▶ VOSA-20 (private) ──withdraw()──▶ ERC-20 (public) │ ├── transfer() (private → private, amounts hidden) ├── consolidate() (merge N VOSAs → 1) ├── createPolicy() (recurring authorization) └── createPermit() (one-time authorization) On-chain state is a single mapping: mapping(address => bytes32) balanceCommitmentHash; // bytes32(0) → never used // Poseidon(amt, blind, ts) → has balance (commitment hides amount) // SPENT_PREFIX | block.number → spent (deletable after configurable window) Dependencies ERC-20 : Underlying token standard EIP-712 : Typed structured data signing ERC-5564 : Stealth address standard Constants /// @notice Spent marker prefix (high 128 bits) bytes16 public constant SPENT_PREFIX = 0xDEADDEADDEADDEADDEADDEADDEADDEAD; /// @notice Maximum inputs per transaction (DoS protection) uint256 public constant MAX_INPUTS = 10; /// @notice Maximum outputs per transaction uint256 public constant MAX_OUTPUTS = 10; Core Interface 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 State Management interface IVOSA20VOSA { /// @return bytes32(0) if unused, SPENT_MARKER if spent, commitment if active 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=normal, 1=Policy, 2=Permit function getAddressType(address vosa) external view returns (uint8); } Core Operations interface IVOSA20Core { /// @notice Wrap ERC-20 into private VOSA balance function deposit( uint256 amount, address recipient, bytes32 commitment, uint256 outputTimestamp, bytes calldata ephemeralPubKey, bytes calldata proof, bytes calldata memo ) external returns (bool); /// @notice Private transfer (N inputs → M outputs, amounts hidden) 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 Unwrap to ERC-20 (optionally with change to new 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 Merge multiple VOSAs into one (same owner) 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); } Authorization Interfaces Policy (Recurring Authorization) Analogous to ERC-20 approve , but for one-time-use addresses. Owner authorizes a spender; when the spender transfers, the Policy auto-migrates to the change VOSA. interface IVOSA20Policy { struct PolicyMeta { address owner; address spender; uint256 expiry; // 0 = never expires 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 (One-time Authorization) Single-use authorization. After use, used = true and the Permit is consumed. No migration. 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 Type Definitions 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 Circuit Specifications Commitment Format commitment = Poseidon(amount, blinder, timestamp) Where: - amount: uint256, MUST be < 2^96 - blinder: Field element, MUST NOT be 0 (CSPRNG generated) - timestamp: uint256, for replay protection (validated within ±2 hour window) TransferCircuit For transfer and consolidate — verifies balance conservation without revealing amounts: Public inputs: txHash, inputCommitments[], outputCommitments[], outputTimestamps[] Private inputs: inputAmounts[], inputBlinders[], inputTimestamps[], outputAmounts[], outputBlinders[] Constraints: 1. Each inputCommitment == Poseidon(inputAmount, inputBlinder, ...) 2. Each outputCommitment == Poseidon(outputAmount, outputBlinder, outputTimestamp) 3. sum(inputAmounts) == sum(outputAmounts) 4. All amounts in [0, 2^96) 5. All blinders ≠ 0 Constraints: ~1,985 (2→2), ~2,886 (5→1) AmountCircuit For deposit and withdraw — verifies commitment matches public amount: Public inputs: txHash, inputCommitment, outputCommitment, absAmount, isWithdraw, outputTimestamp Private inputs: inputAmount, inputBlinder, outputAmount, outputBlinder Constraints: Deposit: absAmount == outputAmount, outputCommitment check Withdraw: inputAmount == absAmount + outputAmount, both commitment checks Constraints: ~782 (deposit), ~1,240 (withdraw with change) Poseidon Parameters Hash: Poseidon Width: t = 3 (2 inputs + 1 capacity) Rounds: RF = 8 full, RP = 57 partial Field: BN254 Fr Authorization Logic At any moment, each VOSA has exactly one authorized signer — zero concurrency conflicts: 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; // Normal VOSA: owner is the address itself } Optional Extensions Auditing Support Users MAY register auditor public keys. Encrypted memos in transactions allow authorized auditors to decrypt amounts. 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; // owner only function globalAuditorKey() external view returns (bytes memory); } Fat Token Mode A single contract with both public ERC-20 and private VOSA layers. Users can shield() public balance into private, or unshield() back. Useful for tokens that want privacy as an opt-in feature. 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); } Epoch Cleanup SPENT markers encode the block number when spent: SPENT_PREFIX | uint128(block.number) . After a configurable window (default ~216,000 blocks / ~1 month), anyone MAY call cleanup() to delete expired entries and save 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; // owner only } Rationale Why Poseidon Hash? Hash R1CS Constraints (per commitment) Proof Time (deposit) SHA256 ~30,000 ~3s Pedersen ~3,400 ~500ms Poseidon ~350 ~92ms Poseidon is ZK-friendly (no curve operations needed), resulting in significantly smaller circuits and faster proofs. Why Two Verifiers? Separating circuits optimizes for each use case: AmountCircuit : Simple deposit/withdraw with public amount (~782–1,240 constraints) TransferCircuit : Complex multi-input/output with hidden amounts (~1,985–2,886 constraints) Why SPENT_MARKER Instead of Nullifiers? Aspect Nullifier SPENT_MARKER Transfer graph Hidden Visible Circuit complexity Higher (Merkle membership proof) Lower State lookup O(log n) O(1) State growth Unbounded Bounded (epoch cleanup) Compliance Difficult Easy VOSA-20’s design choice: Compliance + simplicity > Full anonymity . The transfer graph being visible means an observer can see which VOSAs were consumed together — but not the amounts. For most use cases (salary privacy, business transactions, personal finance), hiding amounts and balances is what matters. And it makes compliance straightforward. Backwards Compatibility ERC-20 : deposit() uses standard transferFrom ; withdraw() uses standard transfer . Underlying token is unchanged. EIP-712 : All signatures use typed structured data. Wallets that support EIP-712 (MetaMask, etc.) work out of the box. ERC-5564 : VOSA addresses follow the stealth address derivation pattern. Infrastructure : Standard eth_call for queries, event-based indexing, block explorer compatible (amounts hidden in commitments). Security Considerations Trusted Setup Groth16 requires a trusted setup ceremony. RECOMMENDED: Powers of Tau with 100+ participants and published transcripts. Future implementations MAY consider PLONK for universal setup. Front-Running Protection EIP-712 signatures bind to specific parameters (inputs, outputs, commitments, deadline) Deadline prevents stale transactions ZK proofs require private inputs (blinder) — cannot be forged from public data Double-Spending Each VOSA is spent exactly once. The SPENT_MARKER is set atomically with ReentrancyGuard . The commitment check balanceCommitmentHash[vosa] == inputCommitment implicitly rejects SPENT (prefix doesn’t match any valid Poseidon hash) and unused addresses ( bytes32(0) doesn’t match). Amount Security Range proofs enforce 0 ≤ amount < 2^96 (prevents negative amounts / overflow) Balance conservation enforced in ZK circuit: sum(inputs) == sum(outputs) Blinder ≠ 0 enforced in circuit (prevents commitment from being a simple hash of the amount) Address Security Address collision probability: ~2^-80 (negligible for 20-byte addresses) Address squatting: An attacker cannot pre-claim a VOSA without knowing the private key Replay protection: DOMAIN_SEPARATOR includes chainId + contract address; each VOSA spent only once Timestamp Validation Output timestamps MUST be within [block.timestamp - 2 hours, block.timestamp] . This prevents replay of old transactions while allowing reasonable clock drift. Reference Implementation Repository: [GitHub link] contracts/ ├── WrappedVOSA20.sol # Main contract (EIP-712, Pausable, ReentrancyGuard, Ownable) ├── interfaces/ │ └── IVOSA20.sol # Full interface ├── libraries/ │ └── Poseidon.sol # Poseidon hash (BN254 Fr) └── verifiers/ ├── TransferVerifier.sol # Groth16 verifier for transfers └── AmountVerifier.sol # Groth16 verifier for deposit/withdraw circuits/ ├── configs/ │ ├── amount_0_1.circom # Deposit (0→1) │ ├── amount_1_0.circom # Full withdraw (1→0) │ ├── amount_1_1.circom # Partial withdraw (1→1) │ ├── transfer_1_2.circom # Split (1→2) │ ├── transfer_2_1.circom # Merge (2→1) │ ├── transfer_2_2.circom # Standard transfer (2→2) │ ├── transfer_5_1.circom # Consolidate (5→1) │ ├── transfer_10_1.circom # Large consolidate (10→1) │ └── ... # Additional configurations └── lib/ ├── chunked_poseidon.circom # Poseidon hash (chunked for variable inputs) ├── hash_commitment.circom # Commitment + range proof (amount < 2^96) └── sum.circom # Balance conservation check Test Cases Required coverage: Deposit : Correct commitment, ZK verification, fee-on-transfer rejection Transfer : Multi-input/output (1→2, 2→2, 5→1), balance conservation, duplicate/overlap checks Withdraw : Full and partial withdrawal with change Consolidate : Multiple VOSAs to one, same-owner enforcement Policy : Create, use (spender), revoke (owner), expire, auto-migrate to change Permit : Create, use (one-time), revoke, expire Cleanup : Epoch window, batch cleanup, cannot clean active VOSAs Security : Replay prevention, double-spend, front-running, overflow Copyright Copyright and related rights waived via CC0 . 1 post - 1 participant Read full topic

newsence

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

Ethereum Magicians
3 天前

AI 生成摘要

本 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)進行驗證。

該標準利用了 模型 —— 每個私有餘額都存在於一個標準的 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?

維度 | Nullifier | SPENT_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):重放保護、雙重支出、搶跑、溢出

版權

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

        1 則貼文 - 1 位參與者        [閱讀完整主題](https://ethereum-magicians.org/t/draft-erc-vosa-20-privacy-preserving-wrapped-erc-20-token-standard/27832)