---
eip: 8086
title: Privacy Token
description: An abstract privacy foundation layer that can be used in both dual-mode tokens and wrapped tokens
author: Rowan (@0xRowan)
discussions-to: https://ethereum-magicians.org/t/erc-8086-privacy-token/26623
status: Draft
type: Standards Track
category: ERC
created: 2025-11-19
---

## 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](./eip-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](./eip-20) → Privacy → ERC-20):

DAI (transparent) → zDAI (private) → DAI (transparent)

- Each protocol implements custom privacy token logic
- No interoperability between different privacy implementations
- Duplicated effort, increased security risks

**Dual-Mode Tokens** (Public ↔ Private in one token):

Single Token: Public mode (ERC-20) ↔ Private mode (ZK-based)

- Needs a privacy primitive as foundation
- Current implementations reinvent the wheel

**The Solution**: Standardize the privacy primitive to enable:

- Consistent wrapper protocol implementations
- Reusable dual-mode token architectures
- Faster ecosystem development

### 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

- **Native Privacy Asset**: A token with privacy as an inherent property from genesis, not achieved through post-hoc mixing
- **Commitment**: A cryptographic binding of value and ownership that hides both the amount and recipient identity
- **Nullifier**: A unique identifier proving a commitment has been spent, preventing double-spending
- **Note**: Off-chain encrypted data `(amount, publicKey, randomness)` for recipient
- **Merkle Tree**: Authenticated structure storing commitments for zero-knowledge membership proofs
- **Proof Type**: Parameter routing different proof strategies

### Core Interface

```solidity
// 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:

- Proof systems (Groth16, PLONK, STARK, etc.)
- Circuit implementations (different WASM/ZKEY files)
- Note encryption algorithms
- Tree structures (single vs. dual-layer)
- Public signals schemas

To enable **client interoperability** across different implementations, this standard defines an **OPTIONAL but RECOMMENDED** Privacy Configuration File mechanism, inspired by [ERC-8004](./eip-8004.md)'s Agent Registration File pattern.

#### Configuration URI Functions

Implementations SHOULD provide:

- `privacyConfigURI()`: Returns the URI pointing to the configuration file
- `setPrivacyConfigURI(string)`: Sets/updates the configuration URI (typically owner-restricted)

#### Privacy Configuration File Schema

The configuration file MUST be a valid JSON document. The following shows the schema structure with field descriptions:

```json
{
  "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**:

```json
{
  "protocol": "groth16",
  "curve": "bn128",
  "fieldSize": "21888242871839275222246405745257275088548364400416034343698204186575808495617"
}
```

**How to obtain `fieldSize`**:

- For `bn128`: This is the BN254 scalar field prime (Fr), a 254-bit prime
- For `bls12-381`: Use the BLS12-381 scalar field prime
- Can be obtained from ZK cryptographic libraries or computed directly from curve parameters

**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)**:

```json
{
  "type": "single",
  "levels": 20
}
```

**Example (dual-layer tree)**:

