noircashdocs

Architecture

The contracts and off-chain parts of noircash, who runs each one, and how a purchase, a private send and a private sale move through them.

NOIR is an ordinary ERC-20 launched on the Pons V2 launchpad. noircash is the private side around it: a vault that holds NOIR behind encrypted notes, a router that trades on NOIR's market and shields or sells in the same transaction, and a fee harvester that turns the Pons creator fees into gas and holder yield. The vault is also its own ERC-4337 account. Everything else can be run by anyone (bundlers, the indexer, the app).

Components

ComponentOn-chain / off-chainWho runs itWhat it can do
NOIR (0xDa2E4659dA030AEd0285F9Ed3496ec07058b2319)On-chain, Pons contractNobody (no owner)A Pons PonsV2LauncherToken: OpenZeppelin ERC20 + ERC20Burnable, 1,000,000,000 supply minted to its bonding curve, no transfer restrictions
Pons bonding curve (0x7D1e90200654C2BE3e3a0eC035bC7Fa3C55f026f)On-chain, Pons contractPonsSells and buys NOIR for ETH until graduation, and holds the fees it charges until they are swept
Pons factory, meme hook, fee escrowOn-chain, Pons contractsPonsGraduate NOIR into its Uniswap v4 pool, charge the fees on that pool, and pay the creator share into the escrow
PrivateVault (0x0a55A88524002dd8F053Ffc644015EDc9653963a)On-chain, immutableNobody (no owner)Holds the NOIR backing all notes, the key registry, the note tree and nullifier set, the vault totals B and S, the gas reserve and the price average. Verifies every spend. Acts as the ERC-4337 account for private transactions
PrivateRouter (0x00fe9533Df78aF8e59cF8BC0EcDDe551140920D5)On-chain, immutableNobodybuy: trades ETH for NOIR and shields it into a note in one transaction. sell: spends notes and sells them for ETH. Settles ERC-4337 sales. quote: prices a trade. Holds no funds between transactions
FeeHarvester (0x76d4979272019475b7eD690d5943E14296bDcDd6)On-chain, immutableAnyone may call harvestNOIR's Pons creator fee recipient. Claims the ETH fees, keeps the vault's EntryPoint deposit funded, buys NOIR with the rest and donates it to the vault
Verifier (HonkVerifier, 0xeA05c34fF6a60EBF6E73544E449B0DE7095C7941)On-chain, immutableNobodyChecks UltraHonk proofs of the transact circuit. Generated by Barretenberg
Poseidon2 hasher (0x33E32aa162d637F149924ab025DAcA55E1571842)On-chain, immutableNobodyPoseidon2 over BN254 in Yul, kept outside the vault for contract size. FallbackHasher (Stylus first, Yul as backup) exists as an option; deployments use the Yul hasher
EntryPoint v0.8On-chain, canonicalNobody (ERC-4337 standard contract)Calls the vault's validateUserOp and execute4337, pays bundlers from the vault's deposit
BundlerOff-chainAnyone (Pimlico's public endpoint, Alchemy, self-hosted Alto or Rundler)Submits UserOperations. Cannot change the recipient, the fee or the sale terms, which the proof binds
Indexer (optional)Off-chainAnyoneServes the vault's events by block range, the same to everyone. Sees only public and encrypted data. Wallets check its data against an on-chain root and fall back to the chain
App and proverOff-chain, on the user's deviceThe userDerives keys, scans and decrypts notes, builds and proves transactions in the browser, asks the wallet to sign each spend

PrivateRouter and FeeHarvester share one trading library, PonsSwapper. It trades on the bonding curve before graduation and on the Pons Uniswap v4 pool after, and performs a pending graduation step itself before trading. See The market.

The addresses for each network are in Deployments; the contract interfaces are in Contracts.

No admin keys

The noircash contracts have no owner, no proxy, no pause, no upgrade path and no initializer. Every address the vault works with is a constructor argument:

ValueWhere
NOIR, Pons factory, curve, PoolManager, graduated pool IDPrivateVault immutables
Verifier, hasher, EntryPoint, sale router, harvesterPrivateVault immutables
EIP-712 name (noircash) and version (1)PrivateVault constructor
Pool key of the graduated poolPrivateRouter, FeeHarvester, read from the Pons factory at construction
Target deposit, fee escrow, vaultFeeHarvester immutables
MAX_SLIPPAGE_BPS (300), CALLER_TIP_BPS (50)FeeHarvester constants

The vault and the harvester refer to each other. Both are deployed through CreateX's CREATE3 at addresses fixed in advance, so each constructor receives the other's address before it exists. See NOIR on Pons.

A bug cannot be fixed in place. Nobody can withdraw the EntryPoint deposit or stake, or the vault's NOIR, except by spending notes with valid proofs. See the trust model.

What Pons controls

NOIR, its curve, its graduated pool and its fees are Pons contracts. The noircash contracts cannot change them, and they cannot touch the vault. The Pons owner can redirect NOIR's creator fee recipient away from the harvester with setCreatorFeeRecipient: the change waits a 3-day timelock (CREATOR_FEE_RECIPIENT_TIMELOCK), is visible in pendingCreatorFeeRecipient(noir) during that time, and must then be executed within a further 3 days. That would stop the holder yield described in The vault. It would not touch any note, the vault's NOIR or the ability to shield, send, unshield and sell.

Flow 1: buy through the app

The app creates a note for the user with random rho and r, computes stub = H(opk, rho, r), and encrypts (rho, r, 0) to the user's viewing key. The share amount is not known yet. It asks PrivateRouter.quote(true, ethIn) for the expected output and subtracts 1% slippage.

The user's wallet calls PrivateRouter.buy{value: ethIn}(stub, minTokensOut, ciphertext).

The router buys NOIR: on the curve with curve.buy(ethIn, minTokensOut, router) before graduation, or with an exact-input swap on the Pons v4 pool after. Pons charges the same fees on both venues and pays them out in ETH.

The router checks tokensOut >= minTokensOut, approves the vault and calls vault.shield(stub, tokensOut, ciphertext). The vault pulls the NOIR, mints shares, computes cm = H(1, stub, shares), inserts it into the tree and emits Shielded and NoteAdded. The router then calls vault.poke().

The router returns any unspent ETH (a buy that fills the curve is refunded in part). The user's app later finds the note by decrypting the ciphertext, and reads its shares from the Shielded event of the same transaction.

A purchase anywhere else (a DEX, an aggregator, the Pons site) delivers public NOIR. The holder turns it into a note with one shield call: see Public NOIR and shielding.

Flow 2: private send

The app scans the tree and picks up to two unspent notes from the same tree (one proof has one root).

It reads the recipient's keysOf(recipient), creates a note for them with fresh rho and r, and a change note for the sender. Each output is encrypted to its owner's viewing key.

It builds ExtData with caller = the vault, gasFee = the network fee in shares, and data = abi.encode(callGasLimit, address(0), 0). The only exit is the gas fee: exitShares = gasFee.

The app estimates gas through a bundler with a placeholder proof, then the wallet signs EIP-712 Spend(bytes32 transaction) once, and the app proves once, in the browser.

The UserOperation (sender = the vault, signature = abi.encode(Transaction, proof)) goes to any bundler. The EntryPoint calls validateUserOp, which checks the fee, verifies the proof, marks the nullifiers spent, inserts both output commitments and moves the fee to the gas reserve.

execute4337(nf0) finds no pending sale and returns. On-chain, the transaction shows two nullifiers, two new commitments and a fee. It does not show sender, recipient or amount.

Flow 3: private sale through ERC-4337

As in a send, but data = abi.encode(callGasLimit, ethRecipient, minEthOut), where minEthOut is the router's quote less 1%. exitShares = sale shares + fee shares, and recipient is zero.

In validation the vault applies the whole private transition and puts the sale amount in escrow, pendingSales[nf0], instead of paying anyone.

In execution, execute4337(nf0) approves PrivateRouter for the amount and calls onPrivateExit. The router pulls the NOIR, sells it on the live venue and sends the ETH to ethRecipient.

If the sale fails (for example the price moved below minEthOut), the vault transfers the NOIR to ethRecipient instead. If execution ran out of gas, anyone can call claimSale(nf0) to do the same.

The address that receives the ETH is public; which notes paid for it is not. Details in Gas and ERC-4337.

Flow 4: the harvest

Every trade of NOIR, on the curve or on the pool, pays the Pons fees in ETH. The creator share accrues to the harvester.

Anyone calls FeeHarvester.harvest(). It sweeps the fees Pons holds for NOIR, claims the harvester's ETH from the Pons fee escrow and tips the caller 0.5%.

It tops up the vault's EntryPoint deposit to targetDeposit, then buys NOIR with the rest of the ETH and calls vault.donate. The vault's backing B grows, its shares S do not, so every note is worth more NOIR.

The steps and their limits are in The vault.

Paths that need no bundler

A private transaction can also be submitted directly with PrivateVault.transact(t, proof) (with ext.caller zero or the caller's address), and a sale with PrivateRouter.sell(t, proof). The address paying gas is then visible. If the router cannot trade, notes can exit to any address as public NOIR and be sold elsewhere. See the threat model.

On this page