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.
When launching a new token, projects face a fundamental choice:
This creates real-world problems: - DAOs need public treasury transparency but want anonymous governance voting - Businesses require auditable accounting but need private payroll transactions - Users want DeFi participation but need privacy for personal holdings
Existing solutions require trade-offs that limit adoption.
Mechanism: Wrap existing tokens (DAI, ETH) into a privacy pool contract.
DAI (public) → deposit → Privacy Pool → withdraw → DAI (public)
Strengths: - ✅ Works with any existing ERC-20 token - ✅ Permissionless deployment - ✅ No changes to underlying token required
Limitations for New Token Projects: - ❌ Creates two separate tokens (Token A vs. Wrapped Token B) - ❌ Splits liquidity between public and wrapped versions - ❌ Requires managing two separate contract addresses - ❌ Users must unwrap to access DeFi (additional friction)
Best suited for: Adding privacy to existing deployed tokens (DAI, USDC, etc.)
This standard provides a alternative option specifically designed for new token deployments that want privacy as a core feature from day one.
Target Use Case: Projects launching new tokens (governance tokens, protocol tokens, app tokens) that need both DeFi integration and optional privacy.
Mechanism: Single Token Contract ↓ Public Mode (ERC-20) ←→ Privacy Mode (ZK-SNARK) ↓ ↓ DeFi/DEX Trading Private Holdings
Key Advantages:
Simplified token distribution and airdrops
Seamless Mode Switching
toPrivate()toPublic()Users choose privacy per transaction, not per token
Full ERC-20 Compatibility
Standard totalSupply() accounting tracks both modes
Transparent Supply Tracking
totalSupply() includes both public and privacy mode balancestotalPrivacySupply() reveals aggregate privacy supply (no individual balances)Regulatory visibility into aggregate metrics
Permissionless Application-Layer Deployment
This standard is not a universal solution. Key constraints:
Cannot add privacy to existing tokens (use wrapper-based solutions for that)
Privacy-to-DeFi Requires Conversion
toPublic() before DeFi operationsPublic Mode: - Treasury management (transparent) - Grant distributions (auditable) - DEX trading (liquidity)
Privacy Mode: - Anonymous voting (no vote buying) - Private delegation (confidential strategy) - Personal holdings (no public scrutiny)
Public Mode: - Investor reporting (compliance) - Exchange listings (liquidity) - Public fundraising (transparency)
Privacy Mode: - Employee compensation (confidential) - Supplier payments (competitive advantage) - Strategic reserves (private holdings)
Public Mode: - Staking (DeFi integration) - Liquidity provision (AMM pools) - Trading (price discovery)
Privacy Mode: - Long-term holdings (privacy) - Over-the-counter transfers (confidential) - Strategic positions (no front-running)
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.
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.
balanceOf()/**
* @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)
*/
interface IDualModeToken is IERC20, 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
*/
function toPrivate(
uint256 amount,
uint8 proofType,
bytes calldata proof,
bytes calldata encryptedNote
) 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)
*/
function toPublic(
address recipient,
uint8 proofType,
bytes calldata proof,
bytes[] calldata encryptedNotes
) 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
*/
function totalSupply() external view override(IERC20, IZRC20) returns (uint256);
/**
* @notice Get total supply in privacy mode
* @dev Tracked by increments/decrements during mode conversions
* @return Total privacy supply
*/
function totalPrivacySupply() external view returns (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
*/
function isNullifierSpent(bytes32 nullifier) external view returns (bool);
}
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)
Implementations MUST implement the ERC-20 interface. All ERC-20 functions operate exclusively on transparent mode balances:
balanceOf(account) MUST return the transparent mode balance onlytransfer(to, amount) MUST transfer transparent balance onlyapprove(spender, amount) MUST approve transparent balance spendingtransferFrom(from, to, amount) MUST transfer transparent balance with allowancetotalSupply() MUST return the sum of all public balances plus totalPrivacySupply()Implementations MUST emit standard ERC-20 Transfer events for transparent mode operations.
For mode conversions:
- toPrivate(): MUST emit Transfer(account, address(0), amount)
- toPublic(): MUST emit Transfer(address(0), recipient, amount)
Implementations MUST maintain the following invariant at all times:
totalSupply() == sum(all balanceOf(account)) + totalPrivacySupply()
Where:
- totalSupply(): Inherited from ERC-20, represents total token supply across both modes
- sum(all balanceOf(account)): Sum of all transparent mode balances
- totalPrivacySupply(): Aggregate privacy mode supply, tracked by:
- 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.
BURN_ADDRESS Requirement for toPublicProblem: 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
This standard is fully backward compatible with ERC-20 and ERC-8086:
IZRC20) functions and events are supported for privacy modeERC-8085 Reference Implementation
Attack Vector: If the contract does not verify BURN_ADDRESS, an attacker can:
Mitigation: Implementations MUST ensure the converted value cannot be spent in privacy mode. For BURN_ADDRESS approach:
// Example for implementations using unspendable public key
require(isUnspendableAddress(recipientPublicKey), "toPublic: output must be unspendable");
This verification is critical—failure to prevent double-spending across modes would allow minting tokens out of thin air.
Transparent Mode: Standard ERC-20 balance checking prevents double-spending.
Privacy Mode: Nullifier uniqueness enforced on-chain:
require(!nullifiers[nullifier], "Nullifier already spent");
nullifiers[nullifier] = true;
Each commitment can only be spent once, as nullifiers are deterministically derived from commitments and private keys.
Attack: Malicious proof claiming incorrect values.
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.
Copyright and related rights waived via CC0.