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 Wrapped 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 Wrapped Token (ZWToken) is a wrapped token minted by a Zero Knowledge Token Wrapper. It adds a commitment-based privacy layer to existing tokens, including ERC-20, ERC-721, ERC-1155, ERC-6909. 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 Wrapped 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 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 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 may transfer (in the ZWToken-aware workflow) or deposit (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.
/// @notice Deposits a specified amount of the underlying asset and mints the corresponding amount of ZWToken to the given address.
/// @dev
/// If the underlying asset is an ERC-20/ERC-721/ERC-1155/ERC-6909 token, the caller must approve this contract to transfer the specified `amount` beforehand.
/// If the underlying asset is ETH, the caller should send the deposit value along with the transaction (`msg.value`), and `msg.value` MUST be equal to `amount`.
/// @param to The address that will receive the minted ZWTokens.
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @param amount The amount of the underlying asset to deposit.
/// @param data Additional data for extensibility, such as fee information, callback data, or metadata.
functiondeposit(addressto,uint256id,uint256amount,bytescalldatadata)externalpayable;
Depositor SHOULD be msg.sender.
The function MUST transfer the specified amount of the underlying asset from depositor to the ZWToken contract.
If the underlying asset is an ERC-20, ERC-721, ERC-1155 or ERC-6909 token, the caller MUST approve this contract to transfer the specified amount beforehand.
If the underlying asset is ETH, the caller MUST send the deposit value along with the transaction (msg.value), and msg.value MUST be equal to amount.
The function SHOULD mint ZWToken to the recipient to. The amount minted MAY be reduced by fees as defined by the implementation.
Commitment Update:
For contract-level commitment schemes, since to may be a provable burn address, the implementation SHOULD update the corresponding commitment.
However, this MAY be optimized: if to == depositor, the implementation MAY skip updating the commitment, as depositor cannot be a provable burn address (otherwise the transaction could not be initiated).
For protocol-level commitment schemes (e.g., Ethereum’s native Merkle Patricia Trie), the commitment (e.g., state root or block hash) is automatically updated by the protocol.
The function MUST emit a Deposited(depositor, to, id, amount) event upon successful deposit.
The amount parameter in the Deposited event represents the net amount of ZWToken received by to after deducting applicable fees, rather than the amount of underlying tokens deposited.
The data parameter is reserved for future extensibility and MAY be used to pass additional information such as fee configurations, callback data, or metadata. Implementations MAY ignore this parameter if not needed.
Withdraw / Unwrap
/// @notice Withdraw underlying tokens by burning ZWToken
/// @param to The recipient address that will receive the underlying token
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @param amount The amount of ZWToken to burn and redeem for the underlying token
/// @param data Additional data for extensibility, such as fee information, callback data, or metadata.
functionwithdraw(addressto,uint256id,uint256amount,bytescalldatadata)external;
Withdrawer SHOULD be msg.sender.
The function MUST burn the specified amount of ZWToken from withdrawer.
The function SHOULD transfer underlying token to the recipient to. The amount transferred MAY be reduced by fees as defined by the implementation.
The function MUST emit a Withdrawn(withdrawer, to, id, amount) event upon successful withdrawal.
The amount parameter in the Withdrawn event represents the net amount of underlying tokens received by to after deducting applicable fees, rather than the amount of ZWToken burned.
The data parameter is reserved for future extensibility and MAY be used to pass additional information such as fee configurations, callback data, or metadata. Implementations MAY ignore this parameter if not needed.
Transfer and Update Commitment
The Zero Knowledge Wrapped Token (ZWToken) remains fully compatible with the underlying token’s transfer interface, while extending it to support privacy-preserving operations.
When a ZWToken is transferred to a provable burn address, those tokens MUST be eligible for reminting through the remint interface, effectively breaking the traceability of the fund flow.
A provable burn address MAY take various forms (e.g., as defined in EIP-7503). Its essential properties are:
Such addresses MUST NOT be operable by any user and MUST be provably non-correspondent to any externally owned account (EOA) or smart contract.
Only the entity that generates the burn address MAY derive it, for example, through a signature-derived or deterministic generation scheme.
Commitment Update:
For commitment schemes maintained at the contract level:
If the recipient is identified as a provable burn address, the contract MUST update the commitment.
Example — a recipient that has previously sent any ZWToken MUST NOT be a provable burn address, since such addresses are incapable of initiating outgoing transfers. In this case, commitment update is not required.
For protocol-level commitment schemes (e.g., Ethereum’s native MPT tree), the contract does not need to manage any commitment state, since the protocol layer already provides verifiable commitment structures.
Remint
/// @notice Encapsulates all data required for remint operations
/// @param commitment The commitment (Merkle root) corresponding to the provided proof
/// @param nullifiers Array of unique nullifiers used to prevent double-remint
/// @param proverData Generic data for the prover. The meaning and encoding are implementation-specific (e.g., a circuit identifier/version).
/// @param relayerData Generic data for the relayer. The meaning and encoding are implementation-specific (e.g., fee information).
/// @param redeem If true, withdraws the equivalent underlying token instead of reminting ZWToken
/// @param proof Zero-knowledge proof bytes verifying ownership of the provable burn address
structRemintData{bytes32commitment;bytes32[]nullifiers;bytesproverData;bytesrelayerData;boolredeem;bytesproof;}/// @notice Remint ZWToken using a zero-knowledge proof to unlink the source of funds
/// @param to Recipient address that will receive the reminted ZWToken or the underlying token
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @param amount Amount of ZWToken burned from the provable burn address for reminting
/// @param data Encapsulated remint data including commitment, nullifiers, redeem flag, proof, and relayer information
functionremint(addressto,uint256id,uint256amount,RemintDatacalldatadata)external;
The function MUST verify the zero-knowledge proof proof against the provided commitment to ensure:
Ownership of the provable burn address.
Correctness parameters of remint, i.e., the zk proof public inputs.
addressto,uint256id,uint256amount,RemintDatacalldatadata// All fields except data.proof are used as public inputs for zk proof verification
Reminter SHOULD be msg.sender.
The function MUST validate the input data.commitment to ensure it exists.
The function MUST ensure that none of the data.nullifiers have been used previously. Reuse of any nullifier MUST revert the transaction.
This supports batch reminting in a single proof by allowing multiple nullifiers to be provided and consumed atomically.
Upon successful verification:
If data.redeem is false, the function MUST mint ZWToken to the recipient to. The amount minted MAY be reduced by fees as defined by the implementation.
In this case, the function MUST emit the Reminted event after a successful remint of ZWToken.
If data.redeem is true, the function MUST transfer underlying token to the recipient to. The amount transferred MAY be reduced by fees as defined by the implementation.
In this case, the function MUST emit the Reminted event after a successful withdrawal of the underlying token.
The net tokens received by the recipient to MAY be reduced by relayer fees if the relayer charges a fee (as specified in data.relayerData).
The function MUST mark each nullifier in data.nullifiers as spent to prevent double-spending.
The function MUST emit a Reminted(reminter, to, id, amount, data.redeem) event upon successful remint.
The amount parameter in the Reminted event represents the net amount of underlying tokens or ZWToken received by to after all applicable fees have been deducted.
Preview Functions (Optional)
Since the actual token amounts received may differ from the input amounts due to implementation-specific factors (e.g., fees), users need a standardized way to determine the exact amounts they will receive. Following the design pattern established by ERC-4626, the preview functions allow users to simulate the effects of their operations at the current block, returning values as close to and no more than the exact amounts that would result from the corresponding mutable operations if called in the same transaction.
/// @notice OPTIONAL: Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block.
/// @dev MUST return as close to and no more than the exact amount of ZWToken that would be minted in a `deposit` call in the same transaction.
/// @param to The address that will receive the minted ZWTokens.
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @param amount The amount of underlying tokens to deposit.
/// @param data Additional data for extensibility, such as fee information.
/// @return The amount of ZWToken that would be minted to the recipient after deducting applicable fees.
functionpreviewDeposit(addressto,uint256id,uint256amount,bytescalldatadata)externalviewreturns(uint256);
previewDeposit(address to, uint256 id, uint256 amount, bytes calldata data): OPTIONAL. Returns the exact amount of ZWToken that would be minted for the specified amount of underlying tokens.
MUST be inclusive of deposit fees. Integrators SHOULD be aware of the existence of deposit fees.
MUST NOT revert due to implementation-specific user/global limits.
MAY revert due to other conditions that would also cause deposit to revert.
/// @notice OPTIONAL: Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block.
/// @dev MUST return as close to and no more than the exact amount of underlying tokens that would be received in a `withdraw` call in the same transaction.
/// @param to The recipient address that will receive the underlying token.
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @param amount The amount of ZWToken to burn.
/// @param data Additional data for extensibility, such as fee information.
/// @return The amount of underlying tokens that would be received by the recipient after deducting applicable fees.
functionpreviewWithdraw(addressto,uint256id,uint256amount,bytescalldatadata)externalviewreturns(uint256);
previewWithdraw(address to, uint256 id, uint256 amount, bytes calldata data): OPTIONAL. Returns the exact amount of underlying tokens that would be received for burning the specified amount of ZWToken.
MUST be inclusive of withdrawal fees. Integrators SHOULD be aware of the existence of withdrawal fees.
MUST NOT revert due to implementation-specific user/global limits.
MAY revert due to other conditions that would also cause withdraw to revert.
/// @notice OPTIONAL: Allows an on-chain or off-chain user to simulate the effects of their remint at the current block.
/// @dev MUST return as close to and no more than the exact amount of ZWToken or underlying tokens that would be received in a `remint` call in the same transaction.
/// @param to Recipient address that will receive the reminted ZWToken or the underlying token.
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @param amount The amount of ZWToken burned from the provable burn address for reminting.
/// @param data Encapsulated remint data including commitment, nullifiers, redeem flag, proof, and relayer information.
/// @return The amount of ZWToken or underlying tokens that would be received by the recipient after all applicable fees have been deducted.
functionpreviewRemint(addressto,uint256id,uint256amount,RemintDatacalldatadata)externalviewreturns(uint256);
previewRemint(address to, uint256 id, uint256 amount, RemintData calldata data): OPTIONAL. Returns the exact amount of ZWToken (or underlying tokens if data.redeem is true) that would be received for a remint operation.
MUST be inclusive of all applicable fees, including remint fees and relayer fees (as specified in data.relayerData).
MUST NOT revert due to implementation-specific user/global limits.
MAY revert due to other conditions that would also cause remint to revert.
Query Interfaces
/// @notice Returns the current top-level commitment representing the privacy state
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @return The latest root hash of the commitment tree
functiongetLatestCommitment(uint256id)externalviewreturns(bytes32);/// @notice Checks if a specific top-level commitment exists
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @param commitment The root hash to verify
/// @return True if the commitment exists, false otherwise
functionhasCommitment(uint256id,bytes32commitment)externalviewreturns(bool);/// @notice OPTIONAL: Returns the total number of commitment leaves stored
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @return The total count of commitment leaves
functiongetCommitLeafCount(uint256id)externalviewreturns(uint256);/// @notice OPTIONAL: Retrieves leaf-level commit data and their hashes
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @param startIndex Index of the first leaf to fetch
/// @param length Number of leaves to fetch
/// @return commitHashes Hashes of the leaf data
/// @return recipients Recipient addresses of each leaf
/// @return amounts Token amounts of each leaf
functiongetCommitLeaves(uint256id,uint256startIndex,uint256length)externalviewreturns(bytes32[]memorycommitHashes,address[]memoryrecipients,uint256[]memoryamounts);/// @notice Returns the address of the underlying token wrapped by this ZWToken
/// @return The underlying token contract address, or address(0) if the underlying asset is ETH.
functiongetUnderlying()externalviewreturns(address);
getLatestCommitment(uint256 id): MUST return the most recent top-level commitment associated with the specified token identifier, representing the current state of the ZWToken system.
For protocol-level commitments, the block number can be used as the commitment, as it can directly map to the block hash.
hasCommitment(uint256 id, bytes32 commitment): MUST check whether a specific top-level commitment associated with the specified token identifier exists in the contract.
For proving ownership of a provable burn address, it does not require the latest commitment.
For protocol-level commitments, the block number can be used as the commitment, as it can directly map to the block hash.
getCommitLeafCount(uint256 id): OPTIONAL. Returns the total number of leaf-level commitments, which helps getCommitLeaves retrieve the leaves. The returned value also represents the current size of the privacy pool.
getCommitLeaves(uint256 id, uint256 startIndex, uint256 length): OPTIONAL. Retrieves leaf-level commitment data from the commitment tree associated with the specified token identifier.
On-chain storage of commit data can be used to improve privacy and decentralization but will incur higher gas costs.
Event-based reconstruction can be used as an alternative, though it introduces potential centralization risks.
getUnderlying(): MUST return the address of the underlying token that this ZWToken wraps.
If the underlying asset is ETH, MUST return address(0).
Implementations of this ERC MUST implement ERC-165 interface detection.
supportsInterface(bytes4) MUST return true for type(IERC8065).interfaceId (in addition to any other supported interfaces), allowing other contracts to reliably detect whether a token is a ZWToken wrapper.
Events
// Optional: Emitted when a contract-maintained commitment is updated
/// @notice OPTIONAL event emitted when a commitment is updated in the contract
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @param commitment The new top-level commitment hash
/// @param to The recipient address associated with the commitment
/// @param amount The amount related to this commitment update
eventCommitmentUpdated(uint256indexedid,bytes32indexedcommitment,addressindexedto,uint256amount);/// @notice Emitted when underlying tokens are deposited and ZWToken is minted to the recipient
/// @param from The address sending the underlying tokens
/// @param to The address receiving the minted ZWToken (after fees)
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @param amount The net amount of ZWToken minted to `to` after deducting applicable fees
eventDeposited(addressindexedfrom,addressindexedto,uint256indexedid,uint256amount);/// @notice Emitted when ZWToken is burned to redeem underlying tokens to the recipient
/// @param from The address burning the ZWToken
/// @param to The address receiving the redeemed underlying tokens (after fees)
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @param amount The net amount of underlying tokens received by `to` after deducting applicable fees
eventWithdrawn(addressindexedfrom,addressindexedto,uint256indexedid,uint256amount);/// @notice Emitted upon successful reminting of ZWToken or withdrawal of underlying tokens via a zero-knowledge proof
/// @param from The address initiating the remint operation
/// @param to The address receiving the reminted ZWToken or withdrawn underlying tokens (after fees)
/// @param id The token identifier. For fungible tokens that do not have `id`, such as ERC-20, this value MUST be set to `0`.
/// @param amount The net amount of ZWToken or underlying tokens received by `to` after all applicable fees have been deducted
/// @param redeem If true, withdraws the equivalent underlying tokens instead of reminting ZWToken
eventReminted(addressindexedfrom,addressindexedto,uint256indexedid,uint256amount,boolredeem);
CommitmentUpdated(uint256 indexed id, bytes32 indexed commitment, address indexed to, uint256 amount) is OPTIONAL and may be emitted when a contract-maintained commitment is updated, such as when ZWToken is sent to a potential provable burn address. It allows users to reconstruct commitments and generate zero-knowledge proofs, where id is the token identifier, commitment is the new commitment, to is the recipient, and amount is the committed token amount.
Deposited(address indexed from, address indexed to, uint256 indexed id, uint256 amount) signals that underlying tokens have been deposited and ZWToken minted to the recipient, where from is the sender, to is the recipient, id is the token identifier and amount is the net amount of ZWToken minted to to after deducting applicable fees.
Withdrawn(address indexed from, address indexed to, uint256 indexed id, uint256 amount) signals that ZWToken has been burned to redeem underlying tokens to the recipient, where from is the burner, to is the receiver, id is the token identifier and amount is the net amount of underlying tokens received by to after deducting applicable fees.
Reminted(address indexed from, address indexed to, uint256 indexed id, uint256 amount, bool redeem) MUST be emitted upon successful reminting of ZWToken or withdrawal of underlying tokens via a zero-knowledge proof, where from is the reminter, to is the recipient, id is the token identifier and amount is the net amount of ZWToken or underlying tokens received by to after all applicable fees have been deducted.
Rationale
Permissionless Wrapping: Launching a new zk-native token is unlikely to achieve sufficient liquidity or adoption, and major token issuers are unlikely to deploy zk variants due to regulatory and operational constraints. A permissionless wrapper makes privacy a native feature for existing tokens. ZWToken does not require issuer consent—any existing token can be wrapped, ensuring openness and equal accessibility regardless of issuer policies.
Composable Privacy: Wrapped tokens remain fully compatible with their underlying token standards, preserving interoperability across the Ethereum ecosystem. Users and dApps can treat ZWToken as the underlying token when privacy is not required, making privacy an optional and composable extension rather than a separate system.
Awareness of ZWToken: This ERC supports both ZWToken-aware and ZWToken-unaware workflows, each with distinct advantages. In the ZWToken-aware workflow, ZWToken inherits all functional properties of the underlying token and can interact seamlessly with existing DeFi protocols. In the ZWToken-unaware workflow, ZWToken operates transparently beneath the user interface, reducing the number of required contract interactions and improving user experience for those who prefer not to hold ZWToken directly.
Multiple Token Standard Support: This ERC only depends on the transferability of the underlying token, enabling broad compatibility with token standards such as ERC-20, ERC-721, ERC-1155, and ERC-6909. Non-transferable tokens (e.g., Soulbound Tokens, SBTs) are out of scope. Implementations may require extra care for:
Fee-on-transfer tokens.
Rebasing tokens (consider wrapping an existing non-rebasing wrapper to avoid rebase-handling complexity).
Fee Mechanisms: Fee structures are implementation-specific and not defined by this ERC. Implementations MAY apply fees during deposit, remint, or withdraw phases, and MAY support various fee models (e.g., fixed fees, percentage-based fees, or no fees).
Relayer-Enabled Remint: This ERC supports relayer functionality, allowing third parties to submit remint transactions on behalf of users while receiving a fee. This design mitigates privacy leakage caused by revealing the original sender’s address when paying gas fees.
Data Extensibility:
The meaning and encoding of proverData and relayerData are implementation-specific.
Implementations MAY encode prover-specific metadata in data.proverData (e.g., a circuit identifier/version, a proving key identifier, or packed auxiliary public inputs).
Implementations MAY encode relayer-specific metadata in data.relayerData (e.g., fee information, a fee token, or other fee model parameters).
A single universal encoding is impractical because proving schemes and relayer compensation models can vary significantly.
If cross-implementation interoperability is desired, a separate ERC (or profile) SHOULD standardize encoding(s) for proverData and/or relayerData.
Commitment Generalization: The ERC adopts a generic commitment abstraction, supporting various schemes such as Merkle trees or other verifiable cryptographic accumulators. This flexibility enables developers to adapt the standard to different privacy or scalability trade-offs.
Proof System Generalization: Proofs are passed as bytes calldata, allowing the use of SNARKs, STARKs, or any other zero-knowledge proof system, ensuring future-proof interoperability across cryptographic frameworks.
Provable Burn Address Generalization: This ERC does not prescribe a specific method for generating Provable Burn Addresses, as long as the following conditions are met. One example is adopting zk-friendly hash functions such as Poseidon to replace keccak256 in the address generation algorithm.
Such addresses MUST NOT be operable by any user and MUST be provably non-correspondent to any externally owned account (EOA) or smart contract.
Only the entity that generates the burn address MAY derive it, for example through a signature-derived or deterministic generation scheme.
Dual Commitment Options: The ERC supports both contract-maintained commitments and protocol-level commitments (using blockhash as the commitment).
Contract-maintained commitments reduce proof complexity, allowing smaller ZK circuits and enabling proof generation directly in browsers or mobile devices, at the cost of higher gas consumption during transfers.
Using blockhash as the commitment eliminates on-chain maintenance overhead but increases the complexity of off-chain proof generation.
Preview Functions: Following the design pattern established by ERC-4626, this ERC includes optional preview functions (previewDeposit, previewWithdraw, previewRemint) that simulate the exact outcomes of their corresponding mutable operations.
Since the actual token amounts received may differ from the input amounts due to implementation-specific factors (e.g., fees), users and integrators need a standardized way to determine the exact amounts.
These functions provide a standardized interface for querying the net token amounts, enabling accurate UX displays and informed decision-making before executing transactions.
Each preview function mirrors the parameters of its corresponding mutable operation to ensure accurate calculations that may depend on any of these parameters.
Backwards Compatibility
This ERC introduces no breaking changes. It extends the functionality of the underlying token without modifying or overriding its base interfaces.
Reference Implementation
// SPDX-License-Identifier: MIT
pragmasolidity^0.8.20;import{ERC20}from"@openzeppelin/contracts/token/ERC20/ERC20.sol";import{IERC20}from"@openzeppelin/contracts/token/ERC20/IERC20.sol";import{SafeERC20}from"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";import{ERC165}from"@openzeppelin/contracts/utils/introspection/ERC165.sol";import{IERC165}from"@openzeppelin/contracts/utils/introspection/IERC165.sol";interfaceIVerifier{functionverifyProof(bytescalldataproof,uint256[]calldatainput)externalviewreturns(bool);}contractZWTokenisIERC8065,ERC20,ERC165{usingSafeERC20forIERC20;IERC20publicimmutableunderlying;IVerifierpublicimmutableverifier;mapping(bytes32=>bool)publicusedNullifier;eventDeposited(addressindexedfrom,addressindexedto,uint256indexedid,uint256amount);eventWithdrawn(addressindexedfrom,addressindexedto,uint256indexedid,uint256amount);eventReminted(addressindexedfrom,addressindexedto,uint256indexedid,uint256amount,boolredeem);constructor(stringmemoryname_,stringmemorysymbol_,addressunderlying_,addressverifier_)ERC20(name_,symbol_){require(underlying_!=address(0),"Invalid underlying");require(verifier_!=address(0),"Invalid verifier");underlying=IERC20(underlying_);verifier=IVerifier(verifier_);}functiondeposit(addressto,uint256id,uint256amount,bytescalldata/*data*/)externalpayableoverride{require(amount>0,"amount must > 0");require(id==0,"id must be 0 for ERC20");underlying.safeTransferFrom(msg.sender,address(this),amount);_mint(to,amount);emitDeposited(msg.sender,to,id,amount);}functionwithdraw(addressto,uint256id,uint256amount,bytescalldata/*data*/)externaloverride{require(amount>0,"amount must > 0");require(id==0,"id must be 0 for ERC20");_burn(msg.sender,amount);underlying.safeTransfer(to,amount);emitWithdrawn(msg.sender,to,id,amount);}functionremint(addressto,uint256id,uint256amount,IERC8065.RemintDatacalldatadata)externaloverride{require(id==0,"id must be 0 for ERC20");// NOTE: This reference implementation uses a verifier circuit that consumes a single nullifier as a public input.
// The ERC-8065 specification allows `data.nullifiers` to contain multiple nullifiers so implementations can
// support batch reminting (consuming multiple nullifiers atomically within one proof).
require(data.nullifiers.length==1,"Only single nullifier supported");bytes32nullifier=data.nullifiers[0];require(!usedNullifier[nullifier],"nullifier used");bytes32headerHash=blockhash(uint256(data.commitment));require(headerHash!=bytes32(0),"commitment not found");// Example encoding (implementation-specific):
// if relayerData.length >= 32, first 32 bytes are interpreted as relayerFee (uint256)
uint256relayerFee=0;if(data.relayerData.length>=32){assembly{relayerFee:=calldataload(data.relayerData.offset)}}// Replay protection by chain id and contract address is handled externally––
// they MUST be included as parameters when generating the provable burn address and proof.
// (For example, the Poseidon hash that defines the burn address should incorporate these values.)
// No contract-side enforcement is implemented here.
uint256[]memoryinput=newuint256[](7);input[0]=uint256(headerHash);input[1]=uint256(nullifier);input[2]=uint256(uint160(to));input[3]=uint256(id);input[4]=uint256(amount);input[5]=uint256(data.redeem?1:0);input[6]=uint256(relayerFee);require(verifier.verifyProof(data.proof,input),"bad proof");usedNullifier[nullifier]=true;// Fee handling is implementation-specific
// This example implementation applies only relayer fees (parsed from relayerData above)
// In this example, relayerFee is interpreted as a percentage with denominator 10000
uint256remain=amount;if(relayerFee>0){uint256feeDenominator=10000;require(relayerFee<feeDenominator,"invalid relayer fee");remain=amount-amount*relayerFee/feeDenominator;require(remain>0,"invalid remain");}if(data.redeem){underlying.safeTransfer(to,remain);if(relayerFee>0){underlying.safeTransfer(msg.sender,amount-remain);}}else{_mint(to,remain);if(relayerFee>0){_mint(msg.sender,amount-remain);}}emitReminted(msg.sender,to,id,remain,data.redeem);}functiongetLatestCommitment(uint256id)externalviewoverridereturns(bytes32){require(id==0,"id must be 0 for ERC20");returnbytes32(block.number);}functionhasCommitment(uint256id,bytes32commitment)externalviewoverridereturns(bool){require(id==0,"id must be 0 for ERC20");bytes32headerHash=blockhash(uint256(commitment));returnheaderHash!=bytes32(0);}functiongetUnderlying()externalviewoverridereturns(address){returnaddress(underlying);}// Optional preview functions
functionpreviewDeposit(address/*to*/,uint256id,uint256amount,bytescalldata/*data*/)externalviewreturns(uint256){require(id==0,"id must be 0 for ERC20");// This example implementation has no deposit fees, so the output equals the input.
// Implementations with fees SHOULD deduct them here.
returnamount;}functionpreviewWithdraw(address/*to*/,uint256id,uint256amount,bytescalldata/*data*/)externalviewreturns(uint256){require(id==0,"id must be 0 for ERC20");// This example implementation has no withdrawal fees, so the output equals the input.
// Implementations with fees SHOULD deduct them here.
returnamount;}functionpreviewRemint(address/*to*/,uint256id,uint256amount,IERC8065.RemintDatacalldatadata)externalviewreturns(uint256){require(id==0,"id must be 0 for ERC20");// Example encoding (implementation-specific):
// Parse relayerFee from relayerData (if provided)
uint256relayerFee=0;if(data.relayerData.length>=32){relayerFee=abi.decode(data.relayerData[:32],(uint256));}// Apply relayer fee (percentage with denominator 10000)
uint256remain=amount;if(relayerFee>0){uint256feeDenominator=10000;require(relayerFee<feeDenominator,"invalid relayer fee");remain=amount-amount*relayerFee/feeDenominator;}returnremain;}functionsupportsInterface(bytes4interfaceId)publicviewvirtualoverride(ERC165,IERC165)returns(bool){returninterfaceId==type(IERC8065).interfaceId||super.supportsInterface(interfaceId);}}
Security Considerations
Double-Remint Prevention: Each remint operation MUST include one or more unique nullifiers (data.nullifiers) to prevent double-remint attacks.
Reusing any nullifier MUST revert.
On success, all provided nullifiers MUST be marked as spent.
Over-Minting Protection: The total supply of ZWToken MAY temporarily exceed the supply of the underlying token. However, the surplus represents provably burnt tokens, which are permanently removed from circulation and cannot be redeemed.
Local Proof Generation: Circuits SHOULD remain as small and efficient as possible to enable users to generate zero-knowledge proofs locally (e.g., within browsers or mobile devices). This minimizes reliance on third-party provers that may introduce privacy leakage risks.
Provable Burn Address Security: Provable burn addresses MAY follow different generation schemes (e.g., as proposed in EIP-7503). The essential properties are:
These addresses MUST be non-operable and provably non-correspondent to any externally owned account (EOA) or smart contract.
Only the generator of the burn address MAY deterministically derive it, for instance, through a signature-derived scheme.
Implementations SHOULD prefer zk-friendly hash functions (e.g., Poseidon) in place of keccak256 to improve proof efficiency and reduce circuit size.
Burn and Remint Process Privacy:
Burn amounts SHOULD appear indistinguishable from ordinary transfers to prevent correlation with remint amounts.
Burn and remint events SHOULD be separated in time to reduce linkability.
Each burn operation MUST use a unique Provable Burn Address that can be used only once.
Fully On-Chain Operation: The protocol operates entirely on-chain and requires no trusted backend. It can be directly integrated into wallets or dApps without introducing custodial or centralized dependencies.
Commit Data Privacy: When generating proofs, users SHOULD retrieve as much commitment data as possible to maximize anonymity. Relying on selective or limited data sources may allow inference of the specific commit path being proven, weakening privacy guarantees.