-
Notifications
You must be signed in to change notification settings - Fork 830
Add ERC: Oracle-Permissioned ERC-20 with ZK Proofs #1062
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
chadxeth
wants to merge
16
commits into
ethereum:master
Choose a base branch
from
chadxeth:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
f085955
Add ERC: Oracle-Permissioned ERC-20 with ZK-Verified ISO 20022 Paymen…
chadxeth 61edc07
Enhance ERC-N: Update specifications to use RISC Zero as example instead
chadxeth 7c64502
Refine ERC-N: Clarify specifications for oracle-permissioned transfer…
chadxeth f38b103
Remove proposal attribution.
chadxeth 6e3bcf9
Update and rename erc-N.md to erc-7963.md
SamWilsn fb616c7
Rename A&I.jpg to A&I.jpg
SamWilsn cb0fb77
Update erc-7963.md
SamWilsn 53d119d
Merge branch 'master' into master
chadxeth 3fcdbab
Update ERC-7963 title and add description field
chadxeth 8963823
Add ERC-7963 reference implementation
chadxeth 00b9078
Remove unused image file
chadxeth 378974d
Fix EIP validator errors in ERC-7963
chadxeth 697379c
Fix remaining EIP validator errors
chadxeth b4d052a
Fix broken internal links in reference implementation
chadxeth 4e319e9
Rename assets directory and fix internal links
chadxeth e8cf27d
Merge branch 'master' into master
chadxeth File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,297 @@ | ||
| --- | ||
| eip: 7963 | ||
| title: Oracle-Permissioned ERC-20 with ZK Proofs | ||
| description: Extension with oracle-permissioned transfers validated via zero-knowledge proofs of payment instructions | ||
| author: Siyuan Zheng (@andrewcoder666) <[email protected]>, Xiaoyu Liu (@elizabethxiaoyu) <[email protected]>, Wenwei Ma (@madyinglight) <[email protected]>, Jun Meng Tan (@chadxeth) <[email protected]>, Yuxiang Fu (@tmac4096) <[email protected]>, Kecheng Gao (@thanks-v-me-50) <[email protected]>, Alwin Ng Jun Wei (@alwinngjw) <[email protected]>, Chenxin Wang (@3235773541) <[email protected]>, Xiang Gao (@GaoYiRu) <[email protected]>, yuanshanhshan (@xunayuan) <[email protected]>, Hao Zou (@BruceZH0915) <[email protected]>, Yanyi Liang <[email protected]>, Yuehua Zhang (@astroyhzcc) <[email protected]> | ||
| discussions-to: https://ethereum-magicians.org/t/erc-7963-oracle-permissioned-erc-20-with-zk-proofs/12345 | ||
| status: Draft | ||
| type: Standards Track | ||
| category: ERC | ||
| created: 2025-05-13 | ||
| requires: 20 | ||
| --- | ||
|
|
||
| ## Abstract | ||
|
|
||
| This proposal extends [ERC-20](./eip-20.md) 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. Reference implementation uses RISC Zero as the proving system, but the standard admits any ZK-proof system. | ||
|
|
||
| ## 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. Centralising rules in a singleton oracle 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", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. | ||
|
|
||
| ### Interfaces | ||
| ```solidity | ||
| /// @notice One-time ZK-backed approval for a single transfer. | ||
| struct TransferApproval { | ||
| address sender; | ||
| address recipient; | ||
| uint256 minAmt; // Minimum allowed transfer amount (inclusive) | ||
| uint256 maxAmt; // Maximum allowed transfer amount (inclusive) | ||
| uint256 expiry; // UNIX seconds; 0 == never expires | ||
| bytes32 proofId; // keccak256(root‖debtorHash‖creditorHash) | ||
| } | ||
|
|
||
| /// @title External oracle consulted by permissioned tokens. | ||
| interface ITransferOracle { | ||
| /// @dev Verifies zk-proof and stores a one-time approval. | ||
| /// @return proofId – unique handle for off-chain reconciliation | ||
| function approveTransfer( | ||
| TransferApproval calldata approval, | ||
| bytes calldata proof, // ZK proof bytes (system-specific) | ||
| bytes calldata publicInputs // ABI-encoded public outputs | ||
| ) external returns (bytes32 proofId); | ||
|
|
||
| /// @dev Atomically consumes an approval that covers `amount`. | ||
| /// MUST revert if no such approval exists. | ||
| function canTransfer( | ||
| address token, | ||
| address issuer, | ||
| address sender, | ||
| address recipient, | ||
| uint256 amount | ||
| ) external returns (bytes32 proofId); | ||
| } | ||
| ``` | ||
|
|
||
| ### ERC-20 Hook | ||
| A _Permissioned ERC-20_ **MUST** replace the standard internal | ||
| `_update(address from, address to, uint256 amount)` logic with: | ||
|
|
||
| ```solidity | ||
| bytes32 proofId = ORACLE.canTransfer(address(this), owner(), from, to, amount); | ||
| // MUST revert on failure | ||
| _super._update(from, to, amount); | ||
| emit TransferValidated(proofId); | ||
| ``` | ||
|
|
||
| `ORACLE` is an immutable constructor argument. (up to design) | ||
|
|
||
| ### Validation Requirements | ||
|
|
||
| The oracle implementation **MUST** enforce the following validation rules when processing `approveTransfer`: | ||
|
|
||
| ```solidity | ||
| require(minAmt <= maxAmt, "Invalid amount range"); | ||
| require(sender != address(0), "Invalid sender address"); | ||
| require(recipient != address(0), "Invalid recipient address"); | ||
| require(expiry > block.timestamp || expiry == 0, "Approval already expired"); | ||
| ``` | ||
|
|
||
| ### Approval Consumption Behavior | ||
|
|
||
| **Single-Use Policy**: Each approval is consumed entirely when a matching transfer occurs. Approvals **CANNOT** 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. | ||
|
|
||
| ### Events | ||
| ```solidity | ||
| event TransferApproved( | ||
| address indexed issuer, | ||
| address indexed sender, | ||
| address indexed recipient, | ||
| uint256 minAmt, | ||
| uint256 maxAmt, | ||
| uint256 expiry, | ||
| bytes32 proofId | ||
| ); | ||
|
|
||
| event ApprovalConsumed( | ||
| address indexed issuer, | ||
| address indexed sender, | ||
| address indexed recipient, | ||
| uint256 amount, | ||
| bytes32 proofId | ||
| ); | ||
|
|
||
| event TransferValidated(bytes32 indexed proofId); | ||
| ``` | ||
|
|
||
| ### Payment Instruction Message Format | ||
|
|
||
| Payment instructions **MUST** be JSON objects with the following structure: | ||
|
|
||
| ```json | ||
| { | ||
| "messageId": "string", | ||
| "creationDateTime": "ISO 8601 timestamp", | ||
| "paymentInfo": { | ||
| "debtor": { | ||
| "name": "string", | ||
| "identifier": "string", | ||
| "identifierScheme": "string" | ||
| }, | ||
| "creditor": { | ||
| "name": "string", | ||
| "identifier": "string", | ||
| "identifierScheme": "string" | ||
| }, | ||
| "amount": { | ||
| "value": "string (in milli-units)", | ||
| "currency": "string (ISO 4217 code)" | ||
| }, | ||
| "executionDate": "ISO 8601 timestamp" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| **Field Definitions:** | ||
| - `messageId`: Unique identifier for this payment instruction | ||
| - `creationDateTime`: When the instruction was created (ISO 8601 format, UTC) | ||
| - `debtor.identifier`: Sender's account identifier (Ethereum address, IBAN, BIC, etc.) | ||
| - `debtor.identifierScheme`: Type of identifier (e.g., "ethereum_address", "iban", "bic", "swift") | ||
| - `creditor.identifier`: Recipient's account identifier | ||
| - `creditor.identifierScheme`: Type of identifier | ||
| - `amount.value`: Transfer amount in **milli-units** (integers only, no decimals) | ||
| - `amount.currency`: Three-letter currency code (e.g., "USD", "EUR", "GBP") | ||
| - `executionDate`: When the transfer should execute (becomes approval expiry) | ||
|
|
||
| **Milli-unit Conversion**: All monetary amounts **MUST** be represented as integers in milli-units (10⁻³) to avoid floating-point precision issues: | ||
| - 1 milli-unit = 0.001 base currency units | ||
| - Example: 1.50 USD = "1500" milli-units | ||
| - Example: 0.001 BTC = "1" milli-unit | ||
|
|
||
| ### Message Canonicalization | ||
|
|
||
| To ensure deterministic hashing, payment instructions **MUST** be canonicalized before Merkle tree construction: | ||
|
|
||
| 1. **JSON Canonicalization**: Apply [RFC 8785 (JCS)](https://www.rfc-editor.org/rfc/rfc8785) | ||
| - Sort object keys lexicographically | ||
| - Remove insignificant whitespace | ||
| - Use minimal JSON encoding | ||
|
|
||
| 2. **Text Normalization**: Apply UTF-8 NFC (Normalization Form C) to all string fields | ||
|
|
||
| 3. **Timestamp Format**: All timestamps **MUST** use ISO 8601 format in UTC (e.g., "2025-01-03T10:30:00Z") | ||
|
|
||
| **Example Canonicalization:** | ||
|
|
||
| Input: | ||
| ```json | ||
| { "amount": { "value": "1500", "currency": "USD" }, "debtor": { "name": "Alice" } } | ||
| ``` | ||
|
|
||
| Output (canonical): | ||
| ```json | ||
| {"amount":{"currency":"USD","value":"1500"},"debtor":{"name":"Alice"}} | ||
| ``` | ||
|
|
||
| ### Merkle-and-Proof Requirements | ||
| 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: | ||
| 1. **Hash Integrity**: All provided hashes match computed hashes of the actual data | ||
| 2. **Amount Bounds**: The transfer amount falls within the specified range | ||
| 3. **Merkle Proofs**: All fields (debtor, creditor, amount, currency, expiry) belong to the same committed message | ||
| 4. **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**. The reference implementation uses RISC Zero 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 | ||
|
|
||
| However, implementations **MAY** use any ZK proof system (Groth16, PLONK, STARKs, etc.) as long as they: | ||
| 1. Validate the required public inputs listed above | ||
| 2. Ensure proper Merkle proof verification for field binding | ||
| 3. Maintain the same security guarantees | ||
|
|
||
| ### Upgradeability | ||
| + Token and Oracle **MAY** be behind [EIP-1967](./eip-1967.md) proxies. | ||
| + 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 one oracle serve hundreds of issuers. `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 complete reference implementation is available in the [assets directory](../assets/eip-7963/eip-permissioned-erc20). | ||
|
|
||
| The implementation includes: | ||
|
|
||
| + **Solidity Contracts**: Complete implementation with OpenZeppelin v5 compatibility | ||
| - `PermissionedERC20.sol` - ERC-20 token with oracle-based transfer validation | ||
| - `TransferOracle.sol` - Manages one-time transfer approvals with ZK proof verification | ||
| - `RiscZeroVerifier.sol` - RISC Zero proof verification contract | ||
|
|
||
| + **RISC Zero ZK Programs**: Rust-based guest program for payment instruction validation | ||
| - Guest program validates payment instruction messages | ||
| - Merkle proof verification for field integrity | ||
| - Zero-knowledge proof generation | ||
|
|
||
| + **Testing Framework**: Comprehensive test suite | ||
| - 80 passing smart contract tests (Hardhat/TypeScript) | ||
| - 34 passing Rust unit tests | ||
| - 11 passing integration tests | ||
| - Gas profiling and optimization tests | ||
|
|
||
| + **Development Tools**: | ||
| - CLI host program for proof generation and verification | ||
| - Test data generators and utilities | ||
| - Deployment scripts for various networks | ||
|
|
||
| The reference implementation demonstrates: | ||
| - Full payment instruction message validation | ||
| - Merkle proof verification for field integrity | ||
| - RISC Zero proof generation and verification | ||
| - Integration with standard ERC-20 workflows | ||
| - Comprehensive error handling and edge cases | ||
|
|
||
| **Setup Instructions**: See [README.md](../assets/eip-7963/eip-permissioned-erc20/README.md) in the assets directory for installation and usage. | ||
|
|
||
| ## Security Considerations | ||
|
|
||
| + **Replay Protection** – approvals are one-time and keyed by `proofId`. | ||
| + **Field Binding** – Merkle proofs ensure all approval data comes from the same committed message. | ||
| + **Oracle Risk** – issuers SHOULD deploy dedicated oracles; a compromised oracle only endangers its own tokens. | ||
| + **Proof System Security** – the chosen ZK proof system must provide computational soundness and zero-knowledge properties. | ||
| + **Hash Function Security** – implementations should use cryptographically secure hash functions (e.g., Keccak256, SHA256). | ||
| + **Amount Validation** – strict bounds checking prevents amount manipulation attacks. | ||
|
|
||
| ## Copyright | ||
|
|
||
| Copyright and related rights waived via [CC0](../LICENSE.md). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| OracleTest:testApproveAndTransferHappy() (gas: 220163) | ||
| OracleTest:testApproveTransfer_Fail_InvalidApprovalData_Expired() (gas: 51376) | ||
| OracleTest:testApproveTransfer_Fail_InvalidProof() (gas: 61362) | ||
| OracleTest:testApproveTransfer_Fail_InvalidPublicInputs_Mismatch() (gas: 51108) | ||
| OracleTest:testApproveTransfer_Fail_NotIssuer() (gas: 41666) | ||
| OracleTest:testApproveTransfer_Fail_ProofAlreadyUsed() (gas: 228516) | ||
| OracleTest:testApproveTransfer_HappyPath() (gas: 223747) | ||
| OracleTest:testCanTransfer_Fail_AmountTooHigh() (gas: 225253) | ||
| OracleTest:testCanTransfer_Fail_AmountTooLow() (gas: 225309) | ||
| OracleTest:testCanTransfer_Fail_Expired() (gas: 228086) | ||
| OracleTest:testCanTransfer_Fail_NoApproval() (gas: 26345) | ||
| OracleTest:testCanTransfer_Fail_WrongCaller() (gas: 222909) | ||
| OracleTest:testCanTransfer_HappyPath() (gas: 186082) | ||
| OracleTest:testCanTransfer_MultipleApprovals_SelectsBestFit() (gas: 311872) | ||
| OracleTest:testCanTransfer_ReplayConsumedApproval() (gas: 186982) | ||
| OracleTest:testGas_snapshot() (gas: 213919) | ||
| PermissionedERC20Test:testBurnFrom_FailNotOwner() (gas: 14861) | ||
| PermissionedERC20Test:testBurnFrom_Success() (gas: 90227) | ||
| PermissionedERC20Test:testDeployment() (gas: 34592) | ||
| PermissionedERC20Test:testMint_FailNotOwner() (gas: 16883) | ||
| PermissionedERC20Test:testMint_Success() (gas: 46279) | ||
| PermissionedERC20Test:testTransfer_RequiresOracleCheck() (gas: 27380) |
32 changes: 32 additions & 0 deletions
32
assets/eip-7963/eip-permissioned-erc20/.github/workflows/test.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| pull_request: | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| hardhat_operations: | ||
| name: Hardhat Project CI | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| submodules: recursive | ||
|
|
||
| - name: Set up Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
| cache: 'npm' | ||
|
|
||
| - name: Install Node.js Dependencies | ||
| run: npm ci | ||
|
|
||
| - name: Compile Contracts | ||
| run: npx hardhat compile | ||
| id: compile | ||
|
|
||
| - name: Run Hardhat Tests | ||
| run: npx hardhat test | ||
| id: test |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Text describing the reference implementation should be placed in that section.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noted on this.