zerotracedocs

Integrations

What aggregators, wallets, exchanges, explorers and analytics tools need to know to support ZERO and the private side of zerotrace.

ZERO is an ordinary ERC-20. It is a Pons V2 token (OpenZeppelin ERC20 and ERC20Burnable, 18 decimals, 1,000,000,000 supply), with no owner, no hook of its own, no transfer rules and no special balance logic. Holding, sending, trading, listing, bridging and custody need no special handling.

The private side is a separate contract, the PrivateVault, that holders opt into. ZERO sent into the vault is held there against encrypted notes; ZERO taken out is ordinary ZERO again. Integrators only need the vault if they show private balances, index private activity, or count circulating supply.

Addresses are on Deployments.

Trading ZERO

ZERO trades on its Pons bonding curve until the curve has collected 4.2 ETH, then on the Pons Uniswap v4 pool (native ETH / ZERO, fee 0, tick spacing 200, the Pons meme hook). Both are standard Pons venues:

  • Aggregators already route them. 0x and KyberSwap route Pons V2 tokens on Robinhood Chain, on the curve and on graduated pools. Nothing zerotrace-specific is needed to buy or sell ZERO.
  • Fees are Pons fees. Every trade pays Pons' 1% standard fee, on the curve and on the pool alike; ZERO has no creator tax. The creator share is paid in ETH to the zerotrace FeeHarvester, which turns it into ZERO for private holders. See Fees.
  • Only the Pons pool counts. Only the Pons factory can initialize a pool with its hook. Other ETH/ZERO pools can exist with another fee, tick spacing or hook; they have another pool id, and zerotrace ignores them for trading and pricing.

Quoting and private buys

PrivateRouter.quote gives the output of a trade on whichever venue is live, Pons fees included:

function quote(bool isBuy, uint256 amountIn) external returns (uint256 amountOut);

isBuy = true: ETH in, ZERO out. isBuy = false: ZERO in, ETH out. On the pool it runs the real swap and reverts it, so it is not a view; call it with eth_call:

import { parseAbi, parseEther } from "viem";

const router = "0x…"; // PrivateRouter, see Deployments
const abi = parseAbi(["function quote(bool isBuy, uint256 amountIn) returns (uint256 amountOut)"]);

const { result: tokensOut } = await publicClient.simulateContract({
  address: router,
  abi,
  functionName: "quote",
  args: [true, parseEther("0.01")],
});

PrivateRouter.buy buys straight into a private note in one transaction. An aggregator that wants to offer that needs the buyer's note stub and ciphertext, which only the buyer's keys can make; see Notes. Any other purchase delivers ordinary ZERO, which the holder can shield later.

Wallets and exchanges

A ZERO holder can have two balances:

BalanceHow to read itWho can read it
PublicZERO.balanceOf(address)Anyone
Private (notes)Rebuild the note tree from the vault's events and decrypt the notes with the user's viewing keyOnly the owner

A standard token integration shows the public balance correctly. Registering privacy keys does not change it: ZERO only becomes private when the holder shields it (approve the vault, then shield), and becomes public again when they unshield or sell privately to an address.

To show the private balance, a wallet must derive the user's privacy keys (one signature) and scan the notes. See The note tree and Your balance.

The keys that read the private balance come from a signature over a Sign-In with Ethereum message naming one domain and the vault. Do not ask users to sign another site's key-derivation message: the result reveals all their notes. See Keys.

Deposits and withdrawals of an exchange are plain ZERO transfers. An unshield or a private sale to an exchange deposit address arrives as a normal Transfer from the vault (for an unshield) or ETH from the router (for a sale).

Token metadata: name zerotrace, symbol ZERO, decimals 18.

The private side

Supply

ZERO in the vault is not circulating. It backs the notes, and only the holder of a note can take it out. ZERO.balanceOf(vault) is the sum of:

  • totalBacking: ZERO behind all notes, including the seed note locked at deployment, which nobody can spend;
  • gasReserve: ZERO collected as network fees, waiting for the harvester;
  • pendingSaleTotal: ZERO of ERC-4337 sales validated but not yet settled (pendingSales);
  • ZERO sent to the vault by plain transfer, until the next absorb adds it to totalBacking.

The share of supply that is private is totalBacking / totalSupply.

Events

All on the vault unless noted.

EventUse it for
VaultUpdated(totalBacking, totalShares)The share value (B + 1) / (S + 1e6) over time, and how much ZERO is private. Emitted after every vault change
Donation(from, amount)ZERO given to private holders: harvested creator fees and surplus gas fees from the harvester, and ZERO sent by plain transfer from the zero address (absorb)
NoteAdded(index, commitment, ciphertext)Tree size: the set every private balance hides among
Shielded(from, index, amount, shares)ZERO entering the vault: from is the user, or the router for a private purchase
NullifierSpent(nullifier)Private transactions: two per transaction
Exited(nullifier, recipient, amount, shares)ZERO leaving the vault: unshields and router sales
SaleSettled(saleId, swapped)ERC-4337 sales; swapped = false means the ZERO went to the recipient instead
GasFeeCollected(amount)Network fees paid in ZERO by private transactions
PriceUpdated(tokensPerEthEma)The vault's price average
KeysRegistered(owner, opk, viewKey)Addresses that can receive private notes
Bought, Sold on the routerPrivate purchases and sales
Harvested, ReserveSold, ReserveDonated, CallerTipped on the harvesterFee harvests, gas deposit top-ups and reserve donations

The app computes its headline numbers (notes, private transactions, share of supply that is private, holder yield, ZERO distributed, price, trade and holder fee rates, gas fund) from these events and a few reads.

A private transaction's public inputs are the root, two nullifiers, two new commitments, the exit shares, extDataHash(ext) and the vault's EIP-712 domain separator in two 128-bit halves. publicInputs(t) on the vault returns them in circuit order. See Contracts.

What not to assume

  • The ERC-4337 sender is not the user. Private transactions are UserOperations whose sender is the vault, submitted by a bundler. The EntryPoint's UserOperationEvent names the vault for every one of them.
  • A transfer into the vault is not a sale or a burn. It is a shield (with Shielded) or a donation (with Donation). A plain transfer to the vault is counted as a donation by the next absorb.
  • A transfer out of the vault is an exit. An unshield or sale (with Exited), a settled or claimed ERC-4337 sale (with SaleSettled), or the gas reserve going to the harvester.
  • Holder counts miss private holders. A note does not show its owner or amount, so everything in the vault counts as one address.
  • Harvester buys come from fees. Whenever someone calls harvest, the harvester buys ZERO with the creator fees it has collected and donates it to the vault (Harvested on the harvester, Donation on the vault).

On this page