ZWToken - Make privacy a native feature of all tokens on Ethereum
NOTE: PoC is available at zk.walletaa.com
This ERC defines a standard for the Zero Knowledge Token Wrapper, a wrapper that adds privacy to tokens — including ERC-20, ERC-721, ERC-1155 and ERC-6909 — while preserving all of the tokens’ original properties, such as transferability, tradability, and composability. It specifies EIP-7503-style provable burn-and-remint flows, enabling users to break on-chain traceability and making privacy a native feature of all tokens on Ethereum.
Motivation
Most existing tokens lack native privacy due to regulatory, technical, and issuer-side neglect. Users seeking privacy must rely on dedicated privacy blockchains or privacy-focused dApps, which restrict token usability, reduce composability, limit supported token types, impose whitelists, and constrain privacy schemes.
This ERC takes a different approach by introducing a zero knowledge token wrapper that preserves the underlying token’s properties while adding privacy. Its primary goals are:
- Pluggable privacy: the wrapper preserves all properties of the underlying token while adding privacy.
- Permissionless privacy: any user can wrap any token into a Zero Knowledge Wrapper Token (ZWToken).
- Broad token support: compatible with both fungible tokens (e.g., ETH, ERC-20) and non-fungible tokens (e.g., ERC-721).
- EIP-7503-style privacy: supports provable burn-and-remint flows to achieve high-level privacy.
- Compatibility with multiple EIP-7503 schemes: supports different provable burn address generation methods and commitment schemes (e.g., Ethereum-native MPT state tree or contract-managed commitments).
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Overview
A Zero Knowledge Wrapper Token (ZWToken) is a wrapper token that adds a commitment-based privacy layer to existing tokens, including ERC-20, ERC-721, and ERC-1155. This privacy layer allows private transfers without modifying the underlying token standard, while preserving full composability with existing Ethereum infrastructure.
The commitment mechanism underlying this privacy layer may be implemented using Merkle trees, cryptographic accumulators, or any other verifiable cryptographic structure.
A Zero Knowledge Wrapper Token (ZWToken) provides the following core functionalities:
The ZWToken recipient can be a provable burn address, from which the tokens can later be reminted.
- Deposit: Wraps an existing token and mints the corresponding amount of ZWToken to the specified recipient.
- Transfer: Transfers ZWToken to the specified recipient.
- Remint: Mints new ZWTokens to the specified recipient after verifying a zero-knowledge proof demonstrating ownership of previously burnt tokens, without revealing the link between them.
- Withdraw: Burns ZWTokens to redeem the equivalent amount of the underlying tokens to the specified recipient.
Privacy Features by Token Type
For fungible tokens (FTs), e.g., ERC-20:
- This ERC enables breaking the traceability of fund flows through the burn and remint processes.
- The use of provable burn addresses hides the true holder of fungible tokens until the holder performs a withdraw operation of ZWToken.
For non-fungible tokens (NFTs), e.g., ERC-721:
- This ERC cannot break the traceability of fund flows through burn and remint, since each NFT is unique and cannot participate in coin-mixing.
- However, the use of provable burn addresses can still conceal the true holder of the NFT until the holder performs a withdraw operation of ZWToken.
ZWToken-aware Workflow
In the ZWToken-aware workflow, both the user and the system explicitly recognize and interact with ZWToken. ZWToken inherits all functional properties of the underlying token.
For example, if the underlying token is ERC-20, ZWToken can be traded on DEXs, used for swaps, liquidity provision, or standard transfers. Similar to how holding WETH provides additional benefits over holding ETH directly, users may prefer to hold ZWToken rather than the underlying token.
ZWToken-unaware Workflow
This ERC also supports a ZWToken-unaware workflow. In this mode, all transfers are internally handled through ZWToken, but users remain unaware of its existence.
ZWToken functions transparently beneath the user interface, reducing the number of required contract interactions and improving overall user experience for those who prefer not to hold ZWToken directly.
Alternative Workflows
The two workflows described above represent only a subset of the interaction patterns supported by this ERC. Additional workflows are also possible, including:
-
Reminting by the recipient:
Alice maytransfer(in the ZWToken-aware workflow) ordepositTo(in the ZWToken-unaware workflow) ZWToken to Bob’s provable burn address instead of her own. In this case, the remint operation is initiated and proven by Bob rather than Alice. -
Recursive reminting:
A reminted ZWToken may also be sent to another provable burn address controlled by Bob instead of his public address, allowing the privacy state to persist across multiple remint cycles.
The interface:
interface IERC8065 {
// Optional
event CommitmentUpdated(uint256 indexed id, bytes32 indexed commitment, address indexed to, uint256 amount);
event Deposited(address indexed from, address indexed to, uint256 indexed id, uint256 amount);
event Withdrawn(address indexed from, address indexed to, uint256 indexed id, uint256 amount);
event Reminted(address indexed from, address indexed to, uint256 indexed id, uint256 amount, bool withdrawUnderlying);
function depositTo(address to, uint256 id, uint256 amount) external payable;
function withdraw(address to, uint256 id, uint256 amount) external;
function remint(
bytes calldata proof,
bytes32 commitment,
bytes32 nullifier,
address to,
uint256 id,
uint256 amount,
bool withdrawUnderlying,
uint256 relayerFee
) external;
function getLatestCommitment(uint256 id) external view returns (bytes32);
function hasCommitment(uint256 id, bytes32 commitment) external view returns (bool);
// Optional
function getCommitLeafCount(uint256 id) external view returns (uint256);
// Optional
function getCommitLeaves(uint256 id, uint256 startIndex, uint256 length)
external view returns (bytes32[] memory commitHashes, address[] memory recipients, uint256[] memory amounts);
function getFeeConfig() external view returns (uint256 depositFee, uint256 remintFee, uint256 withdrawFee, uint256 feeDenominator);
function getUnderlying() external view returns (address);
}

