This EIP defines a permissionless interface for fungible tokens that operate in two modes: transparent mode (fully compatible with ERC-20) and privacy mode (using ERC-8086 privacy primitives). Token holders can convert balances between modes. The transparent mode uses account-based balances, while the privacy mode uses the standardized IZRC20 interface from ERC-8086. Total supply is maintained as the sum of both modes.
Permissionless Nature: Anyone can implement and deploy dual-mode tokens using this standard without intermediaries, governance approval, or restrictions.
Motivation
The Privacy Dilemma for New Token Projects
When launching a new token, projects face a fundamental choice:
This standard embraces a core principle: “Privacy is a mode, not a separate token.”
Rather than forcing users to choose between incompatible assets (Token A vs. Privacy Token B), we enable contextual privacy within a single fungible token. Users select the appropriate mode for each use case, maintaining capital efficiency and unified liquidity.
This approach acknowledges that privacy and composability serve different purposes, and most users need both at different times—not a forced choice between them.
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
Transparent Mode: Token balance stored using standard ERC-20 accounting, publicly visible and queryable via balanceOf()
Privacy Mode: Token value hidden using cryptographic commitments in an authenticated data structure
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
Mode Conversion: The process of moving value between transparent and privacy modes
Privacy State: An authenticated data structure (e.g., Merkle tree, accumulator) tracking privacy mode commitments
BURN_ADDRESS: A provably unspendable elliptic curve point used to ensure privacy-to-transparent conversions are secure
Interface
/**
* @title IDualModeToken
* @notice Interface for dual-mode tokens (ERC-8085) combining ERC-20 and [ERC-8086](./eip-8086) (IZRC20)
* @dev Implementations MUST inherit both IERC20 and IZRC20
* Privacy events and core functions are inherited from IZRC20 (ERC-8086)
* This interface only defines mode conversion logic - the core value of ERC-8085
*
* Architecture:
* - Public Mode: Standard ERC-20 (transparent balances and transfers)
* - Privacy Mode: ERC-8086 IZRC20 (ZK-SNARK protected balances and transfers)
* - Mode Conversion: toPrivate (public → private) and toPublic (private → public)
*/interfaceIDualModeTokenisIERC20,IZRC20{// ═══════════════════════════════════════════════════════════════════════
// Mode Conversion Functions (Core of ERC-8085)
// ═══════════════════════════════════════════════════════════════════════
/**
* @notice Convert transparent balance to privacy mode
* @dev Burns ERC-20 tokens and creates privacy commitment via IZRC20
* @param amount Amount to convert (must match proof)
* @param proofType Type of proof to support multiple proof strategies.
* @param proof ZK-SNARK proof of valid commitment creation
* @param encryptedNote Encrypted note data for recipient wallet
*/functiontoPrivate(uint256amount,uint8proofType,bytescalldataproof,bytescalldataencryptedNote)external;/**
* @notice Convert privacy balance to transparent mode
* @dev Spends privacy notes and mints ERC-20 tokens to recipient
* @param recipient Address to receive public tokens
* @param proofType Type of proof to support multiple proof strategies.
* @param proof ZK-SNARK proof of note ownership and spending
* @param encryptedNotes Encrypted notes for change outputs (if any)
*/functiontoPublic(addressrecipient,uint8proofType,bytescalldataproof,bytes[]calldataencryptedNotes)external;// ═══════════════════════════════════════════════════════════════════════
// Supply Tracking
// ═══════════════════════════════════════════════════════════════════════
// Note: Privacy transfers use IZRC20.transfer(uint8, bytes, bytes[])
// which is inherited from IZRC20 (ERC-8086)
/**
* @notice Total supply across both modes (overrides IERC20 and IZRC20)
* @return Total supply = publicSupply + privacySupply
*/functiontotalSupply()externalviewoverride(IERC20,IZRC20)returns(uint256);/**
* @notice Get total supply in privacy mode
* @dev Tracked by increments/decrements during mode conversions
* @return Total privacy supply
*/functiontotalPrivacySupply()externalviewreturns(uint256);/**
* @notice Check if a nullifier has been spent
* @dev Alias for IZRC20.nullifiers() with different naming convention
* @param nullifier The nullifier hash to check
* @return True if spent, false otherwise
*/functionisNullifierSpent(bytes32nullifier)externalviewreturns(bool);}
Proof Type Parameter
The proofType parameter in toPrivate, toPublic, and privacyTransfer functions allows implementations to support multiple proof strategies.
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)
Incrementing on toPrivate() (public → private conversion)
Decrementing on toPublic() (private → public conversion)
NOT computed from Merkle tree (commitment values are encrypted)
Note: The public mode supply can be derived as totalSupply() - totalPrivacySupply() if needed, eliminating the need for a separate totalPublicSupply() function.
Rationale
BURN_ADDRESS Requirement for toPublic
Problem: When converting privacy-to-transparent, the ZK circuit enforces value conservation:
input_amount = output_amount
But we need to “convert” value from privacy mode to public mode. The circuit doesn’t know that the contract will create public balance, so we must ensure the converted value doesn’t remain spendable in privacy mode.
Solution: Force the first output to an unspendable address (BURN_ADDRESS):
Input: Note A (100)
Output: Note B → BURN_ADDRESS (50) ← Provably unspendable
Note C → User (50, change) ← Remains private
Contract: Creates 50 public balance for user
This ensures:
✅ Circuit value conservation: 100 = 50 + 50
✅ Security: Note B can never be spent (no private key exists)
✅ Supply invariant: totalSupply unchanged, just redistributed between modes
Backwards Compatibility
This standard is fully backward compatible with ERC-20 and ERC-8086:
All ERC-20 functions operate on transparent balances
All standard ERC-20 events are emitted
All ERC-8086 (IZRC20) functions and events are supported for privacy mode
Mitigation: ZK circuits enforce value conservation. Verifier contracts validate proofs on-chain before state changes. The invariant totalSupply() == sum(balanceOf) + totalPrivacySupply() must hold after every operation.