---
eip: 8085
title: Dual-Mode Fungible Tokens
description: Fungible tokens supporting both transparent (ERC-20) and privacy-preserving (zk-SNARK) modes with seamless conversion
author: Rowan (@0xRowan)
discussions-to: https://ethereum-magicians.org/t/erc-8085-dual-mode-fungible-tokens/26592
status: Draft
type: Standards Track
category: ERC
created: 2025-11-15
requires: 20, 8086
---

## Abstract

This EIP defines a **permissionless** interface for fungible tokens that operate in two modes: transparent mode (fully compatible with [ERC-20](./eip-20)) and privacy mode (using [ERC-8086](./eip-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:

1. **[ERC-20](./eip-20)**: Full DeFi composability but zero privacy
2. **Pure privacy protocols**: Strong privacy but limited ecosystem integration

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.

### Current Approaches and Their Limitations

#### Wrapper-Based Privacy (e.g.,   Privacy Pools)

**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](./eip-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.)


### Our Approach: Integrated Dual-Mode for New Tokens

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

1. **Unified Token Economics**
   - No liquidity split between public/private versions
   - One token address, one market price
   - Simplified token distribution and airdrops

2. **Seamless Mode Switching**
   - Convert to privacy mode for holdings: `toPrivate()`
   - Convert to public mode for DeFi: `toPublic()`
   - Users choose privacy per transaction, not per token

3. **Full [ERC-20](./eip-20) Compatibility**
   - Works with existing wallets, DEXs, and DeFi protocols
   - No special support needed for public mode operations
   - Standard `totalSupply()` accounting tracks both modes

4. **Transparent Supply Tracking**
   - `totalSupply()` includes both public and privacy mode balances
   - `totalPrivacySupply()` reveals aggregate privacy supply (no individual balances)
   - Prevents hidden inflation
   - Regulatory visibility into aggregate metrics

5. **Permissionless Application-Layer Deployment**
   - Deploy today on any EVM chain (Ethereum, L2s, sidechains)
   - No protocol changes or governance votes required
   - No coordination with core developers needed
   - Complete freedom to implement without intermediaries or restrictions

### Honest Limitations

This standard is **not** a universal solution. Key constraints:

1. **New Tokens Only**
   - Designed for new token deployments with privacy built-in
   - Cannot add privacy to existing tokens (use wrapper-based solutions for that)

2. **Privacy-to-DeFi Requires Conversion**
   - Privacy mode balances cannot directly interact with DEXs/DeFi
   - Users must `toPublic()` before DeFi operations
   - Conversion reveals amounts on-chain (privacy-to-public events)

### Real-World Use Cases

#### DAO Governance Token

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


#### Privacy-Aware Business Token

Public Mode:
  - Investor reporting (compliance)
  - Exchange listings (liquidity)
  - Public fundraising (transparency)

Privacy Mode:
  - Employee compensation (confidential)
  - Supplier payments (competitive advantage)
  - Strategic reserves (private holdings)


#### Protocol Token with Optional Privacy

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)


### Design Philosophy

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

```solidity
/**
 * @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);
}
```

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

### [ERC-20](./eip-20) Compatibility

Implementations MUST implement the [ERC-20](./eip-20) interface. All ERC-20 functions operate exclusively on transparent mode balances:

- `balanceOf(account)` MUST return the transparent mode balance only
  - Privacy mode balances are NOT included (they are hidden by design)
- `transfer(to, amount)` MUST transfer transparent balance only
- `approve(spender, amount)` MUST approve transparent balance spending
- `transferFrom(from, to, amount)` MUST transfer transparent balance with allowance
- `totalSupply()` MUST return the sum of all public balances plus `totalPrivacySupply()`
  - This represents the total token supply across both modes

Implementations MUST emit standard [ERC-20](./eip-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)`

### Supply Invariant

Implementations MUST maintain the following invariant at all times:

```solidity
totalSupply() == sum(all balanceOf(account)) + totalPrivacySupply()
```

Where:
- `totalSupply()`: Inherited from [ERC-20](./eip-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.

## 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](./eip-20) and [ERC-8086](./eip-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
- Existing DeFi protocols work without modification
- Privacy mode is additive and optional

## Reference Implementation

[ERC-8085 Reference Implementation](../assets/eip-8085/README.md)

## Security Considerations

### Critical: toPublic Conversion Mechanism

**Attack Vector**: If the contract does not verify BURN_ADDRESS, an attacker can:

1. Hold Note A (100 privacy balance)
2. Call toPublic(50) with proof sending output to attacker's own privacy address
3. If contract skips BURN_ADDRESS check:
   - Attacker receives 50 public balance (converted from privacy mode)
   - Note B (50) sent to attacker's privacy address ← Still spendable in privacy mode!
   - Note C (50 change)
   Result: 50 + 50 + 50 = 150 (created 50 out of thin air!)

**Mitigation**: Implementations MUST ensure the converted value cannot be spent in privacy mode. For BURN_ADDRESS approach:
```solidity
// 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.

### Double-Spending Prevention

**Transparent Mode**: Standard [ERC-20](./eip-20) balance checking prevents double-spending.

**Privacy Mode**: Nullifier uniqueness enforced on-chain:
```solidity
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.

### Supply Inflation

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

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