Abstract
This ERC extends the zero-knowledge proof-of-burn mechanism from EIP-7503 to ERC-20 tokens. Users burn tokens by transferring them to cryptographically unspendable addresses derived from secret preimages. Using Merkle Patricia Trie (MPT) storage proofs and zero-knowledge circuits, users can later prove the burn occurred without revealing the original transaction, enabling private re-minting of equivalent tokens.
Motivation
EIP-7503 introduced Zero-Knowledge Wormholes for native ETH, enabling privacy-preserving burns and re-mints. However, the majority of value transferred on Ethereum exists in ERC-20 tokens. Extending this mechanism to ERC-20 tokens requires additional considerations:
-
Storage vs Account State: ETH balances are stored in account state, while ERC-20 balances are stored in contract storage, requiring a two-level proof structure.
-
Variable Storage Layouts: Different ERC-20 implementations store balances at different storage slots, requiring slot discovery mechanisms.
-
No Contract Interaction for Burns: Like EIP-7503, burns should require only a standard
transfer()call to an unspendable address, avoiding mixer contract interactions that enable censorship.
Current privacy solutions for ERC-20 tokens have significant drawbacks:
- Mixer Contracts: Require direct contract interaction, creating traceable deposit/withdrawal patterns
- Wrapped Privacy Tokens: Add trust assumptions and complexity
- Centralized Bridges: Require trusting bridge operators for cross-chain privacy
This EIP enables permissionless, trustless privacy for any ERC-20 token without modifying the token contract.
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
- Burn Address: An Ethereum address with no known private key, generated via
hash_to_curve(keccak256(secret)) - Secret: A random 256-bit value known only to the burner
- Nullifier: A unique identifier derived from the secret that prevents double-claiming:
keccak256(secret || tokenAddress) - Storage Slot: The position in contract storage where a balance is stored
- Mapping Slot: The base storage slot of the
balancesmapping in an ERC-20 contract
Burn Address Generation
A burn address MUST be generated as follows:
commitment = keccak256(secret)
burn_address = hash_to_curve(commitment)Where:
secretis a cryptographically random 256-bit valuehash_to_curvemaps the commitment to a point on the secp256k1 curve with no known discrete logarithmburn_addressis the Ethereum address derived from this curve point
The burn address is unspendable because finding a private key such that is computationally infeasible.
Storage Slot Calculation
For an ERC-20 token with mapping(address => uint256) balances at storage slot , the storage slot for address is:
storage_slot = keccak256(abi.encode(A, S))This follows Solidity's storage layout rules for mappings.
Proof Structure
A complete ERC-20 burn proof consists of:
Block Reference
| Field | Type | Description |
|---|---|---|
blockNumber | uint256 | Block number containing the burn |
blockHash | bytes32 | Hash of the block |
stateRoot | bytes32 | State root from block header |
Token Reference
| Field | Type | Description |
|---|---|---|
tokenAddress | address | ERC-20 token contract address |
mappingSlot | uint256 | Storage slot of balances mapping |
Burn Data
| Field | Type | Description |
|---|---|---|
amount | uint256 | Amount of tokens burned |
nullifier | bytes32 | keccak256(secret || tokenAddress) |
Proofs
| Field | Type | Description |
|---|---|---|
accountProof | bytes[] | MPT proof from stateRoot to token contract |
storageProof | bytes[] | MPT proof from storageRoot to balance slot |
zkProof | bytes | Zero-knowledge proof |
MPT Proof Verification
Account Proof
The account proof verifies the token contract's state against the block's stateRoot:
- Compute the account key:
accountKey = keccak256(tokenAddress) - Verify the MPT proof traversal from
stateRootusingaccountKey - RLP-decode the leaf value to obtain:
[nonce, balance, storageRoot, codeHash] - Extract
storageRootfor storage proof verification
Storage Proof
The storage proof verifies the balance at the burn address:
- Compute storage slot:
slot = keccak256(abi.encode(burnAddress, mappingSlot)) - Compute storage key:
storageKey = keccak256(slot) - Verify the MPT proof traversal from
storageRootusingstorageKey - RLP-decode the leaf value to obtain the balance
Zero-Knowledge Circuit
The ZK circuit MUST prove the following statements without revealing secret or burnAddress:
Public Inputs
stateRoot: State root from block headerstorageRoot: Storage root of token contract (from account proof)tokenAddress: ERC-20 token contract addressmappingSlot: Storage slot of balances mappingamount: Claimed burn amountnullifier: Nullifier value
Private Inputs
secret: The secret preimageburnAddress: The derived burn addressstorageProof: MPT proof nodes
Constraints
-
Burn Address Derivation:
commitment = keccak256(secret) burnAddress = hash_to_curve(commitment) -
Nullifier Derivation:
nullifier == keccak256(secret || tokenAddress) -
Storage Slot Calculation:
storageSlot = keccak256(abi.encode(burnAddress, mappingSlot)) -
MPT Proof Verification:
verifyMPTProof(storageRoot, keccak256(storageSlot), storageProof) == rlpEncode(amount)
Verification Process
A verifier contract MUST perform the following checks:
-
Block Finality: Verify
blockNumberis finalized (at least 2 epochs on post-merge Ethereum) -
Block Hash Validity: Verify
blockHashis the canonical hash forblockNumber -
State Root Extraction: Verify
stateRootmatches the state root in the block header -
Account Proof: Verify
accountProofagainststateRootand extractstorageRoot -
ZK Proof: Verify
zkProofwith public inputs(stateRoot, storageRoot, tokenAddress, mappingSlot, amount, nullifier) -
Nullifier Uniqueness: Verify
nullifierhas not been previously used -
Mark Nullifier: Record
nullifieras used -
Mint/Release: Mint or release
amounttokens to the recipient
Mapping Slot Registry
Different ERC-20 tokens store balances at different slots. Common patterns:
| Pattern | Mapping Slot | Examples |
|---|---|---|
| OpenZeppelin ERC-20 | 0 | Most tokens |
| OpenZeppelin ERC-20 (older) | 2 | Legacy tokens |
| USDC (FiatTokenV2) | 9 | USDC |
| Custom implementations | Variable | Various |
Implementations SHOULD provide a registry or discovery mechanism for mapping slots.
Rationale
Two-Level Proof Structure
Unlike ETH balances stored in account state, ERC-20 balances exist in contract storage. This requires:
- An account proof to verify the token contract and obtain its
storageRoot - A storage proof to verify the balance at the burn address
This adds ~1M constraints to the ZK circuit but maintains the same privacy guarantees as EIP-7503.
Storage Proofs vs Event Logs
Alternative approaches using Transfer event logs were considered but rejected:
- Event logs reveal the sender address, leaking privacy
- Receipt proofs are larger than storage proofs
- Events don't cryptographically bind to unspendable addresses
Storage proofs reveal only that some address holds tokens, not the transaction history.
Nullifier Construction
The nullifier keccak256(secret || tokenAddress) includes the token address to:
- Prevent cross-token nullifier collisions
- Allow the same secret to be used for different token burns (not recommended but safe)
- Bind the nullifier to a specific token for easier verification
Circuit Complexity
The ZK circuit requires approximately:
| Component | Constraints |
|---|---|
| Keccak256 (5 instances) | ~3.75M |
| Secp256k1 hash-to-curve | ~500k |
| MPT verification | ~200k |
| Total | ~4.5M |
This is feasible with current proving systems:
- Groth16: ~45 seconds proving time
- PLONK: ~60 seconds proving time
- Hardware acceleration can reduce this significantly
Backwards Compatibility
This EIP introduces no changes to existing ERC-20 tokens or the Ethereum protocol. It defines an external verification layer that works with any standard ERC-20 token.
Tokens using non-standard storage layouts (e.g., rebasing tokens, fee-on-transfer tokens) may require custom handling or may be incompatible.
Reference Implementation
A reference implementation in Python for proof generation is available at a repository in jakeolo's GitHub account. check ethereum-magicians discussions for the link. The implementation includes:
- MPT proof generation using
eth_getProofRPC - MPT proof verification
- Storage slot calculation for ERC-20 balances
- Mapping slot discovery for various tokens
Security Considerations
Double Claiming Prevention
The nullifier keccak256(secret || tokenAddress) is deterministic from the secret. Once a nullifier is recorded as used, any attempt to claim with the same nullifier MUST be rejected. Implementations MUST use persistent storage for nullifier tracking.
Block Reorganization Attacks
Proofs generated for non-finalized blocks may become invalid after a reorganization. Verifiers MUST only accept proofs for finalized blocks. On post-merge Ethereum, finality is achieved after 2 epochs (~12.8 minutes).
Mapping Slot Manipulation
An attacker could attempt to submit proofs with incorrect mappingSlot values to claim from unrelated storage slots. The ZK circuit MUST verify the storage slot calculation matches the public mappingSlot input.
Front-Running
Claim transactions reveal the nullifier in the mempool. However, front-running is not profitable because:
- The ZK proof binds the claim to specific parameters
- Changing the recipient requires a different valid ZK proof
- The attacker cannot generate a valid proof without the secret
Token Contract Upgrades
For upgradeable proxy contracts, the storage layout may change between upgrades. Users SHOULD claim burned tokens promptly. Historical proofs remain valid for the state at the proven block number.
Anonymity Set Considerations
The anonymity set is all burn addresses that have received the same token. Unlike mixer contracts with fixed denominations, this EIP allows arbitrary amounts, which may reduce anonymity. Users concerned about amount-based correlation SHOULD use common round amounts.
Interaction with EIP-7503
This EIP is designed to complement EIP-7503. The same secret and burn address pattern can be used for both ETH and ERC-20 burns, though separate nullifiers are generated due to the token address inclusion.
Copyright
Copyright and related rights waived via CC0.
