ERC-8086 - Privacy Token

Created 2025-11-19
Status Draft
Category ERC
Type Standards Track
Authors

Abstract

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

Motivation

Privacy Infrastructure Needs Standardization

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:

Design Philosophy

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 │ └─────────────────────────────────────┘

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.

Definitions

Core Interface

// 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;
}

Privacy Configuration File

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.

Configuration URI Functions

Implementations SHOULD provide:

Privacy Configuration File Schema

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>"
  }
}

Field Specifications

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:

Client 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:

  1. Download WASM and ZKEY files for required operations
  2. Use publicSignalsSchema to correctly encode/decode proof public inputs
  3. Pass proofType value to contract function calls
noteEncryption (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>

How to obtain subgroupOrder:

Client Usage:

  1. Use curve and curveParams for ECDH key exchange
  2. Use domainSeparator as HKDF salt
  3. Use aadTag as AES-GCM additional authenticated data
  4. Use noteFormat to identify encrypted note serialization format
hashFunction (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).

URI Schemes

The privacyConfigURI() function MAY return URIs using these schemes:

Client Integration Flow

  1. Client discovers privacy token at address 0x...
  2. Client calls privacyConfigURI() → "ipfs://Qm..."
  3. Client fetches and parses configuration JSON
  4. Client validates type field matches supported schema version
  5. Client downloads circuit artifacts (WASM, ZKEY) from specified URLs
  6. Client can now:
  7. Generate proofs using correct circuits and public signals schema
  8. Encrypt notes using specified algorithm and parameters
  9. Compute hashes using specified hash function
  10. Interact with the token contract using correct proofType values

Proof Types

Purpose: Different proof types may be needed for:

Privacy Guarantees

Implementations MUST ensure:

  1. Amount Privacy: Transaction amounts not revealed in events/storage
  2. Sender Privacy: Sender identities not linkable across transactions
  3. Recipient Privacy: Recipient addresses not publicly visible
  4. Balance Privacy: No balanceOf(address) queries (completely private)

State Management Options

Option 1: Single Merkle Tree

Option 2: Dual-Layer Tree (RECOMMENDED for scalability)

Implementations MUST document their architecture choice.

Metadata Functions

name(), symbol(), decimals() are OPTIONAL but RECOMMENDED for:

totalSupply() is OPTIONAL:

Rationale

Why Include Metadata Functions?

Ecosystem Benefits:

Why Optional 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.

Why Proof Types?

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;

Why Privacy Configuration File?

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:

  1. Interface stability: Core IZRC20 interface remains minimal and stable
  2. Implementation flexibility: Each project can use different proof systems
  3. Client interoperability: Universal clients can support any compliant token
  4. Upgradability: Configuration can be updated without contract changes
  5. Discoverability: Clients automatically learn how to interact with any token

Design Choices:

Why Standardize Encrypted Notes and View Tags?

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:

  1. Monitor blockchain events to detect received payments
  2. Decrypt note data to learn amounts and spending keys
  3. Build local state to construct future transactions

Without standardization, each implementation would use incompatible formats, fragmenting the ecosystem.

Encrypted Notes:

View Tags (OPTIONAL but RECOMMENDED):

How Higher-Level Protocols Build on This Standard

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:

  1. User deposits DAI into wrapper contract
  2. Wrapper mints privacy token (using IZRC20.mint)
  3. User transfers privately (using IZRC20.transfer)
  4. User withdraws to get DAI back

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:

Backwards Compatibility

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:

Reference Implementation

ERC-8086 Reference Implementation

Security Considerations

Critical: Nullifier Uniqueness

Attack Vector: Reusing the same nullifier allows spending a commitment multiple times (double-spending).

Example:

  1. Attacker has Note A (100 tokens)
  2. Creates valid proof spending Note A → generates Nullifier N
  3. If contract doesn't track nullifiers:
  4. First spend: Valid, creates new notes
  5. Second spend: Same proof, same nullifier N ← Should be rejected!
  6. Result: 100 tokens spent twice = 200 tokens from 100

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.

Proof Verification

Attack: Submitting invalid proofs to create unauthorized commitments or spend notes without proper authorization.

Mitigation: Implementations MUST:

Merkle Tree Integrity

Attack: 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.

Circuit Soundness

Attack: Malicious circuits that don't enforce proper constraints allow minting tokens or stealing funds.

Critical Requirements: Zero-knowledge circuits MUST enforce:

Implementations MUST use audited circuits and trusted setup ceremonies (or transparent setup schemes).

Copyright

Copyright and related rights waived via CC0.