- Overview
- Technical Stack
- Architecture
- User Flow
- Installation
- Deployed Contracts
- E2E Script Scenario
- References
ZK ID Policy Layer is a privacy-preserving modular on-chain policy layer.
In this repo, this ZK ID Policy Layer would be applied for RWA (Real World Asset) product's token issuance as one of use case.
Since the ZK ID Policy Layer is the modular on-chain policy layer, custom ZK Proof Verifier contracts (i.e. Accredition Proof Verifier, Age Proof Verifier, Residency Proof Verifier, etc) can be registered to the ZK Proof Verifier Registry contract. Those registered-custom ZK Proof Verifier contracts can be reused by RWA issuers to configure the eligibility and compliance rules as a policy for RWA buyers (RWA investors) - when the RWA issueres would create/issue a new RWA Product in the form of RWA Product token.
And then, it allow RWA issures to check whether RWA buyers is eligible for buying a RWA prodct and they are compliant or not without touching any sensitive data of RWA buyers by using Zero-Knowledge Proof.
- RWA buyer (RWA investor) prove their identity attributes with
Zero-Knowledge Proofs- without revealing the sensitive personal (raw) data of RWA buyer on-chain. - RWA issuer can mint only to the
eligibleandcompliantbuyers β without touching the underlying personal data of RWA buyer on-chain. For RWA issuer, they do not need to store any sensitive personal (raw) data of RWA buyer, which enable to bring the benefit ofdata minimizationas well.
Key capabilities
| Capability | Description |
|---|---|
Privacy-preserving proofs |
RWA buyers (Investors) generate ZK proofs locally; The ZK proof can be verified via the ZK Proof Verifier on-chain while only nullifier hash will be stored on-chain |
Pluggable ZK Proof Verifier Registry |
Any Noir-compiled proof verifier can be registered without contract upgrades |
Per-RWA product policy config |
Each RWA product independently declares which proof types and threshold values it requires |
| Replay-attack prevention | Poseidon-based nullifier hashs are stored per ZK Proof Verifier ID to prevent from double-submission |
| Role-based UI | Separate dashboards for Admin, RWA Issuer, and RWA Buyer |
| Layer | Technology |
|---|---|
| ZK Circuit Language | Noir / Nargo v1.0.0-beta.19 (Powered by Aztec) |
| Proof Backend | @aztec/bb.js v4.2.0-aztecnr-rc.2 |
| Proof Runtime (browser / scripts) | @noir-lang/noir_js v1.0.0-beta.19 |
| Smart Contracts | Solidity ^0.8.28 |
| Contract Dev & Testing | Foundry |
| Blockchain | HashKey Chain Testnet (Chain ID 133) |
| Frontend Framework | Next.js 15 (App Router) + React 19 |
| Wallet / Chain Abstraction | Reown AppKit + wagmi v2 + viem v2 |
| Script Runtime | Bun |
+------------------------------+
| Data Sources |
| KYC / Bank / Workday / CEX |
+-------------+----------------+
| zkTLS / API / Attestors (β οΈLimitation: This part has not been implemented yet)
v
+------------------------------+
| ZK Circuit Library |
| accreditation / age / |
| credit-score / kyc-tier / |
| nationality / residency |
+-------------+----------------+
| Noir -> UltraHonk proof
v
+------------------------------+
| ZKIDRegistry |
| - nullifier storage |
| - per-user proof records |
| - on-chain verification |
+-------------+----------------+
|
+----------------------+----------------------+
v v v
+-------------+ +--------------------+ +--------------+
|ZKProofVerif-| | RWAProductFactory | | RWAProduct |
|ierRegistry | | - issuer approval | | - policy |
|(pluggable) | | - product creation| | config |
+-------------+ +--------------------+ | - mint() |
+--------------+
NOTE:
β οΈ Limitation: The"Data Sources"part above has not been implemented yet. In the e2e script and frontend, the mock source data has been used instead of the actual source data. This part will be implemented using zkTLS protocols (i.e. Primus, NexaID, etc) in the future to retrieve the actual source data.
| Contract | Role |
|---|---|
ZKIDRegistry.sol |
Stores each user's ZK proof records and nullifiers; verifies proofs on submission via the verifier registry |
ZKProofVerifierRegistry.sol |
Registry of deployed Noir-generated verifier contracts, identified by sequential uint8 IDs |
RWAProductFactory.sol |
Manages issuer approval and deploys new RWAProduct instances with their policy configuration |
RWAProduct.sol |
ERC-20-like RWA token that enforces proof policy at mint() time by re-verifying buyer credentials |
circuits/ |
Auto-generated HonkVerifier.sol contracts, one per Noir circuit (e.g. AccreditationVerifier.sol) |
Each circuit follows the same pattern:
- private inputs hold the
real credential values(Source data, which will be retrieved via zkTLS protocol, etc in the future. Currently, the mock source data is assigned to here) - public inputs carry
thresholdsand the Poseidonnullifier hash.
The on-chain verifier checks the proof and the registry prevents the same nullifier from being reused.
| Circuit | Private Inputs | Public Inputs |
|---|---|---|
accreditation |
investor_type, income, net_worth, aum, salts |
income_threshold, net_worth_threshold, aum_threshold, nullifier_hash |
age |
age, salts |
age_threshold, nullifier_hash |
credit-score |
credit_score, salts |
credit_score_threshold, nullifier_hash |
kyc-tier |
kyc_tier, salts |
kyc_tier_threshold, nullifier_hash |
nationality |
country_code, iso_code, salts |
excluded_country_codes, excluded_iso_codes, nullifier_hash |
residency |
country_code, iso_code, salts |
excluded_country_codes, excluded_iso_codes, nullifier_hash |
In this repo, the ZK Proof Layer is applied to the RWA product's token issurance.
- RWA issuer can choose the preferrable ZK Proof Verifier contracts (i.e.
AccreditationVerifier.sol), which are registered by the Admin user. - By choosing the 6 ZK circuits based ZK Proof Verifier contracts above, RWA issuer can check the following eligibility and compliance for RWA buyer (Investor).
+------------------------------+
| Identity Layer |
| - KYC verified |
| - Human (not Sybil) |
+-------------+----------------+
|
+-------------v----------------+
| Compliance Layer |
| - Jurisdiction |
| - Sanctions / AML |
+-------------+----------------+
|
+-------------v----------------+
| Financial Eligibility |
| - Balance / inflow |
| - Portfolio size |
+-------------+----------------+
|
+-------------v----------------+
| Investor Qualification |
| - Accredited investor |
| - Risk profile |
+------------------------------+
Three roles interact with the system: Admin, RWA Issuer, and RWA Buyer.
[ Admin ] [ RWA Buyer ] [ RWA Issuer ]
| | |
| 1. Register ZKProofVerifier | |
| contracts to | |
| ZKProofVerifierRegistry | |
| | |
| 2. Approve issuer in | |
| RWAProductFactory | |
| | |
| | 3. Generate ZK proofs |
| | (locally in browser / |
| | script) |
| | |
| | 4. Create new RWAProduct
| | with required verifier
| | IDs + thresholds
| | |
| | 5. Submit proof batch |
| | to ZKIDRegistry |
| | |
| |<---------------------- 6. mint()
| | RWA token received |
| | (only if all proofs |
| | pass policy check) |
Step details
-
Admin registers verifiers β Calls
ZKProofVerifierRegistry.registerNewZkVerifier()for each auto-generatedVerifier.sol, associating it with a sequentialuint8ID and human-readable threshold labels. (NOTE: Admin can add more newZKProofVerifercontracts besides the 6 ZK circuits based ZK Proof Verifier contracts above by calling theZKProofVerifierRegistry.registerNewZkVerifier().) -
Admin approves issuer β Calls
RWAProductFactory.approveIssuer()to whitelist the issuer address. -
Buyer generates proofs β For each required credential (age, KYC tier, accreditation, etc.), the buyer runs the corresponding
Noir circuitlocally using@aztec/bb.js. Private inputs never leave the device. -
Issuer creates product β Calls
RWAProductFactory.createNewRWAProduct(), specifying requiredzkProofVerifierIdsand threshold values. A newRWAProductcontract is deployed. -
Buyer submits proofs β Calls
ZKIDRegistry.submitProof()(orsubmitProofBatch()) for each proof. Each proof is verified on-chain;nullifier hashesare stored to prevent replay. -
Issuer mints tokens β Calls
RWAProduct.mint(buyerAddress, amount). The contract re-reads the buyer's stored proofs fromZKIDRegistrycontract and checks every configured verifier'sthresholdbefore minting.
DEMO Video link: https://www.loom.com/share/d325a34398aa4ac48b917988f6b198b0
NOTE:
β οΈ Limitation: The"Data Sources"part above has not been implemented yet.- Therefore, in the RWA buyer page (
localhost:3000/buyer), the randomly generated mock source data has been used by clicking the"Generate Random Input Values"button in the ZK Proof Status panel - instead of the actual source data. - This part will be implemented using zkTLS protocols (i.e. Primus, NexaID, etc) in the future to retrieve the actual source data.
- Therefore, in the RWA buyer page (
- Noir / Nargo (
nargoCLI) - Foundry (
forge,cast) - Bun (script runner)
- Node.js >= 20 (for the frontend)
git clone <repo-url>
cd ZK-ID-policy-layerCompile each circuit and generate the on-chain verifier. Run the convenience script inside each circuit folder:
cd circuits
# Compile and build all circuits
sh compile-and-build-all-circuits.shCompiled artefacts are written to circuits/<name>/target/.
The auto-generated Verifier.sol files are written to circuits/<name>/target/Verifier.sol.
3. Compile & Deploy Smart Contracts on HashKey Chain testnet
NOTE: Fancet for the HashKey Chain Testnet: https://faucet.hashkeychain.net/faucet
cd contracts
# Install Foundry dependencies
forge install
# Compile
sh compile-contracts.sh
# Deploy to HashKey Chain Testnet
sh scripts/deployments/hashkey-chain-testnet/deploy.shCopy the deployed contract addresses into contracts/.env:
cd contracts
cp .env.example .envThen, adding your own private keys to the following 4 variables:
DEPLOYER_PRIVATE_KEY=<0x...>
ADMIN_PRIVATE_KEY=<0x...>
RWA_ISSUER_PRIVATE_KEY=<0x...>
RWA_BUYER_PRIVATE_KEY=<0x...>NOTE: ADMIN_PRIVATE_KEY must be same private key with the DEPLOYER_PRIVATE_KEY in the ./contracts/.env file.
cd scripts
bun installcd app
cp .env.example .envThen fill in the required values in app/.env:
# Reown / WalletConnect Project ID (https://dashboard.reown.com/)
NEXT_PUBLIC_PROJECT_ID=""
# Salt values for ZK proof generation (required)
NEXT_PUBLIC_COMMITMENT_SALT="Your commitment salt here"
NEXT_PUBLIC_NULLIFIER_SALT="Your nullifier salt here"The contract addresses and network configuration are pre-filled from .env.example. Then run:
npm install
npm run devOpen http://localhost:3000.
Each script generates a ZK proof for a single circuit.
Script location: scripts/unit/circuits/<circuit-name>/generate-proof.ts
cd scripts
# Accreditation
bun run generate-proof:accreditation
# Age
bun run generate-proof:age
# Credit Score
bun run generate-proof:credit-score
# KYC Tier
bun run generate-proof:kyc-tier
# Nationality
bun run generate-proof:nationality
# Residency
bun run generate-proof:residencyE2E Script - Only ZK Proof generation and On-chain verification (e2e_zkp-generation-and-onchain-verification.ts)
Script location: scripts/e2e/e2e_zkp-generation-and-onchain-verification.ts
Run command:
cd scripts
bun run e2e:zkp-generation-and-onchain-verificationScript location: scripts/e2e/e2e_rwa-workflow.ts
Run command:
cd scripts
bun run e2e:rwa-workflowThe script exercises the full primary issuance lifecycle across three actor accounts (ADMIN, RWA_ISSUER, RWA_BUYER) read from contracts/.env.
The buyer account generates six proofs locally (using @aztec/bb.js UltraHonk backend) and submits each to ZKIDRegistry via submitProof():
| # | Proof | Key private inputs | Key thresholds |
|---|---|---|---|
| 1 | AccreditationProof |
income=250,000, net_worth=1,500,000, aum=0 |
income >= 200,000 OR net_worth >= 1,000,000 |
| 2 | AgeProof |
age (actual value) |
configurable age_threshold |
| 3 | CreditScoreProof |
credit_score (actual value) |
configurable credit_score_threshold |
| 4 | KycTierProof |
kyc_tier (actual value) |
configurable kyc_tier_threshold |
| 5 | NationalityProof |
country_code, iso_code |
excluded country/ISO lists |
| 6 | ResidencyProof |
country_code, iso_code |
excluded country/ISO lists |
For every proof, a Poseidon-based nullifier hash is computed from the credential values and a random salt, then included as the last public input. ZKIDRegistry stores the nullifier and rejects any duplicate submission.
The admin account calls ZKProofVerifierRegistry.registerNewZkVerifier() for each of the six auto-generated Verifier.sol contracts, associating them with sequential IDs (0-5) and threshold label metadata:
| ID | Verifier contract | Threshold labels |
|---|---|---|
| 0 | AccreditationProofVerifier |
income_threshold, net_worth_threshold, aum_threshold |
| 1 | AgeProofVerifier |
age_threshold |
| 2 | CreditScoreProofVerifier |
credit_score_threshold |
| 3 | KycTierProofVerifier |
kyc_tier_threshold |
| 4 | NationalityProofVerifier |
excluded_country_codes, excluded_iso_codes |
| 5 | ResidencyProofVerifier |
excluded_country_codes, excluded_iso_codes |
RWAProductFactory.approveIssuer(RWA_ISSUER_ADDRESS)
The issuer calls RWAProductFactory.createNewRWAProduct() with:
name/symbolβ token metadatazkProofVerifierIdsβ the subset of verifier IDs the product requires
(e.g.[0, 2, 3, 4, 5]= Accreditation, Credit Score, KYC Tier, Nationality, Residency)zkProofVerifierThresholdsβ parallel array of threshold values per verifier
A new RWAProduct contract is deployed and its address is emitted in the event log.
RWAProduct.mint(RWA_BUYER_ADDRESS, amount)
mint() internally calls ZKIDRegistry contract to retrieve the buyer's stored proof for each configured verifier ID, re-runs the policy threshold check, and reverts if any check fails. On success, the buyer's balance is increased.
| Role | Env variable |
|---|---|
| Admin | ADMIN_PRIVATE_KEY |
| RWA Issuer | RWA_ISSUER_PRIVATE_KEY |
| RWA Buyer | RWA_BUYER_PRIVATE_KEY |
NOTE: ADMIN_PRIVATE_KEY must be same private key with the DEPLOYER_PRIVATE_KEY in the ./contracts/.env file.
All contracts are deployed on HashKey Chain Testnet (Chain ID 133).
- Click any address to view it on the HashKey Chain Testnet Explorer.
- Fancet of the HashKey Chain Testnet is here
| Contract | Address |
|---|---|
| ZKProofVerifierRegistry | 0xc5c0b98b5d9a29da72b28026a2984958b1a3d97f |
| ZKIDRegistry | 0xe06d40a01a583dbbc3eece1463113356271aa3a0 |
| RWAProductFactory | 0x858aa18878e08cd942387c5397efeb57ab19af25 |
| Circuit | Address |
|---|---|
| AccreditationProofVerifier | 0x677fb51e3c949a9b365e336582d3a87ec551b1e9 |
| AgeProofVerifier | 0xc1e640b2c0a49b2bfea59a82e982e3d596d0a407 |
| CreditScoreProofVerifier | 0x934fee07952a865cc233a0de404d8c72b9055028 |
| KycTierProofVerifier | 0x3b395f1920fddeb3ab0bcd5a1eab9f4b393c4bbc |
| NationalityProofVerifier | 0x896483b8ecb671011c6f58e5ec0b0cf1b71d107d |
| ResidencyProofVerifier | 0x6d92e73faf9b427cb9bf124acb3f15aca22154cf |
HonkVerifier Contracts (auto-generated from Noir)
| Circuit | Address |
|---|---|
| HonkVerifier for Accreditation circuit | 0x306406a640207a05ae454dd5eaaa3805082e90b3 |
| HonkVerifier for Age circuit | 0x4dfdbb4761529a6c34da9a92b0582c5484ea8bb2 |
| HonkVerifier for Credit Score circuit | 0x9d369e1225fa0a9d252c2b513bd59b8d4ae92905 |
| HonkVerifier for KYC Tier circuit | 0x8c208cd8e37ad734c488f20214907222b22677a8 |
| HonkVerifier for Nationality circuit | 0x0e5c52239f257bba27ab306a7fcde2668dc989b2 |
| HonkVerifier for Residency circuit | 0x389d349389d79ccc4e5be77fbc33aef1be483741 |
ZK circuit:
Smart Contract:
HashKey Chain:
- Website
- Developer Docs
- Network info
Fancetfor theHashKey Chain Testnet: https://faucet.hashkeychain.net/faucet
Frontend: