manual PT-8086 · rev 5 · live on robinhood chain 4663 · read from the pool on 01 august 2026
How the machine works
This document covers what works and what does not. Anything unmeasured says so.
01The problem
An ordinary ERC-20 transfer writes three things to the chain forever: who sent, who received, how much. For payroll, for paying suppliers, or simply for not publishing your net worth to anyone who asks — that is a real problem.
8086P is a shielded pool: tokens sit together in one contract and ownership is recorded as cryptographic commitments. Moving value inside the pool emits no transfer at all.
02The note model
There is no balance table. Every amount is a note, like a paper bill — spending it consumes the whole thing and the remainder comes back as a new note.
Commitment
commitment = Poseidon(amount, pubkey, blinding, asset) — a hash stored in a 26-level Merkle tree on chain. It reveals neither the amount nor the owner.
Nullifier
nullifier = Poseidon(privkey, leafIndex, commitment) — published when a note is spent. The contract records it to prevent double-spending, but it cannot be traced back to the note it retired.
Encrypted notes
The sender encrypts the note payload to the recipient’s x25519 key (NaCl box with a single-use ephemeral key) and posts it on chain. Only the recipient can open it, and two notes sent to the same person cannot be linked to each other.
Tags
A note a wallet writes to itself — a deposit, the change from a payment — also carries an eight byte tag, SHA256(tagKey, commitment). The tag key is derived from the same private key and never leaves the browser, so to anybody else the tag is eight random bytes, and two notes of one owner carry unrelated tags.
It exists so a wallet can find its own money without opening every note in the pool. See staying usable.
03Pool addresses
A pt1… address is how somebody pays you inside the pool. It is 131 characters: the prefix, then 32 bytes of Poseidon public key — the one that goes inside the commitment — and 32 bytes of x25519 key, the one the note payload is encrypted to. One string, so a sender never has to be told there are two keys.
It is derived from your wallet’s signature over a single fixed message. The signature never leaves the tab, the private key is the hash of it, and the same wallet always derives the same pool address. Nothing is registered on chain and nothing is stored on a server: there is no pool address to create, only one to reveal.
it is not an EVM address
pt1… is not 0x…. It holds no ETH, it cannot receive an ordinary ERC-20 transfer, and sending anything to it outside this pool loses it. The only thing it does is receive notes.
04Paying somebody, and cashing out
These are two different things, and the app no longer lets them be confused. Paying somebody takes a pt1… address and nothing else — a wallet address is refused, because paying one means the pool paying it in the open, which names the person being paid. Cashing out is your own money leaving the pool: its own controls, beside the balance, where the screen says plainly that the transaction is public. It costs 3%, you choose how much of the balance goes, and the wallet it pays is yours by default — or any address you name, which is how you avoid one with a public history.
The contract itself still has both branches — the second is what a cash-out uses, and what anybody building on it directly can call. This is what the two look like on chain.
| paying pt1… (in the pool) | cashing out to 0x… (leaving it) | |
|---|---|---|
| publicAmount | 0 | the amount, snapped to a step |
| what the contract does | records 2 commitments and 2 nullifiers | the same, then calls transfer on USDG |
| ERC-20 movement | none — not one unit | pool → destination, in the open |
| fee | none, and none is possible | 3% |
| who signs it | the relayer, always | the relayer, or you if it is down |
| ties back to your deposit | there is no money movement to tie | yes — amount and time are public |
| what the recipient does | scans, decrypts, signs nothing | nothing; ordinary USDG arrives |
| where the app takes the address | the send field, pt1… only | the cash-out field, any wallet, empty means yours |
Why the private route is private
With publicAmount = 0 the pool returns before it touches a token at all. The transaction contains two commitments, two nullifiers and two encrypted blobs, and that is the whole of it — no address, no amount, no transfer that an observer could line up against the sender’s earlier deposit.
Measured, not assumed: 0x939c5a7f… on chain 4663 carried four proofs and zero ERC-20 Transfer events. Three of those four moved nothing at all, and nothing on chain says which three — see cover.
What is still public about it
If your shielded balance is short, the app deposits one step first, and that deposit is an ordinary transfer signed by your wallet. It says you put money into the pool. It does not say who you paid, how much of it, or whether you paid anybody at all — depositing to earn fees and depositing to pay somebody look identical.
a pool payment is never signed by its sender
The relayer posts it, so no wallet of yours appears in the transaction. Since 01 August 2026 the client refuses to sign one by hand when the relayer is down, rather than falling back. The fallback was measured on testnet: one internal transfer signed by a real wallet, one unshield seven minutes later from a wallet that had never deposited, and the pair joined from public data alone. Not sending is a delay; sending like that is permanent.
05Architecture
| layer | technology | responsibility |
|---|---|---|
| circuit | Circom 2.2.3 · Groth16 / bn254 | holds the money rules: value conservation, membership, nullifier binding |
| contract | Solidity 0.8.24 · Foundry | verify proof, record nullifiers, insert into tree, move ERC-20 only when publicAmount is not zero |
| relayer | Node · ethers v6 | signs every transaction, adds cover proofs, holds the queue |
| indexer | Node · ethers v6 | optional. Not run on this chain — the client rebuilds the tree from logs |
| sdk | circomlibjs · tweetnacl | keys, pool addresses, note encryption, tree — shared by web and server |
| client | snarkjs (wasm) | generate proofs IN the browser |
Five layers, one root
The Merkle tree is built in the circuit, the contract, the indexer, the test harness and the browser. One bit of divergence and every proof fails, for reasons that are painful to find. All five are byte-exact, checked by automated tests on anvil and on Sepolia.
Why proving happens on your machine
Proving needs your private key and the amount. Moving that to a server means handing the server your secrets — at which point the zero-knowledge part is theatre. The client downloads transfer.wasm and transfer_final.zkey and runs it locally.
06Fixed denominations
Deposit and withdrawal amounts are public — they have to be, the contract calls a real transfer. Allow arbitrary amounts and a pool with a thousand users still falls to one query: whoever deposited 137.42 is whoever withdrew 137.42.
So deposits and withdrawals are snapped to a fixed ladder of nine steps: 1 · 10 · 100 · 1,000 · 10,000 · 100,000 · 1,000,000 · 10,000,000 · 100,000,000 USDG. Nobody chooses that ladder, not even the deployer: registerToken(token) reads decimals() — six, for USDG — and derives the nine powers of ten from it. Registered once, never editable, never removable. Since there is nothing to abuse, the function is permissionless.
Payments inside the pool take any amount — that number is already hidden, and the change comes back as a note of whatever is left over.
Steps open in order. UNLOCK_AFTER = 10 deposits have to be sitting at a step before the one above it accepts anything, and it closes again when that depth falls. A step nobody is standing at is a step that hides nobody.
the trade-off
Each denomination is its own crowd. Someone depositing 100 cannot hide among people depositing 1,000,000. A long ladder exists to make real amounts usable, not to make you more private.
07Cover, not waiting
A proof carries the Merkle root it was built against as a public input, and the root is a function of how many leaves the tree holds. A note that has just been created therefore has exactly one root containing it: the one its own creating transaction published. Spend it before anything else lands and that root points at two leaves — the two the creating transaction wrote.
That is not theory. On an earlier pool of ours it chained three hops: a refund proved against a root naming an internal transfer, which named a deposit, which was signed in the open. Recipient to sender, from public data.
Cover proofs
The fix is more leaves in the same transaction, and they do not have to belong to anybody. The relayer builds extra proofs with publicAmount = 0, keys generated in the process and thrown away — nobody can spend or open them, including the relayer a second later. They carry no tag, and the encrypted blobs are the same uniform 141 bytes a real payment writes. By default COVER_PAD = 4 proofs travel in one transaction and the real ones are shuffled into the set.
On chain a cover proof is indistinguishable from a real payment: same two commitments, same two nullifiers, same absence of any ERC-20 movement. It costs one proof and about 1.25M gas and no capital at all, which is why this pool is deployed with BURY_UNDER = 0 — read from the contract, not from a deployment record. There is no burial wait here.
what cover is not
Cover stops a root from naming two specific leaves. It does not manufacture a stranger who might have sent the payment. Anonymity still needs real users, and nothing here pretends otherwise.
The queue, for withdrawals
Payments inside the pool go out as soon as they are proved. A withdrawal is held: the relayer releases it when the step it spends holds SAFE_DEPTH = 10 deposits belonging to other people — the sender’s own are subtracted, because a crowd made of one person is not a crowd. Withdrawals then leave in groups, one denomination per transaction, up to ten at a time and at most half an hour of waiting for company. On a pool this young that wait can be long, and the panel says which wait you are in.
Nothing is trapped while it waits. An unused deposit can be taken back at any moment, free, and refundBatch does ten of them in one confirmation.
08The relayer
Someone has to sign and pay gas. If the user signs, their wallet appears right beside the transaction — next to a withdrawal it pairs up with their deposit, and next to a pool payment it names the sender outright. The relayer signs instead, and because one relayer serves everyone the From field says nothing.
| address | 0x7a36EcBf…cCc7Ce ↗ |
| endpoint | https://relay-rh.8086protocol.xyz |
| serves | this pool, this token, on chain 4663 |
| its cut | 0.01 USDG minimum, taken out of the 3% — never on top |
It is not trusted with money
The destination address is bound into the proof through extDataHash — change it and the contract reverts. On a pool payment both outputs are fixed by the proof and the contract refuses a fee at all when publicAmount is zero, so there is not a unit in it to take. It never sees keys, balances or anyone’s notes. All it can do is refuse service or delay.
What it does see: your IP, and the (recipient, amount) pair of each withdrawal. A pool payment shows it two blobs and no identity. It is one operator, run by us, which is a central point — several independent ones would be better and there is one.
If it is down, a withdrawal falls back to you signing and the screen says you are now exposed. A pool payment does not fall back at all: it refuses, and nothing is spent.
09What it costs
| action | rate | who receives it |
|---|---|---|
| deposit into the pool | 0 | — |
| undo your own deposit, nobody answered your call | 0% | — |
| undo your own deposit, somebody answered | 1% | half to whoever answered, half to the project |
| recipient withdraws to a wallet | 3% | the relayer takes its fee out of this; the rest splits half to the people supplying depth, half to the project |
| opening a call bond | 0 | returned in full if nobody answers |
| paying a pool address | none possible | the contract reverts if a fee is set |
The hard cap is MAX_PROTOCOL_FEE_BPS = 1000, ten per cent, a constant in the bytecode. There is no proxy here, so the owner of the pool cannot exceed it, and today the rate is protocolFeeBps = 300.
Two of those rows are the whole reason the fee table looks odd. Undoing your own unused deposit is free and immediate — somebody who deposited by mistake owes nobody anything. The 1% is charged only when somebody else spent gas and locked capital answering your call for depth, and it is paid to them, not banked as revenue.
Calls and bonds
A step that is closed can be asked for. openCall does it in public and writes your wallet into an event, which tells a watcher that this wallet wants to move more than the open step. A call bond does the same thing without naming anyone: a proof spends one note of tier / 100 into a slot keyed by keccak256(secret), relayed like anything else, and the withdrawal that eventually goes out carries the secret and takes the bond back with it. If the step never got deeper, the bond returns in full; if it did and the caller walked away, after BOND_TTL = 1 day anyone may forfeit it to the people who answered.
10Hidden vs visible
Hidden
- Everything about a payment to a pool address — no addresses, no amounts, no ERC-20 transfer, no wallet signing it
- Which proofs in a transaction moved money and which were cover
- Ownership of each note — only the keyholder can open it
- Balances — there is no balanceOf, the client computes its own
- The link between two notes sent to the same person
- Which deposit paid for a withdrawal
Still visible
- That you used this pool — a deposit is an ordinary transfer from your wallet
- Every withdrawal: the destination wallet and the amount
- Timing, for withdrawals — a deposit and a withdrawal minutes apart are linkable, which is what the queue exists to slow down
- A thin pool falls to statistics, not to broken cryptography
What real anonymity requires
- A crowd. A pool of one is transparent no matter the maths. This cannot be coded.
- Staying inside. Money that is paid on as notes never crosses the visible edge.
- Fixed denominations. Done.
- A relayer. Done, and it is one operator.
Cryptography is the necessary half. Those four are the sufficient half.
11What an observer actually sees
The most common misunderstanding: people expect a withdrawal’s destination to be hidden. It is not, and it cannot be. To pay a normal wallet the pool has to call a real ERC-20 transfer, and a transfer names the address it pays. Any system that moves money to an ordinary address publishes that address.
For a withdrawal, both ends are public and the link between them is what is hidden. For a payment to a pool address there are no ends to see.
A real pool payment, decoded
An actual transaction on chain 4663, with real USDG: 0x939c5a7f…. Anyone can read it.
| field | value |
|---|---|
| signed by | the relayer, not the sender |
| called | the pool contract |
| proofs inside | 4 — one payment, three cover, in no stated order |
| ERC-20 Transfer events | 0 |
| addresses named | none but the pool and the relayer |
| fee taken | none — the contract refuses one here |
An observer learns that the relayer posted four proofs. Not who paid, not who was paid, not how much, and not whether any of the four moved a unit of anything.
A withdrawal, decoded
The other shape, and the one that leaks: the relayer signs, the pool pays the destination 97% of a step, the relayer takes its fee out of the remaining 3%, and the tree grows by two leaves. The destination and the amount are public forever. What stays hidden is which deposit paid for it — the sender’s wallet appears nowhere in the transaction.
Hiding the recipient too
There is exactly one way: the recipient never withdraws. They hold the money as a note inside the pool, addressed to their pt1… pool address, and spend it on to other pool addresses. No transfer is emitted, so no address is published. The moment they cash out to an ordinary wallet, that wallet becomes visible.
true of every shielded pool
Every shielded pool shares the same boundary. Inside the shielded set nothing is visible. Crossing the edge is visible by construction. A tool that claims otherwise is either lying or is not settling on a public chain.
12Being harder to trace
The maths does its part on its own. These are the parts it cannot do for you — and in the one real trace run against this pool, every link that was found came from this list, not from the cryptography.
1 · Ask for a pool address
A payment to a pt1… address emits no transfer, names nobody and costs nothing. A payment that ends at a wallet is public whatever else you do — so the single largest thing you can do is be paid, and pay, inside the pool. The app refuses a wallet address in the send field for this reason.
2 · Cash out to a wallet with no public history with the payer
This is the one that actually broke a trace. A withdrawal was followed back to its source not through the pool but because the paying wallet had sent USDG directly to the receiving wallet four days earlier, in the open, before the pool existed. The pool hid the payment perfectly; the two wallets were already joined in public.
A withdrawal is relayed, so the receiving wallet signs nothing and needs no gas. It can be an address that has never appeared on chain at all. Funding it first — especially from the wallet that is paying you — undoes the whole thing.
3 · Take it out in pieces
A single withdrawal equal to what you were paid is an amount that matches, at a moment that follows. Cash out part of it, and the rest later: the app has an amount field beside the balance for exactly this. Each withdrawal spends one note and the remainder returns to the pool as change, so a large note pays out one step per round.
do not withdraw the total you were paid
Paid 100 and withdrawing 100 is a matching pair even when every transaction between them is invisible. Withdraw a round number, on a step other people are using, and leave the remainder inside — the pool has no balanceOf, so what stays is unreadable.
4 · Wait, and let the queue do it
Timing is what pairs a deposit with a withdrawal. Withdrawals are parked with the relayer and go out together when the step has company, which is why the button does not fire instantly. Pressing it later, rather than the minute the money arrives, costs nothing and helps more than any parameter in the contract.
5 · Crowd into as few steps as possible
Each denomination is its own crowd. Somebody depositing 100 does not blend with somebody depositing 1,000. Read from the pool on 01 August 2026: the 1 step held 13 deposits, the 10 step held zero. Ten separate deposits of 1 hide better than one deposit of 10 does today — and the answer changes as the pool fills, so read the depths on the send screen rather than assuming.
6 · Pay amounts the recipient can get out
Inside the pool any amount is equally private — the note payload is a fixed 60 bytes whatever it holds, so 3 hides exactly as well as 10. But money can only leave on a step. Pay 3.5 and the recipient can take out 3 and never the 0.5: there is no step below 1, and nothing below it can ever be withdrawn. Pay in whole units unless the remainder is meant to stay inside.
7 · Do not reuse the exit wallet
One wallet that receives every withdrawal becomes the thing worth watching, and joins together payments that had nothing else in common. A fresh address per withdrawal costs nothing here, because the recipient never signs.
check a pool address character by character
A pt1… address has no checksum. The app rejects a wrong length or a character that is not hex, and it cannot do more: a typo that is still valid hex is a real address whose key nobody holds, and money sent there is gone permanently. A wallet address has a checksum; this does not.
What none of this fixes
- That you used the pool — a deposit is an ordinary transfer signed by your wallet
- A thin crowd: at 13 deposits, statistics beat all seven of the rules above
- One relayer, which sees every proof it is handed and could log what it likes
13Where it runs
One chain, one pool, one token. Every value below was read from the chain itself on 01 August 2026 — the parameters by calling the pool, the balance by calling the token, not from any deployment record.
| Robinhood Chain | |
|---|---|
| chain id | 4663 · Arbitrum Orbit L2 · native ETH |
| explorer | robinhoodchain.blockscout.com |
| asset | USDG · 6 decimals · 0x5fc5360D…F1d168 ↗ |
| pool | ours, v3 · 0x60552713…161cE5 ↗ |
| deployed at block | 25,760,790 |
| fee | 3% on withdrawals, nothing else |
| denominations | 9 steps · 1 → 100,000,000 USDG |
| burial | BURY_UNDER = 0 — cover proofs do that job |
| steps open after | UNLOCK_AFTER = 10 deposits at the step below |
| undoing your own deposit | free, immediate, batched, for 7 days |
| who signs | our relayer · 0x7a36EcBf…cCc7Ce ↗ |
| held in the pool | 13.02 USDG · 50 leaves in the tree |
| status | live |
the token is not ours, and it can freeze this pool
USDG is an ERC-1967 proxy. Its issuer can replace the implementation, and it exposes isFrozen(address). The pool is an address. Freezing it does not strand one person’s note — it stops every transfer out, so everybody sits inside a contract that cannot pay. No version of the pool can fix that: the pool holds a token whose issuer kept the switch.
History, not a choice you have. Earlier builds ran on Base with Circle USDC, and one build rented Railgun’s shared pool on Arbitrum. Both are off the picker and nothing in this manual describes them. Notes left in retired pools stay reachable through the withdraw-only panels the app keeps pointed at each of them, because there is no proxy here and nobody but a note’s owner can ever spend it.
14Staying usable as the pool fills
Every transaction writes two leaves into the tree, and the tree only ever grows: a commitment cannot be removed without invalidating every proof already built against it. So the honest question is not how to shrink it but what a client has to do per note, and the answer used to be: rebuild the whole tree, then try to open every note in it.
| per note, measured | before | now |
|---|---|---|
| insert into the tree | 1.294 ms | 0.051 ms in batches, or none at all |
| find out if it is yours | 0.339 ms | one SHA-256, if you wrote it |
| a pool of a million notes | about 27 minutes | seconds |
At 50 leaves none of this is felt. It is here because the cost per note is what decides whether the app still works at a million of them, and that is a decision made long before it hurts.
The tree, borrowed and checked
Where an indexer runs, the client asks it for the Merkle path of one leaf, hashes that path back up to a root, and asks the POOL whether it ever published that root — isKnownRoot. A forged path lands on a root the contract never knew and is thrown away, so the indexer is a convenience that cannot lie. No indexer runs on this chain, so the client builds the tree itself from logs, which is the fallback path and is fast at this size.
Your own notes, found by tag
Opening a note costs an x25519 shared secret, and 99.5% of scanning is exactly that. No view tag fixes it, because computing a view tag needs the same shared secret. What does fix it: nearly every note a wallet must find was written by that wallet, so no key exchange is needed at all — the tag in the note model is checked with one hash.
A note somebody else sent you carries no tag you could compute, so those are still opened one by one. That set is exactly the payments made to your pool address, and it is the reason the deep scan still exists.
measured in a browser, not in theory
4,001 notes, 41 of them the wallet’s own: the tag pass found 40 in 78 ms and the deep pass found the remaining one in 1 ms. The old full scan found the same 41 in 3,214 ms. Forty times faster, and the same money.
What is left
The tree cap is 226 leaves, 67 million, or 33.5 million transactions. Reaching it does not mean a new pool: this build answers a full tree by opening the next one inside the same contract instead of freezing every function.
15Measurements
Measured on real hardware. Nothing in this table is an estimate; where a figure has not been taken on this chain, it says so rather than borrowing one.
| item | value |
|---|---|
| constraints (total) | 32,301 |
| public inputs | 8 |
| proof generation (Node CLI) | 1.14 s |
| proof generation (browser) | not measured yet |
| gas to verify a proof | 235,455 |
| gas per transaction (Sepolia) | 1,340,656 – 1,390,268 |
| gas per transaction (chain 4663) | not measured yet |
| gas to deploy the pool | 3,656,193 |
| tree insert, one leaf at a time | 1.294 ms |
| tree insert, in batches | 0.051 ms |
| restore a saved tree | 1.5 s per million leaves |
| scan a note by tag | 0.007 ms |
| scan a note by opening it | 0.360 ms |
| leaves in this pool | 50 |
| USDG held by this pool | 13.02 |
Verification is only 235k of that. The bulk is 26 on-chain Poseidon hashes to insert into the tree — which is why a cheap chain matters more than a smaller circuit, and why four proofs in one transaction is affordable here at all.
16Known limits
live · unaudited · one-contributor setup
The phase-2 trusted setup currently has a single contributor. Whoever holds that file can forge proofs and drain the pool, and right now that is whoever ran the ceremony. The deploy script prints this warning and then deploys anyway — it does not refuse. Funding a pool you run yourself is one thing; inviting strangers in before the ceremony is redone with independent participants is another.
- The crowd is small, and this is a new pool. Read from the contract on 01 August 2026: 13.02 USDG inside, 50 leaves in the tree. Cover proofs fix the root-naming attack; they do not conjure up depositors, and no shielded pool can hide you in a crowd this size. Treat anything you would be upset to see traced as not yet suited to this pool.
- The token can freeze everything. USDG is an upgradeable ERC-1967 proxy with isFrozen(address). If its issuer freezes the pool address, no withdrawal succeeds for anybody until it is unfrozen. This is not a property of the pool and no version of the pool can fix it.
- Withdrawal amounts still match. A withdrawal pays out 97% of a step, so what a destination receives is 0.97 x what somebody deposited. Split a send across days, or across destinations, if that ratio matters to you. Paying a pool address avoids the equation entirely, because nothing moves.
- One relayer, run by us. It cannot steal or redirect — the destination is bound into the proof, and an internal transfer cannot be charged a fee at all — but it sees every withdrawal recipient and every IP, and it can refuse or stall. When it is off, pool payments do not go at all, by design.
- A withdrawal still waits for SAFE_DEPTH = 10 deposits from other people at its step. Cover proofs cannot supply those: they move no money, so they are not deposits. On a quiet pool that wait is real.
- Refund credit is counted per wallet, not per note. A wallet that has deposited once keeps a credit even after that deposit has left by the private route, and could use it to refund a note somebody else sent it at 0% instead of paying 3%. The client blocks it — only notes found by tag, meaning notes the wallet wrote itself, can be refunded — but a hand-built blob is not blocked. The real fix needs a new circuit, a new trusted setup and a new verifier.
- Six-decimal stablecoins only. ETH and WETH do not fit, because the circuit caps amounts below 2^64 and with 18 decimals the smallest denomination would be a whole ETH.
- ERC-8086 is still a Draft — the interface can change.
- No viewing keys, so no selective disclosure. That is the standard's choice, and a genuine regulatory obstacle.
- A malformed proof burns a lot of gas: the verifier correctly returns false, but the failing bn254 precompile consumes the forwarded gas. Not fully investigated.
17The contracts
Three contracts of ours on chain, and only the first holds money. There is no proxy and no admin key that can change any of them: what is deployed is what runs, and replacing a pool means deploying a new one and leaving the old one reachable. The fourth line below is the token, which is not ours and does not share that property.
| contract | on chain 4663 | what it does |
|---|---|---|
| PrivacyTokenV2 | 0x60552713…161cE5 ↗ | the pool. Holds every deposit, verifies proofs, records nullifiers, inserts leaves, enforces the ladder, and pays out when publicAmount is not zero. |
| Groth16Verifier | 0x2D57a33E…e5aF2c ↗ | generated by snarkJS from the circuit, never hand written. Checks a proof against the verifying key and returns true or false, nothing else. Read back from the pool's verifier(). |
| PoseidonT3 | 0x5343a2e9…7f6e72 ↗ | the hash, deployed from circomlibjs bytecode. Used for commitments, nullifiers and every node of the tree, so the contract and the circuit agree bit for bit. Read back from the pool's hasher(). |
| USDG | 0x5fc5360D…F1d168 ↗ | the asset. NOT ours: an ERC-1967 proxy whose issuer can upgrade it and can freeze an address, this pool included. |
| IZRC20 | interface only | the ERC-8086 interface the pool implements. Nothing is deployed for it; it exists so other tools can talk to the pool without reading its source. |
What the pool contract actually enforces
- The denomination ladder, derived from decimals() at registerToken and never editable afterwards, by anybody, including the deployer.
- No burial wait. BURY_UNDER = 0 on this pool, because cover proofs do what burial was standing in for.
- The step ladder opening in order. UNLOCK_AFTER = 10 deposits at a step before the one above it opens, and it closes again when depth falls.
- One nullifier, once. A note cannot be spent twice and the contract cannot be talked out of that.
- No fee on a private payment. FeeNotAllowed reverts the transaction if a fee is set while publicAmount is zero.
- The fee cap. MAX_PROTOCOL_FEE_BPS = 1000 is a constant in the bytecode; protocolFeeBps is 300 today and cannot be raised past ten per cent.
- The fee split. Half of the protocol fee is credited to depth holders through an accumulator at the moment a withdrawal lands, so depositing afterwards earns nothing from it.
- Refunds. An unused deposit can be taken back for REFUND_WINDOW = 7 days, free unless somebody answered your call, and refundBatch does ten of them in one transaction.
- A deposit earns fees only while it is still in the pool. Collecting them takes a proof that the note has not been spent, so a wallet that withdrew or paid its deposit away earns nothing from that moment — with nobody having to notice or clean up.
- Call bonds. BOND_TTL = 1 day, the bond is tier/100 and must itself be a registered denomination, and the key is a hash rather than an address.
no upgrade path, by design
None of the three can be upgraded, paused or drained by us. That is why a fix means a new address rather than a patch: the old pool keeps working for whoever still has notes in it, and the app keeps a withdraw-only panel pointed at each one. The exception is the token, and it is not ours.
18Using it
What you need
- A browser wallet on chain 4663. The proof is generated in the page, so a wallet that only signs is enough; nothing is installed.
- One signature, to derive your pool key and your pt1 address. It is asked for once per session, never leaves the tab, and the same wallet always derives the same address.
- A little native ETH, for the deposit only. Everything after it is signed and paid for by the relayer.
To be paid privately
| step | what to do | what to expect |
|---|---|---|
| 1 | connect and sign once | your pt1 address appears; it is yours for good |
| 2 | give that pt1 address to whoever is paying you | 131 characters, and not an EVM address — it holds no ETH |
| 3 | leave and come back | the note is found by scanning; you sign nothing to receive it |
| 4 | spend it on to another pt1 address, or withdraw | spending on emits no transfer; withdrawing pays 3% and names the destination |
To pay someone
- Paste their pt1 address for the private route, or a 0x wallet to withdraw. The panel says which one it is doing before you confirm.
- If your shielded balance is short, the app deposits one step first — that is one wallet confirmation, and it is the only public part.
- Send a small amount first. Not because the contract differs from testnet, but because it is your money and the first run of anything deserves to be cheap.
- Read the pool figures before a transfer you care about. A step with almost nobody in it hides almost nobody, and no part of the design pretends otherwise.
The Sepolia testnet page is still there, with free test money and the same contract. The waits are real on it, so it is where to find out what the screen says while a transfer is happening, before any of it costs anything.
if it looks stuck
A withdrawal that has not moved is waiting for company at its step, and the panel says so. Nothing is lost while it waits: the notes are yours, and take it back returns an unused deposit to your wallet immediately and free, however thin the step is. That button exists precisely so that waiting is never the same as being trapped.