Discussion topic for ERC-8086:
Reference Implementation Update
A reference implementation is now available:
GitHub Repository: GitHub - 0xRowan/erc-8086-reference
Live Testnet Deployment (Base Sepolia):
- Factory:
0x8303A804fa17f40a4725D1b4d9aF9CB63244289c - PrivacyToken Implementation:
0xB329Dc91f458350a970Fe998e3322Efb08dDA7d1
Interactive Demo: https://testnative.zkprotocol.xyz/
All contracts are verified on Basescan. Anyone can deploy privacy tokens and test the implementation.
Abstract
This EIP defines a minimal interface standard for native privacy tokens on Ethereum.
While developing privacy solutions for the Ethereum ecosystemβincluding wrapper protocols (converting ERC-20 β privacy tokens) and dual-mode tokens (combining public and private balances)βwe identified a recurring need for standardized privacy primitives. Without a common interface, each implementation reinvents commitments, nullifiers, and note encryption, leading to ecosystem fragmentation.
This standard provides that common foundation. It enables:
- Wrapper protocols: Implement this interface for their privacy layer
- Dual-mode tokens protocols: Combine standards via
contract DMT is ERC20, IZRC20
By unifying the native privacy token interface, we facilitate the development of wrapper and dual-mode protocols, accelerating Ethereumβs privacy ecosystem growth.
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)
- 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 Protocols. It is the privacy foundation they can build upon:
Ecosystem Stack:
βββββββββββββββββββββββββββββββββββββββ
β Applications (DeFi, DAO, Gaming) β
βββββββββββββββββββββββββββββββββββββββ€
β Dual-Mode Tokens (ERC-20 + Privacy)β β Optional privacy
β Wrapper Protocols (ERC20βPrivacy) β β 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: Cryptographic binding
H(amount, publicKey, randomness)hiding value and recipient - Nullifier: Unique identifier
H(commitment, secretKey)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 (active/finalized/rollover transfers)
- View Tag: Single-byte scanning optimization (OPTIONAL but RECOMMENDED)
- Stealth Address: One-time recipient address from ephemeral keys (OPTIONAL)
Core Interface
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
/**
* @title IZRC20
* @notice Minimal interface for native privacy assets on Ethereum (ERC-8086)
* @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);
}