noircashdocs

Integrations

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

NOIR 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. NOIR sent into the vault is held there against encrypted notes; NOIR taken out is ordinary NOIR again. Integrators only need the vault if they show private balances, index private activity, or count circulating supply.

Addresses are on Deployments.

Trading NOIR

NOIR trades on its Pons bonding curve until the curve has collected 4.2 ETH, then on the Pons Uniswap v4 pool (native ETH / NOIR, 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 noircash-specific is needed to buy or sell NOIR.
  • Fees are Pons fees. Every trade pays Pons' 1% standard fee, on the curve and on the pool alike; NOIR has no creator tax. The creator share is paid in ETH to the noircash FeeHarvester, which turns it into NOIR for private holders. See Fees.
  • Only the Pons pool counts. Only the Pons factory can initialize a pool with its hook. Other ETH/NOIR pools can exist with another fee, tick spacing or hook; they have another pool id, and noircash 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, NOIR out. isBuy = false: NOIR 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 NOIR, which the holder can shield later.

Wallets and exchanges

A NOIR holder can have two balances:

BalanceHow to read itWho can read it
PublicNOIR.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: NOIR 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 NOIR 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 noircash, symbol NOIR, decimals 18.

The private side

Supply

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

  • totalBacking: NOIR behind all notes, including the seed note locked at deployment, which nobody can spend;
  • gasReserve: NOIR collected as network fees, waiting for the harvester;
  • pendingSaleTotal: NOIR of ERC-4337 sales validated but not yet settled (pendingSales);
  • NOIR 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 NOIR is private. Emitted after every vault change
Donation(from, amount)NOIR given to private holders: harvested creator fees and surplus gas fees from the harvester, and NOIR 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)NOIR 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)NOIR leaving the vault: unshields and router sales
SaleSettled(saleId, swapped)ERC-4337 sales; swapped = false means the NOIR went to the recipient instead
GasFeeCollected(amount)Network fees paid in NOIR 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, NOIR 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 NOIR with the creator fees it has collected and donates it to the vault (Harvested on the harvester, Donation on the vault).

On this page