Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 305 additions & 0 deletions ERCS/erc-8093.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
---
eip: 8093
title: Private ERC-20
description: Private burn and re-mint of ERC-20 tokens using storage proofs and zero-knowledge circuits
author: Jake (@jakeolo) <[email protected]>
discussions-to: https://ethereum-magicians.org/t/erc-8093-ppob-on-erc20-contract-storage/26763
status: Draft
type: Standards Track
category: ERC
created: 2025-11-27
requires: 20, 7503
---

## Abstract

This ERC extends the zero-knowledge proof-of-burn mechanism from [EIP-7503](../assets/erc-8093/eip-7503.md) to [ERC-20](./eip-20.md) 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](../assets/erc-8093/eip-7503.md) 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](./eip-20.md) tokens. Extending this mechanism to [ERC-20](./eip-20.md) tokens requires additional considerations:

1. **Storage vs Account State**: ETH balances are stored in account state, while [ERC-20](./eip-20.md) balances are stored in contract storage, requiring a two-level proof structure.

2. **Variable Storage Layouts**: Different [ERC-20](./eip-20.md) implementations store balances at different storage slots, requiring slot discovery mechanisms.

3. **No Contract Interaction for Burns**: Like [EIP-7503](../assets/erc-8093/eip-7503.md), 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](./eip-20.md) 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](./eip-20.md) 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 `balances` mapping in an [ERC-20](./eip-20.md) contract

### Burn Address Generation

A burn address MUST be generated as follows:

```
commitment = keccak256(secret)
burn_address = hash_to_curve(commitment)
```

Where:

- `secret` is a cryptographically random 256-bit value
- `hash_to_curve` maps the commitment to a point on the secp256k1 curve with no known discrete logarithm
- `burn_address` is the Ethereum address derived from this curve point

The burn address is unspendable because finding a private key $k$ such that $k \times G = \text{hash\_to\_curve}(h)$ is computationally infeasible.

### Storage Slot Calculation

For an [ERC-20](./eip-20.md) token with `mapping(address => uint256) balances` at storage slot $S$, the storage slot for address $A$ is:

```
storage_slot = keccak256(abi.encode(A, S))
```

This follows Solidity's storage layout rules for mappings.

### Proof Structure

A complete [ERC-20](./eip-20.md) 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](./eip-20.md) 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`:

1. Compute the account key: `accountKey = keccak256(tokenAddress)`
2. Verify the MPT proof traversal from `stateRoot` using `accountKey`
3. RLP-decode the leaf value to obtain: `[nonce, balance, storageRoot, codeHash]`
4. Extract `storageRoot` for storage proof verification

#### Storage Proof

The storage proof verifies the balance at the burn address:

1. Compute storage slot: `slot = keccak256(abi.encode(burnAddress, mappingSlot))`
2. Compute storage key: `storageKey = keccak256(slot)`
3. Verify the MPT proof traversal from `storageRoot` using `storageKey`
4. 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 header
- `storageRoot`: Storage root of token contract (from account proof)
- `tokenAddress`: [ERC-20](./eip-20.md) token contract address
- `mappingSlot`: Storage slot of balances mapping
- `amount`: Claimed burn amount
- `nullifier`: Nullifier value

#### Private Inputs

- `secret`: The secret preimage
- `burnAddress`: The derived burn address
- `storageProof`: MPT proof nodes

#### Constraints

1. **Burn Address Derivation**:

```
commitment = keccak256(secret)
burnAddress = hash_to_curve(commitment)
```

2. **Nullifier Derivation**:

```
nullifier == keccak256(secret || tokenAddress)
```

3. **Storage Slot Calculation**:

```
storageSlot = keccak256(abi.encode(burnAddress, mappingSlot))
```

4. **MPT Proof Verification**:

```
verifyMPTProof(storageRoot, keccak256(storageSlot), storageProof) == rlpEncode(amount)
```

### Verification Process

A verifier contract MUST perform the following checks:

1. **Block Finality**: Verify `blockNumber` is finalized (at least 2 epochs on post-merge Ethereum)

2. **Block Hash Validity**: Verify `blockHash` is the canonical hash for `blockNumber`

3. **State Root Extraction**: Verify `stateRoot` matches the state root in the block header

4. **Account Proof**: Verify `accountProof` against `stateRoot` and extract `storageRoot`

5. **ZK Proof**: Verify `zkProof` with public inputs `(stateRoot, storageRoot, tokenAddress, mappingSlot, amount, nullifier)`

6. **Nullifier Uniqueness**: Verify `nullifier` has not been previously used

7. **Mark Nullifier**: Record `nullifier` as used

8. **Mint/Release**: Mint or release `amount` tokens to the recipient

### Mapping Slot Registry

Different [ERC-20](./eip-20.md) tokens store balances at different slots. Common patterns:

| Pattern | Mapping Slot | Examples |
|---------|--------------|----------|
| OpenZeppelin [ERC-20](./eip-20.md) | 0 | Most tokens |
| OpenZeppelin [ERC-20](./eip-20.md) (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](./eip-20.md) balances exist in contract storage. This requires:

1. An account proof to verify the token contract and obtain its `storageRoot`
2. 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](../assets/erc-8093/eip-7503.md).

### 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](./eip-20.md) tokens or the Ethereum protocol. It defines an external verification layer that works with any standard [ERC-20](./eip-20.md) 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_getProof` RPC
- MPT proof verification
- Storage slot calculation for [ERC-20](./eip-20.md) 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](../assets/erc-8093/eip-7503.md)

This EIP is designed to complement [EIP-7503](../assets/erc-8093/eip-7503.md). The same secret and burn address pattern can be used for both ETH and [ERC-20](./eip-20.md) burns, though separate nullifiers are generated due to the token address inclusion.

## Copyright

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