This proposal extends ERC-20 tokens with oracle-permissioned transfers validated by zero-knowledge proofs. Token transfers are only valid when an external “Transfer Oracle” pre-approves them using off-chain payment instructions in a standardized JSON format, proven on-chain via ZK proofs.
The standard defines:
ITransferOracle – a minimal interface that any ERC-20-compatible contract can consult to decide whether transfers should succeed
approveTransfer flow – whereby an issuer deposits a one-time approval in the oracle with a ZK-proof attesting that the approval matches a canonicalized payment instruction message
canTransfer query – whereby the token contract atomically consumes an approval when the holder initiates the transfer
Generic data structures, events, and hooks that allow alternative permissioning logics (KYC lists, travel-rule attestations, CBDC quotas) to share the same plumbing
The scheme is issuer-agnostic, proof-system-agnostic, and network-agnostic (L1/L2). The payment instruction format is compatible with ISO 20022 pain.001 for interoperability with existing financial systems, but does not require implementers to access proprietary ISO specifications.
Motivation
Institutional tokenisation requires both ERC-20 fungibility and legally enforceable control over who may send value to whom and why.
Hard-coding rules in every token contract is brittle and non-standard. Moving rules into a dedicated oracle contract and proving off-chain documentation on-chain gives:
Compliance traceability – every transfer links to a signed payment
order recognised by traditional finance systems.
Issuer flexibility – any institution can swap out its oracle logic
without breaking ERC-20 compatibility.
Composability – DeFi protocols can interact with permissioned tokens
using familiar ERC-20 flows, while downstream permission checks are
encapsulated in the oracle.
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.
Interfaces
/// @notice One-time ZK-backed approval for a single transfer.
structTransferApproval{addresssender;addressrecipient;uint256minAmt;// Minimum allowed transfer amount (inclusive)
uint256maxAmt;// Maximum allowed transfer amount (inclusive)
uint256expiry;// UNIX seconds; 0 == never expires
bytes32proofId;// keccak256(root‖debtorHash‖creditorHash)
}/// @title External oracle consulted by permissioned tokens.
interfaceITransferOracle{/// @dev Verifies zk-proof and stores a one-time approval.
/// @return proofId – unique handle for off-chain reconciliation
functionapproveTransfer(TransferApprovalcalldataapproval,bytescalldataproof,// ZK proof bytes (system-specific)
bytescalldatapublicInputs// ABI-encoded public outputs
)externalreturns(bytes32proofId);/// @dev Atomically consumes an approval that covers `amount`.
/// MUST revert if no such approval exists.
functioncanTransfer(addresstoken,addresssender,addressrecipient,uint256amount)externalreturns(bytes32proofId);}
Transfer Hook
A Permissioned ERC-20MUST consult the oracle on every value-moving
transfer (every transfer and transferFrom) before the balances of from
and to are updated, and MUST revert the transfer unless the oracle
returns a valid proofId. Mints and burns (transfers from or to the zero
address) MAY bypass this check.
For a transfer of amount from from to to, the token MUST perform the
equivalent of:
bytes32proofId=ORACLE.canTransfer(address(this),from,to,amount);// canTransfer MUST revert if no valid approval exists
// ... update balances ...
emitTransferValidated(proofId);
ORACLE is the address of the ITransferOracle instance consulted by the
token, typically fixed at construction. This requirement applies to all
ERC-20 implementations: implementations based on OpenZeppelin’s ERC-20 MAY
satisfy it by overriding the internal
_update(address from, address to, uint256 amount) function, while other
implementations MUST enforce the equivalent check within their own
transfer and transferFrom logic.
The oracle MUST revert canTransfer unless msg.sender is token and
token is a token the oracle serves. The oracle determines the issuer of
token from its own configuration; the issuer is not supplied by the caller.
Validation Requirements
The oracle implementation MUST enforce the following validation rules when processing approveTransfer:
Single-Use Policy: Each approval is consumed entirely when a matching transfer occurs. Approvals MUST NOT be partially consumed or reused for multiple transfers.
Amount Matching: A transfer with amount is valid if and only if minAmt <= amount <= maxAmt (both bounds inclusive).
Best-Match Selection: When multiple valid approvals exist for the same (issuer, sender, recipient) triplet, the oracle SHOULD consume the approval with the smallest amount range to preserve larger approvals for potentially larger transfers.
Expiry Handling: Expired approvals (where block.timestamp >= expiry and expiry != 0) MUST be ignored during transfer validation but MAY remain in storage for auditing purposes.
The merkle tree root is used to verify that the public inputs actually come from the original off-chain payment instruction. The ZK proof system validates that all fields belong to the same committed payment message through Merkle proof verification.
Public Inputs
Purpose
Rationale
root
Merkle root of payment instruction
Data-integrity and field binding
debtorHash
Hash of debtor (sender) data
Privacy-preserving sender identification
creditorHash
Hash of creditor (recipient) data
Privacy-preserving recipient identification
minAmountMilli/maxAmountMilli
Value bounds in milli-units
Anti-front-running protection
currencyHash
Hash of currency code
Currency validation
expiry
Execution date as timestamp
Prevents replay and ensures timeliness
The ZK proof system MUST verify:
Hash Integrity: All provided hashes match computed hashes of the actual data
Amount Bounds: The transfer amount falls within the specified range
Merkle Proofs: All fields (debtor, creditor, amount, currency, expiry) belong to the same committed message
Expiry Validation: The execution date is consistent and not expired
The oracle MAY accept additional public inputs, e.g., extended currency validation, jurisdiction codes, sanctions list epochs
Proof System Flexibility
This standard is proof-system-agnostic. Implementations MAY use any ZK proof system (Groth16, PLONK, STARKs, etc.) as long as they:
Validate the required public inputs listed above
Ensure proper Merkle proof verification for field binding
Verifier is stateless; safe to swap when a new proof system is adopted.
Oracle logic can be upgraded independently of token contracts.
Rationale
Keeping oracle logic out of the token contract preserves fungibility and lets an issuer change its permissioning rules without redeploying the token. TransferApproval uses amount ranges so issuers can sign a single approval before the final FX quote is known. canTransfer returns the proofId, enabling downstream analytics and regulators to join on-chain transfers with off-chain payment system messages.
The Merkle proof requirement ensures that all approval data comes from the same authentic payment instruction, preventing field substitution attacks where an attacker might try to combine legitimate data from different transactions.
Amount Range Design: The minAmt/maxAmt bounds accommodate scenarios where the exact transfer amount is unknown at approval time (e.g., currency conversion with fluctuating exchange rates). The inclusive bounds (minAmt <= amount <= maxAmt) provide clear validation semantics, while the single-use consumption policy prevents approval reuse attacks.
Best-Match Selection: When multiple approvals overlap, selecting the approval with the smallest range optimizes for approval preservation, allowing issuers to create both broad approvals (e.g., 0-1000 tokens) and specific approvals (e.g., 100-110 tokens) without the specific approval being wastefully consumed by small transfers.
Backwards Compatibility
Existing ERC-20 consumers remain unaffected; a failed transfer simply reverts. Wallets and exchanges should surface the oracle’s revert messages so users know they lack approval.
Reference Implementation
A minimal reference implementation is available in the assets directory. It uses
RISC Zero as the proving system, chosen for:
Transparent Setup: No trusted ceremony required
Developer Experience: Write verification logic in Rust
Performance: Efficient proof generation and verification
Auditability: Clear, readable verification code
Any other ZK proof system satisfying the requirements in
Proof System Flexibility MAY be substituted.