This standard defines IZRC20, a minimal interface for privacy-preserving fungible tokens on Ethereum. It uses zero-knowledge proofs to enable confidential transfers where transaction amounts, sender, and recipient identities remain hidden. The core mechanism relies on cryptographic commitments stored in a Merkle tree, with nullifiers preventing double-spending.
This interface serves as a foundational building block for both wrapper protocols (adding privacy to existing ERC-20 tokens) and dual-mode tokens (single tokens supporting both transparent and private transfers).
While building privacy solutions for Ethereum, we identified recurring patterns:
Wrapper Protocols (ERC-20 → Privacy → ERC-20):
DAI (transparent) → zDAI (private) → DAI (transparent)
Dual-Mode Tokens (Public ↔ Private in one token):
Single Token: Public mode (ERC-20) ↔ Private mode (ZK-based)
The Solution: Standardize the privacy primitive to enable:
This standard is not a replacement for Wrapper Protocols or Dual-Mode Protocol. It is the privacy foundation they can build upon:
Ecosystem Stack: ┌─────────────────────────────────────┐ │ Applications (DeFi, DAO, Gaming) │ ├─────────────────────────────────────┤ │ Dual-Mode Tokens │ ← Optional privacy │ Wrapper Protocols │ ← Add privacy to existing ├─────────────────────────────────────┤ │ Native Privacy Token Interface │ ← This standard (foundation) ├─────────────────────────────────────┤ │ Ethereum L1 / L2s │ └─────────────────────────────────────┘
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.
(amount, publicKey, randomness) for recipient// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
/**
* @title IZRC20
* @notice Minimal interface for native privacy assets on Ethereum
* @dev This standard defines the foundation for privacy-preserving tokens
* that can be used directly or as building blocks for wrapper protocols
* and dual-mode protocols implementations.
*/
interface IZRC20 {
// ═══════════════════════════════════════════════════════════════════════
// Events
// ═══════════════════════════════════════════════════════════════════════
/**
* @notice Emitted when a commitment is added to the Merkle tree
* @param subtreeIndex Subtree index (0 for single-tree implementations)
* @param commitment The cryptographic commitment hash
* @param leafIndex Position within subtree (or global index)
* @param timestamp Block timestamp of insertion
* @dev For single-tree: subtreeIndex SHOULD be 0, leafIndex is global position
* @dev For dual-tree: subtreeIndex identifies which subtree, leafIndex is position within it
*/
event CommitmentAppended(
uint32 indexed subtreeIndex,
bytes32 commitment,
uint32 indexed leafIndex,
uint256 timestamp
);
/**
* @notice Emitted when a nullifier is spent (note consumed)
* @param nullifier The unique nullifier hash
* @dev Once spent, nullifier can never be reused (prevents double-spending)
*/
event NullifierSpent(bytes32 indexed nullifier);
/**
* @notice Emitted when tokens are minted directly into privacy mode
* @param minter Address that initiated the mint
* @param commitment The commitment created for minted value
* @param encryptedNote Encrypted note for recipient
* @param subtreeIndex Subtree where commitment was added
* @param leafIndex Position within subtree
* @param timestamp Block timestamp of mint
*/
event Minted(
address indexed minter,
bytes32 commitment,
bytes encryptedNote,
uint32 subtreeIndex,
uint32 leafIndex,
uint256 timestamp
);
/**
* @notice Emitted on privacy transfers with public scanning data
* @param newCommitments Output commitments created (typically 1-2)
* @param encryptedNotes Encrypted notes for recipients
* @param ephemeralPublicKey Ephemeral public key for ECDH key exchange (if used)
* @param viewTag Scanning optimization byte (0 if not used)
* @dev Provides data for recipients to detect and decrypt their notes
*/
event Transaction(
bytes32[2] newCommitments,
bytes[] encryptedNotes,
uint256[2] ephemeralPublicKey,
uint256 viewTag
);
// ═══════════════════════════════════════════════════════════════════════
// Metadata (ERC-20 compatible, OPTIONAL but RECOMMENDED)
// ═══════════════════════════════════════════════════════════════════════
/**
* @notice Returns the token name
* @return Token name string
* @dev OPTIONAL but RECOMMENDED for UX and interoperability
*/
function name() external view returns (string memory);
/**
* @notice Returns the token symbol
* @return Token symbol string
* @dev OPTIONAL but RECOMMENDED for UX and interoperability
*/
function symbol() external view returns (string memory);
/**
* @notice Returns the number of decimals
* @return Number of decimals (typically 18)
* @dev OPTIONAL but RECOMMENDED for amount formatting
*/
function decimals() external view returns (uint8);
/**
* @notice Returns the total supply across all privacy notes
* @return Total token supply
* @dev OPTIONAL - May be required for certain economic models (e.g., fixed cap)
* Individual balances remain private; only aggregate supply is visible
*/
function totalSupply() external view returns (uint256);
// ═══════════════════════════════════════════════════════════════════════
// Core Functions
// ═══════════════════════════════════════════════════════════════════════
/**
* @notice Mints new privacy tokens
* @param proofType Type of proof to support multiple proof strategies.
* @param proof Zero-knowledge proof of valid transfer
* @param encryptedNote Encrypted note for minter's wallet
* @dev Proof must demonstrate valid commitment creation and payment
* Implementations define minting rules
*/
function mint(
uint8 proofType,
bytes calldata proof,
bytes calldata encryptedNote
) external payable;
/**
* @notice Executes a privacy-preserving transfer
* @param proofType Implementation-specific proof type identifier
* @param proof Zero-knowledge proof of valid transfer
* @param encryptedNotes Encrypted output notes (for recipient and/or change)
* @dev Proof must demonstrate:
* 1. Input commitments exist in Merkle tree
* 2. Prover knows private keys
* 3. Nullifiers not spent
* 4. Value conservation: sum(inputs) = sum(outputs)
*/
function transfer(
uint8 proofType,
bytes calldata proof,
bytes[] calldata encryptedNotes
) external;
// ═══════════════════════════════════════════════════════════════════════
// Query Functions
// ═══════════════════════════════════════════════════════════════════════
/**
* @notice Check if a nullifier has been spent
* @param nullifier The nullifier to check
* @return True if nullifier spent, false otherwise
* @dev Implementations using `mapping(bytes32 => bool) public nullifiers`
* will auto-generate this function.
*/
function nullifiers(bytes32 nullifier) external view returns (bool);
/**
* @notice Returns the current active subtree Merkle root
* @return The root hash of the active subtree
* @dev The active subtree stores recent commitments for faster proof computation.
* For dual-tree implementations, this is the root of the current working subtree.
*/
function activeSubtreeRoot() external view returns (bytes32);
// ═══════════════════════════════════════════════════════════════════════
// Privacy Configuration (OPTIONAL but RECOMMENDED for client interoperability)
// ═══════════════════════════════════════════════════════════════════════
/**
* @notice Returns the URI pointing to the Privacy Configuration File
* @return URI string (e.g., "ipfs://Qm..." or "https://...")
* @dev OPTIONAL but RECOMMENDED for client interoperability
* The configuration file contains implementation-specific parameters
* See specification for the full Privacy Configuration File schema
*/
function privacyConfigURI() external view returns (string memory);
/**
* @notice Sets the Privacy Configuration File URI
* @param configURI The configuration URI (can be set multiple times to update)
* @dev OPTIONAL - Implementation may restrict access (e.g., onlyOwner)
* Each call overwrites the previous URI
*/
function setPrivacyConfigURI(string calldata configURI) external;
}
Since this standard defines a minimal interface, different implementations may use different:
To enable client interoperability across different implementations, this standard defines an OPTIONAL but RECOMMENDED Privacy Configuration File mechanism, inspired by ERC-8004's Agent Registration File pattern.
Implementations SHOULD provide:
privacyConfigURI(): Returns the URI pointing to the configuration filesetPrivacyConfigURI(string): Sets/updates the configuration URI (typically owner-restricted)The configuration file MUST be a valid JSON document. The following shows the schema structure with field descriptions:
{
"type": "<schema-identifier-url>",
"version": "<semver>",
"name": "<token-name>",
"symbol": "<token-symbol>",
"proofSystem": {
"protocol": "<protocol-name>",
"curve": "<curve-name>",
"fieldSize": "<field-prime-decimal>"
},
"treeConfig": {
"type": "<tree-type>",
"levels": "<tree-height>",
"subtreeLevels": "<subtree-height>",
"rootTreeLevels": "<root-tree-height>"
},
"circuits": {
"<CIRCUIT_NAME>": {
"proofType": "<uint8>",
"wasmUrl": "<circuit-wasm-url>",
"zkeyUrl": "<proving-key-url>",
"publicSignals": "<signal-count>",
"publicSignalsSchema": [
{ "name": "<signal-name>", "type": "<solidity-type>", "index": "<position>" }
]
}
},
"noteEncryption": {
"algorithm": "<encryption-algorithm-chain>",
"curve": "<ecdh-curve-name>",
"curveParams": {
"subgroupOrder": "<curve-subgroup-order-decimal>",
"baseField": "<base-field-name>"
},
"domainSeparator": "<ecdh-domain-string>",
"aadTag": "<aes-gcm-aad-string>",
"noteFormat": "<format-identifier>",
"noteSchema": { }
},
"hashFunction": {
"name": "<hash-function-name>",
"parameters": { }
},
"keyDerivation": {
"method": "<derivation-method>",
"addressFormat": "<stealth-address-format>"
},
"endpoints": {
"indexer": "<indexer-api-url>",
"relayer": "<relayer-api-url>"
}
}
type (REQUIRED)Purpose: Schema identifier URL for version detection and format validation.
Format: URL string pointing to the specification version.
Example: https:// ... #privacy-config-v1
Client Usage: Clients SHOULD check this field first to ensure they can parse the configuration format. Unknown types SHOULD be rejected.
version (REQUIRED)Purpose: Semantic version of this specific configuration file.
Format: Semantic versioning string (MAJOR.MINOR.PATCH).
Example: "1.0.0"
Client Usage: Clients MAY cache configurations and use version for cache invalidation.
proofSystem (REQUIRED)Purpose: Specifies the zero-knowledge proof system parameters.
| Subfield | Required | Description |
|---|---|---|
protocol |
YES | ZK protocol name. Common values:groth16, plonk, fflonk, stark |
curve |
YES | Elliptic curve for the proof system. Common values:bn128 (alt_bn128), bls12-381 |
fieldSize |
YES | The scalar field prime as a decimal string. This is the maximum value for any public signal. |
Example:
{
"protocol": "groth16",
"curve": "bn128",
"fieldSize": "21888242871839275222246405745257275088548364400416034343698204186575808495617"
}
How to obtain fieldSize:
bn128: This is the BN254 scalar field prime (Fr), a 254-bit primebls12-381: Use the BLS12-381 scalar field primeClient Usage: Clients MUST validate that all public signals are less than fieldSize. The protocol determines which proof verification library to use.
treeConfig (REQUIRED)Purpose: Specifies the Merkle tree structure for storing commitments.
| Subfield | Required | Description |
|---|---|---|
type |
YES | Tree architecture:single or dual-layer |
levels |
CONDITIONAL | Total tree height (required for single type) |
subtreeLevels |
CONDITIONAL | Active subtree height (required for dual-layer type) |
rootTreeLevels |
CONDITIONAL | Root tree height (required for dual-layer type) |
Example (single tree):
{
"type": "single",
"levels": 20
}
Example (dual-layer tree):
{
"type": "dual-layer",
"subtreeLevels": 16,
"rootTreeLevels": 20
}
Client Usage: Clients use this to build correct Merkle proofs. Tree capacity = 2^levels (or 2^subtreeLevels × 2^rootTreeLevels for dual-layer).
circuits (REQUIRED)Purpose: Maps operation types to their circuit artifacts and public signal schemas.
Each circuit entry contains:
| Subfield | Required | Description |
|---|---|---|
proofType |
YES | The uint8 value to pass to the contract's mint() or transfer() function |
wasmUrl |
YES | URL to the circuit's WASM file for proof generation |
zkeyUrl |
YES | URL to the proving key file |
vkeyUrl |
NO | URL to the verification key (optional, verification happens on-chain) |
publicSignals |
YES | Number of public signals in the proof |
publicSignalsSchema |
YES | Array describing each public signal's name, type, and position |
Example:
{
"MINT": {
"proofType": 0,
"wasmUrl": "ipfs://Qm.../Mint.wasm",
"zkeyUrl": "ipfs://Qm.../Mint_final.zkey",
"publicSignals": 4,
"publicSignalsSchema": [
{ "name": "merkleRoot", "type": "bytes32", "index": 0 },
{ "name": "commitment", "type": "bytes32", "index": 1 },
{ "name": "amount", "type": "uint256", "index": 2 },
{ "name": "timestamp", "type": "uint256", "index": 3 }
]
},
"TRANSFER": {
"proofType": 1,
"wasmUrl": "ipfs://Qm.../Transfer.wasm",
"zkeyUrl": "ipfs://Qm.../Transfer_final.zkey",
"publicSignals": 8,
"publicSignalsSchema": [
{ "name": "nullifier", "type": "bytes32", "index": 0 },
{ "name": "newCommitment", "type": "bytes32", "index": 1 }
]
}
}
Client Usage:
publicSignalsSchema to correctly encode/decode proof public inputsproofType value to contract function callsnoteEncryption (REQUIRED)Purpose: Specifies the encryption algorithm and parameters for encrypted notes.
| Subfield | Required | Description |
|---|---|---|
algorithm |
YES | Encryption algorithm chain (e.g.,ECDH+HKDF+AES-GCM) |
curve |
YES | Elliptic curve for ECDH key exchange |
curveParams |
YES | Curve-specific parameters |
curveParams.subgroupOrder |
YES | The curve's subgroup order as decimal string |
curveParams.baseField |
NO | The base field the curve is defined over |
domainSeparator |
YES | Domain separator string for HKDF salt |
aadTag |
YES | Additional Authenticated Data tag for AES-GCM |
noteFormat |
YES | Format identifier for serialized encrypted notes |
noteSchema |
NO | Schema describing the plaintext note structure |
Example:
{
"algorithm": "BJJ-ECDH+HKDF-SHA256+AES-256-GCM",
"curve": "BabyJubjub",
"curveParams": {
"subgroupOrder": "2736030358979909402780800718157159386076813972158567259200215660948447373041",
"baseField": "bn128"
},
"domainSeparator": "pv1|bjj-ecdh|v1",
"aadTag": "pv1|note|v1",
"noteFormat": "BJJ"
}
Algorithm Format: <ECDH-variant>+<KDF>+<AEAD>
BJJ-ECDH (Baby Jubjub), secp256k1-ECDH, etc.HKDF-SHA256, HKDF-SHA512, etc.AES-256-GCM, ChaCha20-Poly1305, etc.How to obtain subgroupOrder:
Client Usage:
curve and curveParams for ECDH key exchangedomainSeparator as HKDF saltaadTag as AES-GCM additional authenticated datanoteFormat to identify encrypted note serialization formathashFunction (REQUIRED)Purpose: Specifies the hash function used for commitments, nullifiers, and Merkle tree.
| Subfield | Required | Description |
|---|---|---|
name |
YES | Hash function name:Poseidon, MiMC, Pedersen, etc. |
parameters |
NO | Hash-specific parameters (e.g., number of rounds, t-value) |
Example:
{
"name": "Poseidon",
"parameters": {
"t": 3,
"nRoundsF": 8,
"nRoundsP": 57
}
}
Client Usage: Clients use this hash function for computing commitments, nullifiers, and Merkle tree nodes locally.
keyDerivation (OPTIONAL)Purpose: Describes how users derive privacy keys from their wallet.
| Subfield | Required | Description |
|---|---|---|
method |
NO | Key derivation method: EIP-712 Signature , BIP-32, etc. |
addressFormat |
NO | Stealth address format:PV1, custom format identifier |
Example:
{
"method": "EIP-712-Signature",
"addressFormat": "PV1"
}
Client Usage: Guides wallet integration for key derivation. This is informational and clients MAY use different methods.
endpoints (OPTIONAL)Purpose: Service discovery for auxiliary infrastructure.
| Subfield | Required | Description |
|---|---|---|
indexer |
NO | URL to transaction indexing service API |
relayer |
NO | URL to gas relayer service API |
Example:
{
"indexer": "https://indexer.example.com/api/v1",
"relayer": "https://relayer.example.com/api/v1"
}
Client Usage: Clients MAY use these endpoints for enhanced functionality (faster sync, gas-free transfers).
The privacyConfigURI() function MAY return URIs using these schemes:
ipfs:// - IPFS content addressing (RECOMMENDED for immutability)https:// - HTTPS URLs (for dynamic updates)ar:// - Arweave permanent storagedata: - Base64 encoded inline data (for small configs)type field matches supported schema versionPurpose: Different proof types may be needed for:
Implementations MUST ensure:
balanceOf(address) queries (completely private)Option 1: Single Merkle Tree
subtreeIndex = 0 in eventsOption 2: Dual-Layer Tree (RECOMMENDED for scalability)
Implementations MUST document their architecture choice.
name(), symbol(), decimals() are OPTIONAL but RECOMMENDED for:
totalSupply() is OPTIONAL:
Ecosystem Benefits:
totalSupply()?Different use cases have different transparency requirements.
Use Cases Requiring totalSupply():
Design Decision: Make it OPTIONAL—let each implementation choose based on:
This flexibility enables the standard to serve both transparent-leaning (wrapper protocols) and privacy-maximalist (pure privacy tokens) use cases.
The proofType parameter is a key design decision for interface stability and implementation flexibility.
The Challenge:
Different implementations may need different proof strategies:
Design Decision: Single parameter routes to appropriate verifiers
function mint(uint8 proofType, bytes proof, ...) external;
function transfer(uint8 proofType, bytes proof, ...) external;
This standard intentionally defines a minimal interface to maximize implementation flexibility. However, this creates a challenge:
The Problem:
Different implementations may use: - Different proof systems (Groth16 vs PLONK vs STARK) - Different circuit designs (different public signals) - Different encryption algorithms - Different tree structures - Different proof encoding formats
Without standardization:
The Solution: Privacy Configuration File (inspired by ERC-8004)
┌─────────────────────────────────────────────────────────────┐ │ On-chain (IZRC20 Contract) │ │ - Minimal interface │ │ - privacyConfigURI() → "ipfs://Qm..." │ └────────────────────────┬────────────────────────────────────┘ │ points to ▼ ┌─────────────────────────────────────────────────────────────┐ │ Off-chain (Privacy Configuration File) │ │ - Complete implementation details │ │ - Circuit artifacts (WASM, ZKEY) │ │ - Public signals schema │ │ - Encryption algorithm │ │ - Tree structure │ │ - All client-needed parameters │ └─────────────────────────────────────────────────────────────┘
Benefits:
Design Choices:
These fields appear in the Transaction event, which is emitted for all privacy transfers.
The Client Synchronization Challenge:
For privacy tokens to work, clients must:
Without standardization, each implementation would use incompatible formats, fragmenting the ecosystem.
Encrypted Notes:
View Tags (OPTIONAL but RECOMMENDED):
This standard is designed as a foundation layer, enabling higher-level protocols without prescribing their exact form.
Wrapper Protocols: Add privacy to existing ERC-20 tokens
Conceptual pattern:
Benefits of standardization:
Dual-Mode Tokens Protocols: Single token with both modes
Conceptual pattern:
contract DualModeToken is ERC20, IZRC20 {
// Inherits both standards
// Adds mode conversion: toPrivacy() / toPublic()
}
Benefits of standardization:
This standard defines a minimal interface for native privacy assets. It is an independent interface implementation that does not depend on other protocols.
As described in the Motivation section, this standard serves as a foundational building block for higher-level protocols to rapidly implement privacy capabilities:
ERC-8086 Reference Implementation
Attack Vector: Reusing the same nullifier allows spending a commitment multiple times (double-spending).
Example:
Mitigation: Implementations MUST permanently track spent nullifiers and reject duplicates:
require(!nullifiers[nullifier], "Nullifier already spent");
nullifiers[nullifier] = true;
Each nullifier can only be used once. Nullifiers MUST never expire or be removed.
Attack: Submitting invalid proofs to create unauthorized commitments or spend notes without proper authorization.
Mitigation: Implementations MUST:
proofTypeAttack: Modifying or deleting commitments from the Merkle tree breaks proof validity and allows erasing transaction history.
Mitigation: Implementations MUST:
Any modification to historical commitments would invalidate all proofs referencing them.
Attack: Malicious circuits that don't enforce proper constraints allow minting tokens or stealing funds.
Critical Requirements: Zero-knowledge circuits MUST enforce:
Σ input amounts = Σ output amountsImplementations MUST use audited circuits and trusted setup ceremonies (or transparent setup schemes).
Copyright and related rights waived via CC0.