Threat model
The attacks noircash is designed against, what stops each one, what risk remains, and the limits the design accepts.
Inside the vault, proofs hide who pays whom and how much. Entries and exits stay public: purchases, shields, sales and unshields show an address and an amount, and there protection comes from the number of users. For each attacker: what they try, what stops them, and what is left.
Attackers and defences
Chain analyst
- Tries: to link a purchase or shield to a later sale or unshield by amount and timing.
- Stopped by: only partly. Spends reveal nullifiers and new commitments, not which notes were spent. Splitting amounts, waiting and selling to fresh addresses help. See Staying private.
- Residual risk: high while the privacy set is small. A distinctive amount shielded and taken out soon after is easy to match.
Malicious bundler
- Tries: to change the recipient, the gas fee or the sale terms of an operation it relays.
- Stopped by: the proof. Recipient, caller, gas fee, sale terms and ciphertexts are hashed into a public input
that your
Spendsignature also covers; changing any field invalidates the proof. A minimumcallGasLimitin the proof stops the bundler starving a sale's execution. - Residual risk: none on content. It can still see your IP address and timing.
Censoring bundler
- Tries: to refuse or delay your operation.
- Stopped by: any other ERC-4337 bundler, or a direct call. A wallet can submit through the EntryPoint's
handleOpsfrom your own address, and the vault'stransactcan be called directly. - Residual risk: a direct call reveals the address that pays gas.
Spammer draining the gas deposit
- Tries: to make the vault's EntryPoint deposit pay for operations that pay nothing.
- Stopped by: the private state change happens in validation, so execution cannot undo it. An invalid proof writes nothing and is not executed. A valid operation pays a fee in shares worth its maximum gas cost plus 20%, at the price average.
- Residual risk: operations pay for their maximum declared gas, which is above the gas used. The surplus goes to holders once the deposit is full, not back to the user.
Price manipulator
- Tries: to move the vault's price average so gas fees are underpriced, or so the harvester buys dear or sells the gas reserve cheap.
- Stopped by: the average reads only NOIR's own Pons venue: the curve's reserves before graduation, the Pons
pool's
slot0after. Only the price at a block's last poke counts, folded in when a later block is first poked, with weight 1/8 and clamped to ±10% of the average: at most 1.25% per block (an L1 block, about 12 seconds, on Robinhood Chain). The harvester's buys and sales must land within 3% of the average after Pons fees; a buy that does not fit is halved, over up to 8 sizes, then skipped. - Residual risk: anyone can poke, so a price pushed, poked and pushed back in one transaction still counts for that block, within the clamp. Repeated over about 15 consecutive L1 blocks (about three minutes), each paying the Pons fees on both legs of the push, it can move the average by the 20% gas margin. See the accepted risks below.
Decoy pool
- Tries: to open another ETH/NOIR pool, at an absurd price, so the router trades there or the vault prices from it.
- Stopped by: the router, the harvester and the vault use only the pool key Pons graduates into (native ETH,
NOIR, the launch's fee and tick spacing,
factory.memeHook()). Only the Pons factory can initialize a pool with that hook, so nobody can open that exact pool first. Any other key has another pool id. A fork test opens a decoy and checks it is ignored. - Residual risk: none for noircash. Third-party routers may still find the decoy.
Sandwicher
- Tries: to trade before and after your buy or sale, or around a harvest.
- Stopped by: minimum outputs from the router's
quote(Pons fees included) less 1% slippage. For sales the minimum ETH out is bound to the proof. Harvest trades are bounded by the 3% rule above. - Residual risk: up to the slippage you accept. Robinhood Chain orders transactions by arrival, which makes
sandwiches harder but does not rule them out. The harvester sweeps fees with no minimum outputs
(
sweepFees(0)andsweepPoolFees(poolId, 0, 0)), so a conversion of fees that Pons performs inside that sweep has no bound set by noircash.
Launch sniper
- Tries: to buy NOIR in the first moments after launch.
- Stopped by: Pons' opening snipe tax on buys, 99% decaying to 0 over the first 5 seconds. The deploy script buys the seed only once the tax is over.
- Residual risk: buying after the first 5 seconds is open to everyone, by design.
Pons redirecting the fees
- Tries: the Pons owner calls
setCreatorFeeRecipientfor NOIR, or uses its rescue functions on the curve or the graduation (rescueCurveFees,forceSweptGraduation,rescueSweptGraduation). - Stopped by: a 3-day timelock on the fee redirect, visible in
pendingCreatorFeeRecipient. NOIR has no owner and no transfer restrictions, so nothing Pons controls can move the vault's NOIR. - Residual risk: holder yield stops, and the gas deposit is no longer topped up with fee ETH; it can still be funded by selling the gas reserve or by anyone's deposit. Trading and the price average depend on Pons' curve and pool. Notes stay spendable: sends and unshields never touch a Pons contract. See Trust model.
Keeper absence
- Tries: nothing. The risk is that nobody calls
harvestand the deposit runs dry. - Stopped by:
harvestis open to anyone and pays its caller 0.5% of the ETH it handles, and a noircash keeper harvests whenever the deposit is low or fees have built up. Anyone can also top up the deposit directly. With an empty deposit, calling the vault'stransactdirectly still works, with gas paid by the caller. - Residual risk: fees wait in the Pons escrow until someone harvests, and a direct call reveals the address that pays gas.
Tree exhaustion
- Tries: to fill the note tree (depth 24, about 16.7 million leaves) so no new notes fit.
- Stopped by: a full tree rolls over to a new one. Its final root stays valid forever, so its notes stay spendable. Each proof uses one root, so both inputs of a spend come from the same tree.
- Residual risk: filling a tree costs gas and NOIR for every insertion.
Lying indexer
- Tries: to hide or alter notes, or to link users to their notes.
- Stopped by: the same event ranges for everyone, and a root check with
isKnownRoot; on a mismatch, or when the indexer is down, the app rereads the chain. See The Merkle tree. - Residual risk: the root check covers commitments only. An indexer that altered a note's ciphertext, or dropped
NullifierSpentorShieldedevents, would pass it: the app would miss a note or show a spent one. Funds are not at risk, and reading from the chain gives the right answer. The indexer also sees IP and timing.
Phishing site asking for the activation signature
- Tries: to get you to sign the activation message on another domain.
- Stopped by: the message is a Sign-In with Ethereum message that names the app's domain, so wallets warn when another site asks for it. A stolen activation signature reveals your notes but cannot spend them: every spend needs a fresh EIP-712 signature from your wallet.
- Residual risk: your privacy is lost for good. The keys are deterministic and cannot be rotated for that wallet; moving your notes to a new wallet restores it for future activity. A site that you then use to sign spends can also get you to sign a harmful transaction (see Trust model).
Bug in the circuits or contracts
- Tries: to exploit a flaw, for example to create shares from nothing or spend a note twice.
- Stopped by: tests of the contracts and the circuit. The vault totals B and S are
public, and every change emits
VaultUpdated, so anyone can check them against the vault's NOIR balance. - Residual risk: the vault has no cap on the NOIR it holds, and a bug cannot be patched.
Stated limits
- Small privacy set early on. While holders are few, "who" is weakly protected. See Staying private.
- Private by choice. NOIR held outside the vault is an ordinary public balance. Only shielded NOIR is private, and only notes earn the holder yield.
- Public entries. A purchase with public ETH, or a shield of public NOIR, shows the address and the amount. What stays hidden is which note is theirs and when it is spent.
- IP and timing metadata. The RPC node, the bundler and the indexer can see IP addresses and when requests arrive. The app does not yet sync in the background on a schedule, so a sync right before a spend can hint at it.
- Dependence on Pons. Trading, the price average and the holder yield run on Pons contracts and on Pons' fee payments. See Trust model.
- Immutability. A bug cannot be fixed in place. The only remedy is a voluntary migration to new contracts.
- Contract-based multisigs are unsupported. The circuit checks one EOA ECDSA signature, and the verifier cannot change.
Accepted risks
These behaviours are known and accepted in the current code.
| Behaviour | Effect | Why accepted |
|---|---|---|
claimSale can run in the same bundle, before a sale executes | The sale's NOIR goes to the ETH recipient instead of being swapped | The attacker gains nothing. The EntryPoint gives no "same transaction" signal, and ERC-7562 bans timestamp and block reads in validation |
| The gas fee covers the maximum declared gas | Users pay more than the gas used; the surplus goes to holders once the deposit is full | The fee must cover the worst case to protect the deposit |
| A harvest buy that does not fit the 3% bound is skipped | Fee ETH waits in the harvester for a later harvest | Buying far from the average would give value away to whoever moved the price |
| A block's price is the price at its last poke, and anyone can poke | A price pushed, poked and pushed back in one transaction enters the average, clamped to ±10% and with weight 1/8 | Each block costs the Pons fees on both legs of a push large enough to move the price 10%, for about 15 consecutive L1 blocks. The most it can win is the 20% gas margin on the ERC-4337 operations of that window, and the harvester stops trading once the market is more than 3% from the average |
| A buy that crosses graduation can be partly refunded by the curve | The router returns the unspent ETH to the buyer; the harvester keeps it for its next harvest | The curve stops selling once it is full; the rest trades on the pool after graduation |