Threat model
This page lays out the threat model for Privacy Pools v2: what is private and when, who is in control of what, and what happens in adversarial conditions.
Honest-but-curious chain observer
The default adversary is someone watching every transaction on Ethereum.
- Sees: deposits (depositor address and amount, both public). Transact events (commitment count in/out, no values). Withdrawals (destination address and amount).
- Doesn't see: which input commitment is being spent. Recipient of a transact. Linkage between deposit and any later spend.
- Anonymity set: privacy depends on on-chain transact volume. A quiet pool provides less unlinkability than a busy one.
Malicious ASP
The ASP service refuses to attest, attests selectively, or goes offline.
- Worst case (refuses to attest): your deposit can't be spent normally, but the note's current owner can always ragequit to recover, gated by
msg.sender == ownerAddress(since notes are transferable, this is whoever holds the note now, not necessarily the original depositor). - Sees: deposit labels (
Poseidon(precommitment, depositSecret)) and the on-chain pool events. The ASP cannot decrypt note payloads, derive nullifiers, or learn values from labels alone. - Doesn't compromise: the privacy of other users. The ASP never sees decrypted note contents and never holds your keys.
- Trust scope: liveness only. Whether your specific deposit gets approved depends on the ASP's policy.
Compromised relayer
A relayer is malicious or compromised.
- Sees: the asset, the public fee and public-outflow leg, the proof shape (input/output counts), the encrypted note payloads, and request metadata such as the originating IP.
- Doesn't see: the amount of a private in-pool transfer (a withdrawal's amount is public), the sender or recipient, or your keys.
- Can: refuse to submit or censor your transaction. Mitigate this by using an alternative relayer if available or publicly exiting via ragequit.
- Cannot: steal funds. The proof binds the destination, so the relayer can only submit the transaction as prepared or refuse.
Compromised viewing key
An attacker has your viewingPrivateKey.
- Reveals: every incoming note ever encrypted to you, past and future, until you migrate.
- Doesn't enable: spending. Spending requires
privateNullifyingKey, which is held separately. - Mitigation: migrate to a new wallet for future activity. Old notes remain visible to the attacker but stay spendable by you alone.
Wallet compromise
Your wallet's signing key is leaked.
- Attacker can: re-derive all four protocol keys from your wallet, which gives them full account access.
- Mitigation: move funds via withdraw to a fresh wallet as fast as possible.
Selective disclosure receipt
You handed an auditor a payment receipt.
- They see: exactly that one payment: the value, asset, recipient address, tx hash, and pool address.
- They don't see: any other transaction in your wallet's history, any other note's content, or your viewing private key.
- They cannot: forge other receipts, spend the note, or derive nullifiers.
Trusted setup
Every user operation is authorised by a Groth16 proof, and Groth16 relies on a trusted setup: a phase-2 ceremony per circuit whose "toxic waste" must be destroyed by every contributor.
- If the setup is sound: the contracts' checks are exactly as strong as the circuits, since a verifier accepts only proofs of true statements.
- If the phase-2 secret is known to anyone: they can forge a proof for any statement, which defeats every on-chain check at once: mint notes from nothing, spend notes they do not own, and forge keystore authorisation. This is the single assumption that everything else in this document sits on top of.
- What to check before trusting a deployment: that the twenty-seven verifier contracts (deposit, ragequit, and the
transact_NxMfamily) were generated from a multi-party ceremony with a published transcript and a public random beacon, and that the deployed verifier bytecode matches those ceremony keys. A verifier produced by a single-party development setup is fine for a testnet and unacceptable for real value. - Governance surface:
UPGRADER_ROLEcan swap any verifier on the PoolVault without an upgrade. Verifier replacement is therefore a governance action with the same weight as a contract upgrade.
Root freshness windows
Three roots gate a spend, and they age differently.
- State root: the vault keeps a circular buffer of the last
rootHistorySizeroots (16 on the deployed configuration), so a proof built a few insertions ago still lands. - Keystore root: a superseded root stays valid for
keystoreRootLivenessseconds after replacement (1200, twenty minutes, on the deployed configuration). This is what lets in-flight proofs survive a registration landing ahead of them, and it is also a revocation delay: rotating your revocable key withupdateAuthPolicydoes not invalidate a proof built against the old root for up to twenty minutes. Rotation is a recovery step, not a panic button. - ASP root: exact equality with
latestASPRoot(), no history. The moment a new root is published, every in-flight transact proof is invalid and must be rebuilt. A postman posting frequently can grief pending withdrawals, and a postman could time a publication against a specific one. Funds are never at risk, since ragequit bypasses the ASP root entirely, but this is a liveness asymmetry to plan for.
Aave, when you hold yield shares
A yield-share note is backed by aTokens the wrapper holds on Aave v3.
- You are exposed to Aave: a loss on the reserve is a loss on your position; the pool cannot insulate you from it.
- Unwrapping depends on Aave liquidity: if the reserve is paused, frozen, or fully utilised, a withdrawal to the underlying reverts and the note is not spent. You can still withdraw the shares as shares through the plain relay, or exit to the aToken with
PPYieldToken.redeem, which never calls Aave. - The wrapper's owner: can set the fee (capped at 20% of yield, never retroactive), claim accrued fees, pause new deposits, and rescue tokens other than the backing. It cannot reach principal. See PPYieldToken.
- Aave itself is an assumed-honest external protocol with a standard, hook-free, monotonically accruing aToken; the factories only wrap aTokens of one trusted pool.
Aggregator and bridge, when you swap
Withdraw-and-swap and reshield forward your withdrawn value to a DEX aggregator with calldata you committed in the proof.
- The relayer cannot change the route: target, calldata, recipient, fee, and output floor are all in the proof's
context. It can submit what you proved or nothing. - You trust the aggregator to execute the committed calldata, and the bridge, on a cross-chain route, to deliver on the destination chain. A same-chain swap is checked on-chain by the recipient's balance delta; a cross-chain one cannot be, and is checked by the aggregator's destination-side executor instead.
- The RelaySwaps owner curates which routers and zaps may receive funds. A compromised owner could allowlist a malicious router for future quotes, but could not alter any routing already proven.
Stealth announcements
A stealth withdrawal is public about one thing: the announcement marks the withdrawal as a stealth payment.
- An observer sees a withdrawal to a fresh address and an ERC-5564 announcement in the same transaction, and can tell the two belong together.
- They cannot link the stealth address to the recipient's meta-address or to any other payment, and the on-chain binding prevents a relayer from announcing an address the withdrawal does not fund.
- A relayer sees the recipient's meta-address material only if you resolve it client-side, which the SDK does; it receives the derived address and announcement, not your recipient's keys.