Public ZERO and shielding
ZERO outside the vault is an ordinary ERC-20, how shielding and unshielding move it in and out of notes, and what stays visible on-chain.
ZERO (0x316fa3AB9A8FD8d7567a823DedecF28d9FEE2894) is a plain ERC-20 created by Pons: OpenZeppelin ERC20 and ERC20Burnable, no owner and no
transfer restrictions. Any address can hold, send and receive it, and wallets, exchanges, routers and aggregators
handle it like any other token. It has no privacy of its own. Privacy starts when ZERO is shielded into the vault
and ends when it is unshielded back out.
Public ZERO
ZERO held by an address outside the vault is public ZERO. Its balance and every transfer are visible, as for any ERC-20. Holding public ZERO earns nothing from the vault: only notes hold shares (see The vault).
Registering privacy keys does not change public ZERO: a registered address receives ZERO like any other address, and
its balanceOf is its ordinary ERC-20 balance. ZERO becomes private only when its holder shields it.
| Where public ZERO comes from | Example |
|---|---|
A purchase anywhere other than PrivateRouter.buy | The Pons site, the curve or pool directly, an aggregator |
| A transfer from another address | An exchange withdrawal, a payment |
| An unshield | transact with an exit to an address |
| A sale that could not be completed | The vault sends the ZERO to the sale's ETH recipient |
Shielding
function shield(uint256 stub, uint256 amount, bytes calldata ciphertext) external returns (uint32 index);The holder's app creates a note with random rho and r, computes stub = H(opk, rho, r) and encrypts
(rho, r, 0) to the holder's viewing key.
The holder approves the vault for amount on ZERO (approve(vault, amount)).
The holder calls vault.shield(stub, amount, ciphertext). The vault pulls amount with transferFrom, mints
convertToShares(amount) shares, inserts cm = H(1, stub, shares) into the tree and emits
Shielded(from, index, amount, shares) and NoteAdded(index, cm, ciphertext).
The app's shield sends the approval first, only if the current allowance is short, then the shield, both from the
holder's wallet. PrivateRouter.buy
does the same in one transaction for ETH: it buys ZERO, approves the vault and shields for the buyer (see
The market).
Unshielding
An unshield is a transact whose exit goes to an address: exitShares leave the private side and
convertToAssets(exitShares - gasFee) ZERO, rounded as described in The vault, are
transferred to ext.recipient. The vault emits Exited(nullifier, recipient, amount, shares). The recipient can be
any address, including one that never used zerotrace. From there it is public ZERO again.
A private sale is the same exit with the router as recipient: the router sells the ZERO and sends ETH instead. See Gas and ERC-4337.
What is visible on-chain
| Information | Public? |
|---|---|
| ZERO's total supply, public balances and public transfers | Yes |
| Trades on the curve and on the pool: amounts, price, time, trader | Yes |
| B, S, the gas reserve, the price average | Yes |
| Registered addresses and their public keys | Yes |
| A shield's caller, amount, shares and leaf | Yes (Shielded) |
| A private buy's buyer, ETH amount and ZERO amount | Yes (router Bought, and the curve or pool trade) |
| An exit's recipient and amount; a sale's ETH recipient | Yes (Exited, SaleSettled, router Sold) |
| Which notes a spend consumed | No |
| Sender, recipient and amount of a private send | No |
| A person's private balance | No |
| Whether a shielded note has been spent | No |
Entries into and exits from the private side are public; what happens in between is not. Timing and amounts at entry and exit can still link them when few people use the vault. See the threat model.
Architecture
The contracts and off-chain parts of zerotrace, who runs each one, and how a purchase, a private send and a private sale move through them.
Notes, commitments and nullifiers
What a note contains, how its commitment and nullifier are computed, how notes are encrypted, and how shielded notes get their shares.