Alle Docs
Operations·Aktualisiert 2026-05-26

Threat model

What SolMask protects against, what it explicitly does not, and which protocol element provides each guarantee.

A privacy protocol is only as good as the gap between what it promises and what it actually defends. Every promise below is mapped to the protocol element that enforces it. Every non-promise is named so users can act accordingly.

What SolMask defends against

1. On-chain linking of a deposit to its withdraw

The core guarantee. Given a withdraw to wallet B and a set of deposits including one from wallet A, a chain-analytics observer cannot distinguish "wallet A's deposit produced B's withdraw" from "any other deposit in the anonymity set produced B's withdraw." Enforced by the withdraw circuit's Merkle-membership proof: the path elements and indices are private inputs; the chain sees only (root, nullifier_hash, recipient, ...). The strength is bounded by anonymity-set size — the count of deposits at the same asset, similar denomination, with overlapping unlock windows. The web client surfaces the current set size before a deposit is committed.

2. Double-spend of a note

A note can be spent exactly once. The withdraw instruction initializes a per-note nullifier record PDA. On a second attempt, that account already exists and the transaction reverts. The nullifier is derived inside the circuit from a private witness, so a prover cannot announce a different nullifier than the one their note dictates — that would be a Poseidon collision (≥254-bit work).

3. Early spend (before the user-chosen privacy delay)

The user sets an unlock slot at deposit time. A deposit spent in the next slot is trivially correlatable; the unlock delay forces a wait. Enforced inside the circuit as unlock_slot ≤ current_slot. The on-chain verifier additionally pins the proof's current_slot to a bounded backward window — it must be the runtime slot and no more than 150 slots (~60s) behind it — so the prover cannot future-date the slot to escape the timelock, while still tolerating the proof-gen→submission latency of an async relayer. A stale proof is rejected with SlotMismatch once it drifts past the window.

4. Cross-pool nullifier replay

A nullifier consumed in the SOL pool cannot also be used to spend in the USDC pool. Enforced by the mint binding in the public-input vector and the per-pool seeding of nullifier-record PDAs: the pool address is part of the PDA seeds, so the same nullifier in two different pools produces two distinct accounts.

5. Stale-root attack

A withdraw proof that targets a root no longer in the recent history is rejected — the defense against an old proof leaking back into the chain. Enforced by the root_history ring buffer, which holds the most recent roots; the withdraw handler rejects any proof whose root is not in that window.

6. Recipient redirect

A relayer cannot redirect funds to an attacker-controlled address mid-transaction. The mechanism is two-layered: the circuit forces recipient into the witness, and the Groth16 verifier binds it as a public input — the proof is computed over a specific public-input vector and is only valid for that vector. Changing recipient after proof generation requires producing a new proof, which requires the witness secrets the prover already chose. The on-chain handler adds a belt-and-suspenders check that the recipient token account matches the public-input recipient.

The same layering applies to fee_recipient and mint: circuit forces them into the witness, Groth16 binds them via public inputs, the program adds runtime checks. Circuit and protocol are jointly required — neither alone is sufficient.

7. Banned-wallet deposit

A wallet flagged as high-risk (industry risk feeds, sanctions lists, curated additions) cannot deposit. Enforced by an on-chain ban-registry check at the deposit boundary: if the depositor's ban record is present, the deposit is rejected. Adding and removing ban entries is admin-only.

8. Pause as kill-switch

In an emergency, admin can stop new deposits and withdraws via a paused flag enforced on both the deposit and withdraw paths. Pause and unpause emit events for transparency.

What SolMask does NOT defend against

These are explicit non-promises. If your threat model requires defending any of them, layer additional measures on top.

1. Low anonymity-set timing correlation

If you deposit a large amount into a pool where you are one of three depositors with overlapping unlock windows, the anonymity-set math is weak — a few candidates produce the same withdraw within a narrow window and chain-analytics can rank them by other signals. SolMask's UI warns when a deposit amount lacks a healthy crowd. User-side mitigations: choose a denomination with a deep set, split large deposits across multiple notes with different unlock slots, wait longer.

2. RPC-level IP correlation

A passive observer who sees both your deposit's signing IP and your withdraw's submission IP can correlate them statistically. The cryptographic proof says nothing about network-layer metadata. Defenses outside the protocol: use the hosted relayer (its IP touches the RPC, not yours), Tor, a per-session VPN, or your own private RPC.

3. Admin compromise

If the admin key is stolen or coerced, the attacker can pause the protocol (DoS), add entries to the banlist (reversible DoS), rotate the fee destination (redirects future fees only), or adjust the sweep threshold. What the admin cannot do under any compromise scenario is withdraw from any pool's vault, forge proofs, or read note plaintext — the vault is PDA-signed and only the withdraw instruction releases funds, gated on a valid proof. Admin authority is held by a multisig.

4. Trusted-setup toxic-waste retention

The phase-2 ceremony's soundness rests on contributors discarding their entropy. A public Bitcoin-block beacon renormalizes the trapdoor; a public multi-party MPC ceremony distributes this trust across many independent contributors. See Trusted setup for the verification procedure and roadmap.

5. Browser compromise

A compromised browser can read note plaintext during a session or substitute the proving WASM with a malicious build that leaks secrets. Defenses are outside the protocol: keep your browser updated, prefer hardware-wallet signing for the wallet itself, and prefer the official deployed app over un-audited forks.

6. Side-channel leakage during proving

In-browser proof generation runs in a single-tab WASM context. Timing variations could in principle leak information about private inputs to a co-resident process. We are not aware of practical attacks against snarkjs+wasm in this configuration; browser-side proving is still materially safer than server-side proving, where the prover would have to see your secrets in plaintext.

7. Censorship at the relayer layer

The hosted relayer can refuse to broadcast your withdraw. It cannot steal — the recipient is bound to the proof — but it can delay. The mitigation is self-relay: anyone with SOL can submit a withdraw directly, because the protocol does not whitelist relayers.

8. Front-running and pattern analysis

A mempool observer sees your deposit's commitment before it lands. They learn nothing about pre-images from a Poseidon hash, but they know you deposited at a specific moment. If you withdraw to the same recipient repeatedly, an adversary builds a profile from public information. Use a fresh recipient for each withdraw.

Guarantee-to-element map

Most guarantees are enforced in two places — the circuit forces a value into the witness, and the on-chain handler asserts that value against runtime state. Either layer alone is insufficient.

GuaranteeCircuit layerProtocol layer
Deposit ↔ withdraw unlinkabilityMerkle-membership proofcryptographic property (no on-chain check)
Double-spend protectionnullifier bound to nullifier secretnullifier-record PDA init
Privacy delayunlock_slot ≤ current_slotcurrent_slot within [clock_slot − 150, clock_slot]
Cross-pool separationmint check + per-pool nullifier PDA seeding
Stale-root rejectionroot-history window check
Recipient bindingwitness presenceGroth16 public input + runtime ATA check
fee_recipient bindingwitness presenceGroth16 public input + runtime ATA check
mint bindingwitness presenceGroth16 public input + mint check
Banlist enforcementban-record check at deposit
Emergency stoppaused-flag constraint on deposit + withdraw

A claim not in this table is not a claim SolMask makes.

Threat model · SolMask