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.
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:
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.
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.
For fungible tokens (FTs), e.g., ERC-20:
For non-fungible tokens (NFTs), e.g., ERC-721:
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.
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.
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.
The interface:
interface IERC8065 is IERC165 {
struct RemintData {
bytes32 commitment;
bytes32[] nullifiers;
bytes proverData;
bytes relayerData;
bool redeem;
bytes proof;
}
// 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 redeem);
function deposit(address to, uint256 id, uint256 amount, bytes calldata data) external payable;
function withdraw(address to, uint256 id, uint256 amount, bytes calldata data) external;
function remint(
address to,
uint256 id,
uint256 amount,
RemintData calldata data
) external;
// Optional
function previewDeposit(address to, uint256 id, uint256 amount, bytes calldata data) external view returns (uint256);
// Optional
function previewWithdraw(address to, uint256 id, uint256 amount, bytes calldata data) external view returns (uint256);
// Optional
function previewRemint(address to, uint256 id, uint256 amount, RemintData calldata data) external view returns (uint256);
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 getUnderlying() external view returns (address);
}
/// @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.
function deposit(address to, uint256 id, uint256 amount, bytes calldata data) external payable;
msg.sender.amount of the underlying asset from depositor to the ZWToken contract.amount beforehand.msg.value), and msg.value MUST be equal to amount.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.
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.
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.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./// @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.
function withdraw(address to, uint256 id, uint256 amount, bytes calldata data) external;
msg.sender.amount of ZWToken from withdrawer.to. The amount transferred MAY be reduced by fees as defined by the implementation.Withdrawn(withdrawer, to, id, amount) event upon successful withdrawal.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.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./// @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
struct RemintData {
bytes32 commitment;
bytes32[] nullifiers;
bytes proverData;
bytes relayerData;
bool redeem;
bytes proof;
}
/// @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
function remint(
address to,
uint256 id,
uint256 amount,
RemintData calldata data
) 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.
solidity
address to,
uint256 id,
uint256 amount,
RemintData calldata data // All fields except data.proof are used as public inputs for zk proof verification
Reminter SHOULD be msg.sender.
data.commitment to ensure it exists.data.nullifiers have been used previously. Reuse of any nullifier MUST revert the transaction.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.
Reminted event after a successful remint of ZWToken.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.Reminted event after a successful withdrawal of the underlying token.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.
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.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.
function previewDeposit(address to, uint256 id, uint256 amount, bytes calldata data) external view returns (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.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.
function previewWithdraw(address to, uint256 id, uint256 amount, bytes calldata data) external view returns (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.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.
function previewRemint(address to, uint256 id, uint256 amount, RemintData calldata data) external view returns (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.data.relayerData).remint to revert./// @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
function getLatestCommitment(uint256 id) external view returns (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
function hasCommitment(uint256 id, bytes32 commitment) external view returns (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
function getCommitLeafCount(uint256 id) external view returns (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
function getCommitLeaves(uint256 id, uint256 startIndex, uint256 length)
external view returns (bytes32[] memory commitHashes, address[] memory recipients, uint256[] memory amounts);
/// @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.
function getUnderlying() external view returns (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.hasCommitment(uint256 id, bytes32 commitment): MUST check whether a specific top-level commitment associated with the specified token identifier exists in the contract.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.
address(0).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.// 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
event CommitmentUpdated(uint256 indexed id, bytes32 indexed commitment, address indexed to, uint256 amount);
/// @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
event Deposited(address indexed from, address indexed to, uint256 indexed id, uint256 amount);
/// @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
event Withdrawn(address indexed from, address indexed to, uint256 indexed id, uint256 amount);
/// @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
event Reminted(address indexed from, address indexed to, uint256 indexed id, uint256 amount, bool redeem);
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.
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.
data.proverData (e.g., a circuit identifier/version, a proving key identifier, or packed auxiliary public inputs).data.relayerData (e.g., fee information, a fee token, or other fee model parameters).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.
previewDeposit, previewWithdraw, previewRemint) that simulate the exact outcomes of their corresponding mutable operations.This ERC introduces no breaking changes. It extends the functionality of the underlying token without modifying or overriding its base interfaces.
// SPDX-License-Identifier: MIT
pragma solidity ^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";
interface IVerifier {
function verifyProof(
bytes calldata proof,
uint256[] calldata input
) external view returns (bool);
}
contract ZWToken is IERC8065, ERC20, ERC165 {
using SafeERC20 for IERC20;
IERC20 public immutable underlying;
IVerifier public immutable verifier;
mapping(bytes32 => bool) public usedNullifier;
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 redeem);
constructor(
string memory name_,
string memory symbol_,
address underlying_,
address verifier_
) ERC20(name_, symbol_) {
require(underlying_ != address(0), "Invalid underlying");
require(verifier_ != address(0), "Invalid verifier");
underlying = IERC20(underlying_);
verifier = IVerifier(verifier_);
}
function deposit(address to, uint256 id, uint256 amount, bytes calldata /*data*/) external payable override {
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);
emit Deposited(msg.sender, to, id, amount);
}
function withdraw(address to, uint256 id, uint256 amount, bytes calldata /*data*/) external override {
require(amount > 0, "amount must > 0");
require(id == 0, "id must be 0 for ERC20");
_burn(msg.sender, amount);
underlying.safeTransfer(to, amount);
emit Withdrawn(msg.sender, to, id, amount);
}
function remint(
address to,
uint256 id,
uint256 amount,
IERC8065.RemintData calldata data
) external override {
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");
bytes32 nullifier = data.nullifiers[0];
require(!usedNullifier[nullifier], "nullifier used");
bytes32 headerHash = 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)
uint256 relayerFee = 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[] memory input = new uint256[](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
uint256 remain = amount;
if (relayerFee > 0) {
uint256 feeDenominator = 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);
}
}
emit Reminted(msg.sender, to, id, remain, data.redeem);
}
function getLatestCommitment(uint256 id) external view override returns (bytes32) {
require(id == 0, "id must be 0 for ERC20");
return bytes32(block.number);
}
function hasCommitment(uint256 id, bytes32 commitment) external view override returns (bool) {
require(id == 0, "id must be 0 for ERC20");
bytes32 headerHash = blockhash(uint256(commitment));
return headerHash != bytes32(0);
}
function getUnderlying() external view override returns (address) {
return address(underlying);
}
// Optional preview functions
function previewDeposit(address /*to*/, uint256 id, uint256 amount, bytes calldata /*data*/) external view returns (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.
return amount;
}
function previewWithdraw(address /*to*/, uint256 id, uint256 amount, bytes calldata /*data*/) external view returns (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.
return amount;
}
function previewRemint(address /*to*/, uint256 id, uint256 amount, IERC8065.RemintData calldata data) external view returns (uint256) {
require(id == 0, "id must be 0 for ERC20");
// Example encoding (implementation-specific):
// Parse relayerFee from relayerData (if provided)
uint256 relayerFee = 0;
if (data.relayerData.length >= 32) {
relayerFee = abi.decode(data.relayerData[:32], (uint256));
}
// Apply relayer fee (percentage with denominator 10000)
uint256 remain = amount;
if (relayerFee > 0) {
uint256 feeDenominator = 10000;
require(relayerFee < feeDenominator, "invalid relayer fee");
remain = amount - amount * relayerFee / feeDenominator;
}
return remain;
}
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(ERC165, IERC165)
returns (bool)
{
return interfaceId == type(IERC8065).interfaceId || super.supportsInterface(interfaceId);
}
}
data.nullifiers) to prevent double-remint attacks.Burn and Remint Process Privacy:
Burn amounts SHOULD appear indistinguishable from ordinary transfers to prevent correlation with remint amounts.
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.
Copyright and related rights waived via CC0.