```json
{
  "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**:

```json
{
  "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**:

```json
{
  "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>`

- ECDH variant: `BJJ-ECDH` (Baby Jubjub), `secp256k1-ECDH`, etc.
- KDF: `HKDF-SHA256`, `HKDF-SHA512`, etc.
- AEAD: `AES-256-GCM`, `ChaCha20-Poly1305`, etc.

**How to obtain `subgroupOrder`**:

- For Baby Jubjub: This is the order of the prime-order subgroup (~251 bits)
- Can be obtained from elliptic curve libraries or the Baby Jubjub curve specification

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

```json
{
  "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](./eip-712) Signature , `BIP-32`, etc. |
| `addressFormat` | NO       | Stealth address format:`PV1`, custom format identifier      |

**Example**:

```json
{
  "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**:

```json
{
  "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:

- `ipfs://` - IPFS content addressing (RECOMMENDED for immutability)
- `https://` - HTTPS URLs (for dynamic updates)
- `ar://` - Arweave permanent storage
- `data:` - Base64 encoded inline data (for small configs)

#### 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:
   - Generate proofs using correct circuits and public signals schema
   - Encrypt notes using specified algorithm and parameters
   - Compute hashes using specified hash function
   - Interact with the token contract using correct proofType values

### Proof Types

**Purpose**: Different proof types may be needed for:

- Different data structures (e.g., active vs. archived state in dual-tree implementations)
- Different optimization strategies (e.g., activeTree proofs vs. finalizedTree proofs)

### 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**

- One tree storing all commitments chronologically
- Simpler implementation
- Higher proof generation cost for large trees
- `subtreeIndex = 0` in events

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

- Active subtree (e.g., height 16): Recent commitments, fast proofs
- Root tree (e.g., height 20): Finalized subtree roots, archival
- Better performance for common operations (2-3x faster proofs)
- Decades of capacity (e.g., 68.7B notes with 16×20 config)

Implementations MUST document their architecture choice.

### Metadata Functions

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

- User interface display
- Wallet integration
- Ecosystem interoperability

`totalSupply()` is OPTIONAL:

- Required for fixed-cap verification
- Privacy trade-off: reveals aggregate supply (but not individual balances)
- Can be omitted for maximum privacy

## Rationale

### Why Include Metadata Functions?

**Ecosystem Benefits**:

- **Wallet Integration**: Existing wallets can display privacy tokens without special handling
- **Explorer Compatibility**: Block explorers show meaningful token information
- **Developer Familiarity**: Matches [ERC-20](./eip-20) conventions, reducing learning curve
- **Interoperability**: Higher-level protocols can query token metadata consistently

### Why Optional `totalSupply()`?

Different use cases have different transparency requirements.

**Use Cases Requiring `totalSupply()`**:

- Fixed-cap tokens (verify no hidden inflation)
- DAO treasuries (aggregate holdings visible)
- Regulatory compliance (prove total supply matches expectations)
- Wrapper protocols (track total wrapped amount)

**Design Decision**: Make it OPTIONAL—let each implementation choose based on:

- Target use case requirements
- Regulatory environment
- Privacy vs. transparency trade-offs

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:

- Simple single-tree implementations: One proof type for all operations
- Optimized dual-layer implementations: Different proofs for active vs. archived state
- Future optimizations: New proof types without breaking existing contracts

**Design Decision**: Single parameter routes to appropriate verifiers

```solidity
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**:

- Clients must be custom-built for each token implementation
- No universal privacy wallet possible
- Ecosystem fragmentation

**The Solution**: Privacy Configuration File (inspired by [ERC-8004](./eip-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**:

- **OPTIONAL but RECOMMENDED**: Not mandatory for simple implementations
- **URI-based**: Supports IPFS (immutable), HTTPS (dynamic), Arweave, etc.
- **JSON schema**: Easy to parse and validate
- **Complete schema**: Includes everything clients need to interact

### 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**:

- **Required for privacy**: Notes contain amounts and secrets that must stay private
- **Standardizing the concept**: Enables wallets to support multiple implementations
- **Not mandating the algorithm**: Implementations can use different encryption schemes
- **Event carries the ciphertext**: Clients know where to find their data

**View Tags** (OPTIONAL but RECOMMENDED):

- **Problem**: Scanning requires trial-decryption of every transaction (expensive)
- **Solution**: Small tag allows fast pre-filtering
- **Trade-off**: Minimal metadata leakage vs. practical usability
- **Standardizing usage**: Wallets can optimize scanning across implementations

### 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](./eip-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:

- All wrapper protocols use the same privacy interface
- Wallets support all wrappers without custom integration
- Security audits focus on the wrapper logic, not reinventing privacy primitives

**Dual-Mode Tokens Protocols**: Single token with both modes

Conceptual pattern:

```solidity
contract DualModeToken is ERC20, IZRC20 {
    // Inherits both standards
    // Adds mode conversion: toPrivacy() / toPublic()
}
```

Benefits of standardization:

- Public mode: Standard ERC-20 (works with all DeFi)
- Private mode: Standard IZRC20 (works with all privacy wallets)
- Higher-level protocols can focus more on specific business scenarios and solving concrete problems—this is the advantage of a unified privacy asset interface.

## 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](../assets/eip-8086/README.md)

## 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:
   - First spend: Valid, creates new notes
   - Second spend: Same proof, same nullifier N ← Should be rejected!
   - Result: 100 tokens spent twice = 200 tokens from 100

**Mitigation**: Implementations MUST permanently track spent nullifiers and reject duplicates:

```solidity
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:

- Verify all zero-knowledge proofs on-chain before any state changes
- Use verifier contracts (generated by trusted ZK frameworks)
- Validate all public signals match current contract state
- Route to correct verifier based on `proofType`

### Merkle Tree Integrity

**Attack**: Modifying or deleting commitments from the Merkle tree breaks proof validity and allows erasing transaction history.

**Mitigation**: Implementations MUST:

- Use append-only commitment trees (no deletions or modifications)
- Atomically update roots when adding commitments
- Verify old roots in proofs match current state before acceptance
- For dual-layer implementations: validate both active and finalized roots during state transitions

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:

- **Value conservation**: `Σ input amounts = Σ output amounts`
- **Merkle membership**: Input commitments exist in the tree
- **Nullifier binding**: Nullifiers are derived from commitments and private keys (prevents theft)
- **Public signal validation**: All public inputs match on-chain state

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

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
