Introduction
What is ZeroPrint?
ZeroPrint is a zero-knowledge privacy pool for ETH on Robinhood Chain. You deposit from one wallet and later withdraw to a fresh wallet, and nobody on-chain can tell which deposit paid for which withdrawal.
It runs on the open-source, audited Privacy Pools protocol by 0xbow (Apache-2.0). ZeroPrint deploys those contracts without an owner, adds an on-chain Association Set Provider that keeps stolen funds out, and gives you an app and a command-line tool where every secret stays on your device.
Why privacy matters
A blockchain is a public ledger. The moment someone links your name to one address (an exchange withdrawal, an ENS name, a payment to a friend), they can read:
- your balance, today and at any time in the past;
- every payment you made and received, and with whom;
- your other wallets, by following the money between them.
That is the equivalent of publishing your bank statement. ZeroPrint breaks the on-chain link between an old, known wallet and a new one, so the new one starts clean.
How it works
Picture a box full of sealed envelopes, each holding the same kind of coin. When you put an envelope in, you keep a secret that only fits your envelope. Later you prove to the box, with math, that one of the envelopes is yours, without pointing at which one, and it pays you out to any address you name.
- Deposit. Your browser creates a secret and a nullifier from your Zero Key and sends only their hash, the commitment, with your ETH. The commitment joins a public Merkle tree of all deposits.
- Wait. The ASP screens the deposit (about 30 minutes). Others deposit too. Every approved deposit is part of your anonymity set.
- Withdraw. Your browser builds a Groth16 proof that says: "I know the secret of some commitment in the pool tree, its deposit is in the approved set, and here is its nullifier hash." The relayer submits it; the contract checks the proof, records the nullifier so the note can't be spent twice, and pays the recipient.
The nullifier hash and the commitment are different hashes of different inputs, so no one can connect them. The proof reveals the amount, the recipient and the nullifier hash, and nothing else.
Partial withdrawals. You can withdraw part of a deposit. The proof creates a new hidden note for the rest (same deposit lineage), which you can withdraw later.
Clean funds: the ASP
A plain mixer lets hackers hide stolen money among honest users. Privacy Pools adds an Association Set Provider (ASP): a list of deposits that passed screening. Your withdrawal proof also proves your deposit is on that list, still without saying which one it is.
On ZeroPrint the ASP is a contract, ZeroPrintASP, and it is the only address allowed to publish the approved set. No server decides who gets in:
- Queue. Each deposit is queued on-chain, which starts a 30-minute window so hacks can be reported first. Anyone can queue; ZeroPrint's keeper does it automatically.
- Screen. Inside the window, a guardian can deny the deposit. ZeroPrint's keeper denies depositors on the OFAC sanctions list and on a ZeroPrint blocklist of known scam and trap wallets on Robinhood Chain. The guardian can do nothing else, and nothing after the window.
- Approve. After the window, anyone can approve the deposit: the contract adds it to its Merkle tree and posts the new root to the Entrypoint. The keeper does this automatically.
- Final. An approval cannot be revoked; the set only grows.
The app rebuilds the whole set from the contract's events and checks it against the on-chain root, so nobody learns which deposit a visitor owns.
Ragequit
If a deposit is denied, still pending, or you simply want out, the original depositing wallet can ragequit: it proves ownership of the note and gets the remaining balance back. This is public (it shows the money returned to the same wallet), but it means the ASP can never freeze or take your funds.
User guide
Getting started
- Install a browser wallet: MetaMask, Rabby or OKX. If you have several, the app lets you choose one.
- Open the app. It checks the network, the contracts and the ZK circuits before you start.
- Click Connect wallet. The app adds and switches to the right network.
- Create a Zero Key (next section).
Your Zero Key
Your Zero Key is 12 words, separate from your wallet's seed. Every deposit's secret and nullifier are derived from it, so these 12 words are the only thing that can withdraw your deposits.
- Back it up on paper. The app asks you to type two of the words back before you continue.
- In this browser it is stored encrypted with your password (PBKDF2 with 310,000 iterations, then AES-GCM). It is never sent anywhere.
- Restore on any device by pasting the 12 words. The app rescans the chain and rebuilds every deposit.
- Never type it into a site other than ZeroPrint, and never share it. Anyone who has it can withdraw your funds.
Depositing
- Unlock your Zero Key and connect your wallet.
- Enter an amount (minimum 0.001 ETH). Prefer round amounts.
- Confirm in your wallet. The deposit appears under My deposits with a countdown.
- After about 30 minutes it shows Approved (or Denied with the reason).
Withdrawing privately
- In My deposits, pick an approved deposit and click Withdraw privately.
- Enter the amount and a fresh recipient address that has never been used.
- The app shows the relayer fee (currently none: ZeroPrint's relayer pays the gas). Click Generate proof & withdraw.
- Your browser builds the proof (a few seconds), the relayer submits it, and the recipient receives the ETH. The recipient needs no gas.
You don't need to connect a wallet to withdraw; nothing is signed.
Privacy checklist
The math is solid. Most leaks come from habits. Before you withdraw:
- Wait. Let time pass and let others deposit after you. Withdrawing minutes after depositing narrows the crowd.
- Round amounts. Deposit 0.1, not 0.0937. Withdraw in several parts at different times if your amount is unusual.
- Fresh recipient. Never your depositing wallet, never a wallet you used before.
- Don't fund the recipient yourself. The relayer pays gas; sending gas from your old wallet links them.
- Don't send it back. Moving funds from the new wallet to the old one undoes everything.
- Different behaviour. If the new wallet buys the same rare token at the same time as the old one, it can be matched.
- Network privacy. Your IP is visible to the site and RPCs you use. A VPN or Tor helps.
Footprint
Footprint is a read-only exposure scan. Paste any wallet and see what an observer sees: where its money came from (exchanges, bridges, other wallets) and where it went, across Robinhood, Ethereum, BNB Chain, Base and Solana. Bridge hops are re-checked on both chains and marked Verified on-chain, Bridge API only or Likely match.
Use it before depositing to see what is already linked to you, and after withdrawing to confirm your fresh wallet shows nothing but the pool's relayer. Footprint scans mainnets only.
Token launches. Footprint follows the money up to two hops from the wallet, across chains, and shows token launches it finds on the way, with the path that links them:
- tokens the wallet created itself (Robinhood, Solana pump.fun / LaunchLab / Meteora DBC or minted directly, Ethereum);
- tokens created by wallets it funded, was funded by, or bridged to, directly or through one more wallet;
- bundle patterns: two or more linked wallets that bought the same token right at launch (Solana: in its launch block; Robinhood: within 3 blocks), with the token's deployer and, when the deployer is on the trail, how it connects.
Each token links to its contract on the chain's explorer and to GMGN, with its peak market cap: Robinhood peaks come from our own index of the chain; Solana and Ethereum peaks are approximate, from GeckoTerminal's daily candles, and can miss launch-day spikes. Scanning a token's own address shows the token and its creator. Nothing is shown when nothing is found.
Privacy. Scans are not logged: no accounts, no analytics, no access logs. The scan service only sees a scrambled ID of your IP that changes daily, never the IP itself. Results, which are public chain data, are cached for up to 6 hours. Every scan ends with what Footprint cannot see, and each finding says where it comes from. Exchanges, DEXes and privacy pools end a path, because there is no on-chain link through them.
Troubleshooting
- "ASP set does not match the on-chain root yet"
- The ASP just published a new root. Wait a minute and try again.
- "This deposit is not approved by the ASP (yet)"
- Check the countdown in My deposits. Denied deposits can only ragequit.
- My deposit still says "waiting" long after 30 minutes
- The keeper may be offline. Approve it yourself in My deposits, from the wallet that made the deposit, or with the CLI.
- The relayer fee is above the 0.5% cap
- The pool never lets a relayer take more than 0.5%. Use another relayer or withdraw a larger amount.
- Ragequit fails with OnlyOriginalDepositor
- Connect the exact wallet that made the deposit.
- My deposits are missing
- Make sure you unlocked the right Zero Key. Restore with your 12 words; the app rescans the chain.
- The wrong wallet extension opens
- Click the wallet button again and pick your wallet from the list.
Command-line tool
Everything the app does also works from a terminal, without this website: list your deposits, approve them yourself, withdraw privately or ragequit. Proofs are made on your computer; your Zero Key is read from the terminal and never stored. Needs Node.js 22 and Yarn.
git clone https://github.com/zeroprinxyz/zeroprint && cd zeroprint
yarn install --ignore-scripts
yarn workspace @0xbow/privacy-pools-core-sdk build
node zeroprint/cli/zeroprint.mjs status
zeroprint status // list your deposits
zeroprint approve // queue/approve your deposits (depositing wallet's key)
zeroprint withdraw <eth> --to <0x…> // private withdraw through a relayer
zeroprint withdraw <eth> --direct // no relayer: your wallet sends it and pays the gas
zeroprint ragequit [--deposit N] // public exit back to the depositing wallet
// options: --network mainnet|testnet, --rpc <url>, --relayer <url>
If this website goes away. Your funds live in the contracts, not on our servers. With your 12 words and the CLI you can still find your deposits, approve them from the depositing wallet, and withdraw through any relayer (--relayer) or on your own (--direct; use a fresh wallet funded from somewhere unlinked). Ragequit needs nothing but the contract.
Developers
Architecture
browser app / CLI + Privacy Pools SDK (keys, proofs: all local)
│ /rpc (read-only allow-list) /relayer/* /asp/stats /footprint-api/*
▼
ZeroPrint web server (static files, strict CSP, same-origin proxies)
├─► chain RPC
└─► relayer submits Entrypoint.relay(), pays the gas
keeper (guardian key) queues, screens and approves deposits on ZeroPrintASP
chain: Entrypoint (proxy, owner renounced) → PrivacyPoolSimple (ETH)
→ WithdrawalVerifier, CommitmentVerifier
ZeroPrintASP (the Entrypoint's only ASP postman) → posts association-set roots
The pool contracts and circuits are upstream Privacy Pools, unmodified. ZeroPrint adds the ZeroPrintASP contract, a deploy script that renounces the owner, the keeper, a relayer launcher, the app and the CLI. All of it is open source.
Networks & contracts
| Contract | Robinhood Chain |
|---|
Addresses are read from this site's deployment and verified on Sourcify.
Circuits & trusted setup
- Proof system: Groth16 over BN254, circuits written in Circom.
- withdraw: proves the existing note's commitment is in the state tree, its label is in the ASP tree, the withdrawn amount fits (128-bit range checks), the new nullifier differs from the old one, and outputs the new commitment hash and the spent nullifier hash. The withdrawal context (recipient, fee data, pool scope) is bound into the proof.
- commitment: used by ragequit; proves knowledge of a note's value, label, nullifier and secret.
- Hashes: commitment = Poseidon(value, label, Poseidon(nullifier, secret)); trees are Lean incremental Merkle trees (Poseidon), max depth 32.
- Trusted setup: proving keys come from the public 0xbow ceremony. The SDK checks the SHA-256 of every circuit file against pinned hashes before using it.
Contract interface
// Entrypoint
function deposit(uint256 precommitment) payable returns (uint256 commitment);
function relay(Withdrawal withdrawal, WithdrawProof proof, uint256 scope);
function latestRoot() view returns (uint256); // current ASP root
function assetConfig(address asset) view returns (pool, minimumDepositAmount, vettingFeeBPS, maxRelayFeeBPS);
// PrivacyPoolSimple (ETH)
function ragequit(RagequitProof proof); // original depositor only
function currentRoot() view returns (uint256); // state tree root
event Deposited(address indexed depositor, uint256 commitment, uint256 label, uint256 value, uint256 precommitmentHash);
event Withdrawn(address indexed processooor, uint256 value, uint256 spentNullifier, uint256 newCommitment);
event Ragequit(address indexed ragequitter, uint256 commitment, uint256 label, uint256 value);
A withdrawal proof must use one of the pool's last 64 state roots and the latest ASP root.
ASP contract
ZeroPrintASP is the only address with the Entrypoint's ASP postman role. Labels come from the pool's Deposited events.
function queue(uint256 label); // anyone: starts the window
function queueMany(uint256[] labels); // skips unknown or queued labels
function deny(uint256 label, string reason); // guardian only, inside the window
function approve(uint256[] labels) returns (uint256); // anyone, after the window; posts the root
function status(uint256 label) view returns (Status); // None, Waiting, Ready, Approved, Denied
function isApproved(uint256 label) view returns (bool);
function root() view returns (uint256);
function size() view returns (uint256);
function guardian() view returns (address); // address(0) once renounced
function WINDOW() view returns (uint256); // 1800 seconds
function transferGuardian(address to); // guardian only
function renounceGuardian(); // guardian only, for good
event Queued(uint256 indexed label, address indexed depositor, uint256 at);
event Denied(uint256 indexed label, string reason);
event Approved(uint256 indexed label, uint256 index);
event RootPosted(uint256 root, uint256 size);
The approved set is the Approved events in index order; rebuild the LeanIMT (Poseidon) from them and compare with Entrypoint.latestRoot(). The "CID" posted with each root is the root's own 64 hex digits, since the set lives on-chain. The keeper also serves read-only counts at GET /asp/stats.
Relayer API
The Privacy Pools relayer, configured to charge only its base fee (currently 0) and pay the gas itself. Base path /relayer.
| Endpoint | Purpose |
|---|---|
GET /relayer/details?chainId=&assetAddress= | Fee receiver, minimum withdrawal, max gas price. |
POST /relayer/quote | { chainId, amount, asset, recipient } → feeBPS and a signed feeCommitment (valid 60 s) whose withdrawalData you bind into the proof. |
POST /relayer/request | { withdrawal, publicSignals, proof, scope, chainId, feeCommitment } → { success, txHash } |
The relayer cannot change the recipient or the fee: both are inside the proof's context. At worst it can refuse; use another relayer or submit relay() yourself.
Fees & limits
| Protocol / vetting fee | 0% |
| Relayer fee | 0 (the relayer pays the gas); capped on-chain at 0.5% |
| Minimum deposit | 0.001 ETH |
| Minimum relayed withdrawal | 0.001 ETH |
| ASP window | 30 minutes, fixed in ZeroPrintASP |
| Asset | ETH only |
These parameters were set at deploy and cannot be changed: the owner role that could change them is renounced.
Verify a deployment
- Open the Entrypoint on the explorer and call
hasRole(keccak256("OWNER_ROLE"), deployer): it must befalse. - Check the
RoleRevokedevent from the deploy transaction. - Compare the verifier bytecode with the upstream build of
WithdrawalVerifierandCommitmentVerifier. - Read
ZeroPrintASP'sApprovedevents, rebuild the LeanIMT with Poseidon and compare withlatestRoot(). Check that only the ASP contract holdsASP_POSTMAN. - In the app, open DevTools → Network during a withdrawal: the only outgoing payload is the proof, amount and recipient. The Verify page runs the live checks for you.
Security
Trust model
| Party | Can | Cannot |
|---|---|---|
| ZeroPrint team | Run the keeper, relayer and website | Upgrade, pause or drain the contracts; see your keys |
| Guardian (keeper key) | Deny a deposit for private withdrawal, inside its 30-minute window | Approve, revoke, move or freeze funds; act after the window (ragequit always works) |
| Relayer | See proof, amount, recipient and your IP; refuse to relay | Change the recipient or fee; link you to a deposit |
| Website | Serve the app code | Read your Zero Key, unless the served code is malicious; see Risks |
| Sequencer | Delay or censor transactions | Forge proofs or steal funds |
Audits
- Privacy Pools core (contracts, circuits, Entrypoint upgrade): audited by Oxorio and Auditware. Reports are in the upstream repository's
audit/folder. - ZeroPrint additions (
ZeroPrintASP, deploy script, keeper, app, CLI): not audited yet. They are tested (contract tests, end-to-end runs, testnet and mainnet smoke tests), but start with small amounts.
Risks
- Smart contract or circuit bugs could lose funds. Mitigation: the pool is unmodified audited code;
ZeroPrintASPis small and tested but not audited. - Malicious front-end. A compromised website could steal Zero Keys. Mitigation: strict CSP, no third-party scripts, open-source code and a CLI that needs no website; reproducible builds and an IPFS mirror are planned.
- Losing your Zero Key loses your funds. Nobody can recover it.
- Small anonymity set. Privacy depends on how many others use the pool.
- Censorship. The sequencer could delay withdrawals; ragequit and direct contract calls remain.
- Regulatory risk. Rules about privacy tools differ by country and change.
Status
Live on Robinhood Chain mainnet since 20 October 2026.
- Unmodified audited pool contracts and circuits
- Owner renounced at deploy, verified on-chain; contracts verified on Sourcify
- On-chain ASP: 30-minute guardian window, OFAC + blocklist screening, approval by anyone
- Browser-side proofs, encrypted Zero Key, restore from 12 words
- Command-line tool that works without the website
- End-to-end tests, live testnet, mainnet smoke test
- Open-source code
- Independent audit of ZeroPrintASP, keeper and app
- Hardware or multisig guardian key
- More relayers and a relayer fee policy
- IPFS front-end mirror and reproducible builds
- Bug bounty
- Legal review for the jurisdictions served
Resources
Glossary
- Commitment
- A hash of your note (value, label, nullifier, secret) stored in the pool tree. Reveals nothing.
- Nullifier
- A secret whose hash is published when a note is spent, so it can't be spent twice. Unlinkable to the commitment.
- Label
- An ID tied to a deposit and inherited by its change notes; the ASP approves labels.
- Anonymity set
- The approved deposits your withdrawal could have come from.
- ASP
- Association Set Provider: publishes the set of approved (clean) deposits.
- Ragequit
- A public exit that returns a deposit to the wallet that made it.
- Relayer
- A service that submits your withdrawal and pays the gas, so the recipient needs no funding.
- Zero Key
- Your 12-word phrase that derives all your notes' secrets.
- Groth16
- The zero-knowledge proof system used; proofs are small and cheap to verify on-chain.
Legal & risk notice
ZeroPrint is experimental, open-source software provided as is, without warranty. It runs on Robinhood Chain mainnet and testnet. ZeroPrint is non-custodial: the team never holds user funds or keys. Do not use ZeroPrint to hide the proceeds of crime; deposits from sanctioned or blocklisted wallets are denied private withdrawal. You are responsible for complying with the laws that apply to you. Nothing here is financial or legal advice.
ZeroPrint is built on Privacy Pools by 0xbow, licensed under Apache-2.0.