1. Problem Statement
1.1 Background
EIP-7503 (Zero-Knowledge Wormholes) proposes an on-chain privacy design built around burn addresses. ERC-8065 (Zero Knowledge Token Wrapper) defines a wrapper that adds EIP-7503-style privacy to existing tokens. ZWToken is a full implementation of ERC-8065, supporting ERC-20, native ETH, ERC-721, and ERC-1155.
As an ERC-8065 implementation, ZWToken inherits the core security issue raised but not resolved in EIP-7503—the 160-bit birthday attack. This document states that problem and ZWToken’s mitigation.
1.2 Burn address privacy flow
The core flow of EIP-7503 is:
- The user derives a burn address (privacy address) from a
secret - Tokens are sent to that address (deposit into a commitment)
- Via a ZK proof that they know
secret, the user remints an equal amount of tokens at another address
Privacy in this flow relies on: no one can link the burn address to the recipient address.
1.3 Attack vectors
Ethereum addresses are 160 bits. The burn address can be derived as:
addrScalar = Poseidon(8065, tokenId, secret) // ~254 bits
addr20 = addrScalar mod 2^160 // truncated to 160 bits
A birthday attack can find a collision in 2^80 operations. The attacker searches two spaces in parallel; by the birthday paradox, ~2^80 trials suffice to find a matching pair:
Vector 1: Double-mint (two secrets collide to the same address)
Poseidon(8065, 0, secret_A) mod 2^160 == Poseidon(8065, 0, secret_B) mod 2^160
The attacker uses secret_A and secret_B to produce different nullifiers and remints twice against the same commitment.
Vector 2: CREATE2 collision
Poseidon(8065, 0, secret) mod 2^160 == CREATE2(deployer, salt, codehash)
The attacker uses CREATE2 to control the burn address: they can drain tokens directly and also remint via ZK proof.
Vector 3: EOA private-key collision
Poseidon(8065, 0, secret) mod 2^160 == keccak256(publicKey)[12:32]
The attacker searches over both secret and ECDSA keys; on collision they hold the private key for the burn address and can sign transfers out.
1.4 Severity
| Metric | Value |
|---|---|
| Attack complexity | 2^80 hash operations |
| 128-bit security | 2^128 operations |
| Gap | Does not meet 128-bit security |
| Rough cost estimate | Tens of billions of USD in hardware (today) |
| Impact | Protocol inflation; pool may be drained |
Though economically infeasible today, risk grows with hardware and ASIC optimization.
2. Prior Art
2.1 EIP-7503 Proof-of-Work
Add a PoW constraint in the ZK circuit:
Poseidon(MAGIC_POW, secret) mod 2^POW_BITS == 0
secret must satisfy the PoW condition, raising the cost of finding valid secrets.
Pros:
- No change to the user-facing flow
- Fits naturally into the ZK circuit
Cons:
- Only improves the constant factor, not asymptotic complexity—the attacker pays a
2^POW_BITSmultiplier - Proof generation time increases significantly (users run PoW client-side)
- Worse experience on mobile
POW_BITSis hard to tune: too low is ineffective, too high is unacceptable for users
2.2 Deposit Cap
Cap the maximum amount per deposit/transfer/remint.
Pros:
- Simple to implement
- Caps per-attack profit below the cost of a 2^80 birthday attack
Cons:
- Breaks token compatibility and composability—deposit, transfer, etc. cannot exceed the cap, so ZWToken (e.g. Wrapped ERC-20 with privacy) cannot behave as a standard ERC-20 with DeFi (Uniswap, Aave, etc.)
- Constrains legitimate large transfers
3. ZWToken Solution: Dual-Mode Remint
3.1 Core Idea
Set an anonymousCap below the economic cost of a 2^80 birthday attack:
- Amount ≤ anonymousCap: fully anonymous mode; behavior unchanged from before
- Amount > anonymousCap: reveal mode; the user must publish the burn address; the contract burns from that address then mints
In the worst case, after ~2^80 work the attacker can only steal up to anonymousCap. If anonymousCap is set below the estimated cost of a 2^80 attack (tens of billions of USD today), there is no rational economic motive. Large transfers are forced through burn-from-address, so inflation from this vector is impossible.
Unlike a global deposit cap, the limit applies only at remint; standard ERC-20 deposit, transfer, and withdraw are unrestricted, preserving full compatibility and DeFi composability. The UI caps transfers to burn addresses at anonymousCap; transfers between ordinary addresses are unlimited.
3.2 Circuit Design
Add one public input revealedAddr and one quadratic constraint:
signal input revealedAddr; // 0 = anonymous, addr20 = revealed
// If revealedAddr != 0, it must equal addr20
revealedAddr * (revealedAddr - addr20) === 0;
revealedAddr = 0: anonymous mode; constraint holds triviallyrevealedAddr = addr20: reveal mode; provesrevealedAddrmatches the in-circuitaddr20revealedAddr = anything else: constraint fails; proof cannot be produced
Cost: one extra constraint (13084 → 13085); negligible impact on proving time.
3.3 Contract Design
// BaseZWToken.sol
uint256 public immutable anonymousCap; // 0 = no cap
function _requireRevealIfNeeded(uint256 amount, address revealedAddr) internal view {
// Amount above cap requires reveal
if (anonymousCap > 0 && amount > anonymousCap && revealedAddr == address(0)) {
revert RevealRequired();
}
// Revealed address with code = CREATE2 collision attack
if (revealedAddr != address(0) && revealedAddr.code.length != 0) {
revert BurnAddressHasCode();
}
}
Reveal-mode remint flow (ZWERC20 example):
// 1. Burn from burn address first
if (revealedAddr != address(0)) {
_burn(revealedAddr, amount);
}
// 2. Then normal remint (mint to recipient)
_executeRemint(to, id, amount, redeem, relayerFee);
Effect: burn amount → mint amount (split across recipient + relayer + protocol); net totalSupply change is 0.
3.4 EXTCODESIZE Guard
A valid burn address is a random address from Poseidon; it is always an EOA (no bytecode). If a revealedAddr has code, someone deployed a contract there via CREATE2 collision—direct evidence of attack.
if (revealedAddr.code.length != 0) revert BurnAddressHasCode();
Cost: EXTCODESIZE opcode, ~100 gas.