# SolMask — Full LLM Context -------------------------------------------------------------------------------- title: "Architecture" description: "The four subsystems that make SolMask work, what each one knows, and the trust boundaries between them." last_updated: "2026-05-26" source: "https://solmask.xyz/en/docs/architecture" -------------------------------------------------------------------------------- SolMask is split across four subsystems. The split is not incidental — each one has a different trust posture, and the boundaries are where the privacy guarantees are actually enforced. ## The four subsystems | Subsystem | Role | Trust posture | |---|---|---| | Anchor program | On-chain state, verifier, fee math, banlist | Trustless (verifier code is on-chain and reproducible) | | ZK circuits | Browser-side proof generation | Trustless once compiled (witness never leaves your machine) | | Relayer | Pays gas for withdraws, dedupes nullifiers, queues jobs | Trusted only with: your IP, your nullifier, the recipient address. Cannot steal funds. | | Web + Admin | UI, note storage, address-risk pre-check, admin tools | Trusted with: your wallet connection, your note plaintext in the browser session | ## The Anchor program The program is the only one of the four where the rules are enforced rather than merely followed. Its instructions fall into three groups: - **Admin-only configuration** — initialize config, rotate admin, set the fee destination, configure the risk-screening endpoint, pause/unpause. All gated by an admin constraint against the singleton config account. - **Pool setup** — per-asset pool initialization and sweep-threshold configuration, admin-only. - **User-facing** — deposit, withdraw, and fee sweep. State is a singleton config, one pool per asset (each with a shielded vault and a fee vault), per-pool Merkle-tree state and a recent-root history, one nullifier record per spent note, and one ban record per banned wallet. The program is the trust root. Anything it accepts is by definition correct under the protocol's rules. Anything else — the relayer's idea of which nullifiers have been spent, the web client's idea of how much is in a note — is a hint at best. ## The ZK circuits The withdraw circuit is compiled to R1CS, then through a Groth16 phase-2 ceremony into a proving key and a verification key. The proving key is served to your browser; the verification key is embedded in the on-chain verifier. The two MUST come from the same ceremony — see [Trusted setup](/docs/trusted-setup). Proof generation runs entirely in your browser via snarkjs+wasm. The circuit takes your note's private secrets plus a public-input vector (root, nullifier_hash, recipient, fee_recipient, current_slot, mint, amount_to_recipient, change_commitment) and produces a ~200-byte proof. The proof binds every public input — including the recipient — so a relayer that tries to redirect funds invalidates the proof. ## The relayer The relayer is an HTTP + queue service that accepts withdraw jobs and submits them on-chain. It pays the protocol fee (`config.withdraw_fee_lamports`, 0.003 SOL by default and admin-tunable without a redeploy) up front, then recoups exactly that fee by keeping a small tip slice — denominated in the deposit asset — out of the released `amount_to_recipient` before forwarding to the recipient. The Solana network gas for the transaction is absorbed by the relayer, not reimbursed. It runs an address-risk pre-check on the recipient and rejects high-risk addresses before broadcasting; it also dedupes nullifiers on the way in so a doubly-submitted proof from a confused client doesn't waste two on-chain attempts. The relayer cannot steal: the proof binds the recipient address. It cannot censor in a strong sense either, because users who don't want to use it can self-relay (see [Self-relay guide](/docs/relayer)). What it can do is observe your IP and the moment you withdrew — the IP-correlation trade-off. ## The web + admin apps The web app is the user-facing surface: wallet connect, deposit form, note storage, withdraw flow, swap UI, blog/docs. The admin app is operational: it lets the configured admin pause/unpause, manage the banlist, sweep fees, and inspect pool state. Note secrets — `secret`, `nullifier_secret`, `amount`, `unlock_slot` — never leave your browser. They are not random-and-stored; they are *derived* from your wallet. Connecting and signing SolMask's fixed key-derivation message yields a deterministic per-wallet seed (`sha256` of the ed25519 signature), and every note's secrets are HKDF-expanded from that seed. The proof is generated locally and submitted as opaque bytes. Because the derivation is deterministic, the same wallet reproduces the same secrets on any device — there is no passphrase to choose and no note file to back up. localStorage is only a convenience cache. The authoritative copy of each note is an encrypted recovery blob published on-chain (see [Note recovery](#note-recovery) below), so a freshly opened browser recovers your full balance the moment you reconnect the wallet. This stays non-custodial: only your wallet can derive the seed, so only you can decrypt or spend. Lose access to the wallet itself and the notes are gone — SolMask holds nothing that could recover them. ## Note recovery A note's spendability depends only on its four secrets, and those secrets are reproducible from your wallet. To make a note discoverable on a device that has never held it, the deposit also publishes an encrypted recovery blob: - At deposit time the web app encrypts `{ leaf_index, amount, unlock_slot }` to a key only your wallet can derive (AES-GCM under an HKDF-expanded view key). The ciphertext is emitted on-chain as `DepositEvent.encrypted_note`, capped at `MAX_ENCRYPTED_NOTE_LEN`. - A partial withdraw does the same for its change note via `WithdrawEvent.encrypted_change_note` (empty when the withdraw leaves no change). To recover, the client re-derives the wallet seed, fetches every recovery blob for a pool from the relayer's indexer, and **trial-decrypts** each one. Only the blobs encrypted to your wallet decrypt successfully; for each hit the client re-derives the note's secrets, confirms the commitment matches the on-chain leaf, and checks the nullifier set to mark the note spent or unspent. The relayer serves the blobs but cannot read them, so it never learns which notes are yours. The whole scan is client-side and idempotent — it, not localStorage, is the source of truth for your balance. ## Sequence: a deposit ``` User wallet Web app Anchor program | | | | --- click "Deposit 1 SOL" --->| | | | derive note secrets from wallet | | | commitment = Poseidon(s, n, a, u) | | | encrypt recovery blob (wallet key)| | | | | | --- build & request signature --->| | <--- sign deposit ix ---------| | | ----- broadcast tx ---------------------------------------> | | | program: | | - check banlist | | - full amount → shielded vault (deposit is free) | | - append commitment | | - push new root | | - emit deposit event | | (incl. encrypted_note) | | <-- deposit event (leaf index, root) -- ``` ## Sequence: a withdraw ``` User wallet Web app Relayer Anchor program | | | | | --- click Withdraw --->| | | | | generate proof | | | | (snarkjs/wasm) | | | | | | | | -- POST /withdraw ->| | | | {proof, PIs} | | | | | recipient risk-check | | | | + dedup | | | | | | | | --- submit tx ------>| | | | program: | | | - verify proof | | | - check root in history | | | - init nullifier record | | | - release tokens | | | - protocol fee → fee destination | | | - emit withdraw event | | | (incl. encrypted_change_note) | | <-- 200 OK + sig --| | ``` ## Trust boundaries summarized - **Between user wallet and web app**: the web app holds note plaintext during the session. - **Between web app and relayer**: the relayer sees the public-input vector, the proof bytes, the recipient, and your IP. It never sees the note secrets. - **Between relayer and program**: the program enforces the rules. The relayer can submit malformed payloads — they get rejected. - **Between admin app and program**: admin instructions all gate on the admin signer. The admin can pause, ban, sweep, and set the fee destination. It cannot view note plaintext and cannot retroactively spend a note it didn't create. The architecture is deliberately top-heavy on the program. Everything that matters for funds and for privacy is enforced there. The off-chain services exist for UX and economics, not for safety. -------------------------------------------------------------------------------- title: "ZK circuits" description: "The withdraw circuit's public inputs, private witnesses, Merkle tree parameters, nullifier derivation, and the conservation constraint." last_updated: "2026-05-26" source: "https://solmask.xyz/en/docs/circuits" -------------------------------------------------------------------------------- The withdraw circuit is the cryptographic heart of SolMask. It proves five things in one proof, all without revealing which deposit you are spending. This page is a reference; if you want the math intuition first, read [How ZK proofs work](/learn/how-zk-proofs-work). ## The circuit at a glance | Property | Value | |---|---| | Proving system | Groth16 | | Curve | BN254 (alt_bn128) | | Hash | Poseidon (circomlib) | | Merkle tree depth | 20 | | Per-pool capacity | 2^20 = 1,048,576 deposits | | Public inputs | 8 (each a 32-byte BE field element) | The choice of Groth16 over more modern systems (PLONK, Halo2, Nova) is pragmatic: Groth16 produces ~200-byte proofs that verify on-chain in single-digit milliseconds against Solana's `alt_bn128` syscalls. The cost is a circuit-specific phase-2 trusted setup, which we cover in [Trusted setup](/docs/trusted-setup). ## Public inputs The public-input vector is defined twice — once in the circuit and once in the on-chain verifier — and the orderings must match exactly: ``` root nullifier_hash recipient fee_recipient current_slot mint amount_to_recipient change_commitment ``` Each input's role: - **`root`** — the Merkle root the proof commits against. The on-chain verifier checks that `root` is in the recent-root ring buffer before accepting the proof. This window lets in-flight proofs survive new deposits that change the root. - **`nullifier_hash`** — the spend tag, derived in-circuit from `nullifier_secret`. The program initializes a per-note nullifier record; double-spend is mechanically impossible because the second initialization fails. - **`recipient`** — bound into the proof. A relayer cannot redirect funds without invalidating the proof. - **`fee_recipient`** — the token account that receives the released `amount_to_recipient`. Typically a relayer-controlled escrow account that subsequent instructions in the same tx route to the final recipient (possibly via a Jupiter swap). - **`current_slot`** — the slot the proof was built against. The on-chain verifier does *not* demand an exact match with the runtime slot — an async relayer (proof-gen → queue → build → submit) makes that unachievable. Instead it enforces a bounded backward window: `current_slot ≤ clock.slot` **and** `clock.slot − current_slot ≤ 150` (~60s at 400ms/slot). A future-dated slot is rejected outright, so the timelock can never be escaped; a stale proof is rejected once it drifts past the window. Combined with the in-circuit `unlock_slot ≤ current_slot` check, this is how the timing-delay privacy primitive is enforced. Anything outside the window returns `SlotMismatch`. - **`mint`** — must equal the pool's mint. Prevents a pool-A nullifier being applied to pool B. - **`amount_to_recipient`** — how much to release from the pool's vault. Must satisfy `amount_to_recipient ≤ amount_total`. - **`change_commitment`** — a fresh leaf inserted into the tree for `amount_total - amount_to_recipient`. Zero if the entire note is being spent. ## Private witnesses The private inputs never leave your browser. They are: - `secret` — 254-bit random scalar - `nullifier_secret` — 254-bit random scalar - `amount_total` — the note's total value (u64) - `unlock_slot` — the slot at which the note becomes spendable (u64) - `merkle_path[20]`, `merkle_path_indices[20]` — the inclusion proof - `change_secret`, `change_nullifier_secret`, `change_unlock_slot` — fresh secrets for the change note (unused if `change_amount == 0`) A deposit's commitment is `Poseidon(secret, nullifier_secret, amount, unlock_slot)`. The commitment is the only thing that lands on-chain as the leaf. The four pre-images are derived deterministically from your wallet: connecting and signing SolMask's fixed key-derivation message yields a per-wallet seed (`sha256` of the signature), and each note's secrets are HKDF-expanded from that seed. They never leave your browser, and because the derivation is deterministic the same wallet reproduces them on any device — there is no passphrase and no note file to lose. See [Architecture](/docs/architecture) for how the encrypted recovery blob is published on-chain so a fresh device can re-discover its notes. ## The five constraints The circuit proves all five simultaneously: 1. **Leaf binding.** The hashes of the private inputs reconstruct the leaf: ``` commitment = Poseidon(secret, nullifier_secret, amount_total, unlock_slot) nullifier_hash = Poseidon(nullifier_secret) ``` The output `nullifier_hash` is constrained equal to the public input of the same name. You cannot announce a different nullifier than the one your `nullifier_secret` derives — that would be a Poseidon collision, ≥254-bit work. 2. **Merkle membership.** A depth-20 path-checker walks from your leaf up to the claimed `root`. The path elements and indices are private; only the root is public. 3. **Timing check.** A range comparator enforces `unlock_slot ≤ current_slot`. Early-spend is impossible at the circuit level. 4. **Amount conservation.** ``` amount_to_recipient ≤ amount_total (range check) change_amount = amount_total - amount_to_recipient ``` The subtraction is in the field; the range check above ensures it doesn't underflow. 5. **Change-note construction.** The change commitment is `Poseidon(change_secret, change_nullifier_secret, change_amount, change_unlock_slot)`, multiplied by `(1 - isZeroChange)` — so if `change_amount == 0`, the expected change commitment is the field zero. The public `change_commitment` must match this expected value, which means a partial withdraw cannot output a change leaf that doesn't actually match a note you control. There is also a binding constraint that forces `recipient`, `fee_recipient`, and `mint` into the R1CS by squaring them. This is a circom idiom: if a public input is not otherwise constrained by an arithmetic operation, the prover could in principle ignore it. Squaring is the cheapest way to introduce a constraint without altering the semantics. The proof is non-malleable against changes to these three fields. ## The Merkle tree The tree is a binary Poseidon Merkle tree of depth 20, giving a per-pool capacity of 2^20 = 1,048,576 deposits. Empty subtrees use precomputed zero-roots. On deposit, a leaf is appended in O(log n) using a cached frontier — only the right-most filled node at each level. After insertion, the new root is pushed to the recent-root history. The recent-root history is a ring buffer; withdraw proofs are accepted against any root in this window. The window matters because between the moment your browser builds the proof and the moment the relayer submits it, other deposits can land and change the latest root. The tolerance means many intervening deposits before your proof is stale. If the history rolls over before your proof lands, you get an `UnknownRoot` error. The fix is to rebuild the proof against the new tree state — your note secrets are unchanged, only the inclusion path changes. ## Nullifier derivation `nullifier_hash = Poseidon(nullifier_secret)` — single-input Poseidon. Using one input domain-separates the nullifier from the leaf hash, which is four-input Poseidon. There is no preimage relationship between a leaf and its nullifier observable on-chain — the chain sees a 32-byte commitment at deposit, an unrelated 32-byte `nullifier_hash` at withdraw, and a `nullifier_secret` that is never seen. ## The on-chain verifier The on-chain verifier consumes `(proof_a, proof_b, proof_c, public_inputs)` and dispatches to Solana's `alt_bn128_pairing` syscall. The verifying key is generated deterministically from the published `verification_key.json`; if you want to verify the binding, re-run the generation against the published JSON and diff against the deployed verifier. ## Things to verify yourself If you want to audit the cryptography without taking our word for it: - Re-derive a commitment from your own note using `Poseidon(4)` and confirm it appears in the on-chain Merkle tree's leaf set (the relayer's indexer endpoint exposes the full leaf list per asset). - Run `snarkjs zkey verify` against the published phase-1 ptau and the published `withdraw_final.zkey`. See [Trusted setup](/docs/trusted-setup). - Read the verifying key into a separate Groth16 verifier (e.g. the snarkjs CLI) and verify a real on-chain withdraw's proof bytes, which are emitted in the withdraw transaction logs. -------------------------------------------------------------------------------- title: "Fee model" description: "Free deposits. The protocol fee — a percentage in the withdrawn token plus a flat 0.003 SOL — is charged at withdraw time. Worked examples, where the fees go, and how sweep works." last_updated: "2026-05-31" source: "https://solmask.xyz/en/docs/fees" -------------------------------------------------------------------------------- **Deposits are free.** SolMask charges its protocol fee at withdraw time, in two parts. Both are enforced on-chain, both are admin-tunable without a redeploy, and both are deliberately simple. ## The fees | Fee | Where | Amount | |---|---|---| | Deposit | — | **Free** | | Withdraw — percentage | At withdraw time, in the withdrawn token | `Config.withdraw_fee_bps` (default 0, recommended 23 bps = 0.23%; capped at 100 bps) | | Withdraw — flat | At withdraw time, in SOL | `Config.withdraw_fee_lamports` (default 0.003 SOL; capped at 0.1 SOL) | The full amount you deposit enters the shielded pool — nothing is skimmed on the way in. When you withdraw, the percentage fee is taken in the asset you are withdrawing and accrues to the pool's token fee vault; the flat fee is paid in SOL to the fee destination. Both rates live in the global `Config` account and are tunable by the admin (the percentage via `set_withdraw_fee_bps`, the flat fee via `set_withdraw_fee`) within hard on-chain caps. The percentage fee is **bound inside the withdraw zero-knowledge proof**: the circuit proves `Σ inputs = Σ outputs + fee_amount + change`, and the on-chain handler recomputes the expected fee from `Config.withdraw_fee_bps` and rejects any proof whose bound `fee_amount` doesn't match. The relayer cannot tamper with it. The flat SOL fee works as before: the relayer pays it to the fee destination on every withdraw and recoups exactly it by keeping a small tip slice in the withdrawn asset; the network gas it pays to land the transaction is a separate cost the relayer absorbs. If you self-relay, you pay the current fees yourself — the program collects them either way. There is no minimum, no maximum beyond the caps, no slab structure, and no surge pricing. ## Worked example: 1 SOL deposit You deposit 1 SOL: ``` amount = 1_000_000_000 lamports fee on deposit = 0 credited = 1_000_000_000 lamports (the full 1 SOL) ``` Two on-chain effects: 1. The full 1,000,000,000 lamports moves from your token account to the shielded vault. 2. Your `commitment` is appended to the Merkle tree; the new root goes into the recent-root history. Your note's amount field is 1,000,000,000 lamports — the full deposit. Later you withdraw the 1 SOL to a fresh wallet, with the percentage rate set to 23 bps: ``` percentage fee = released * 23 / 10_000 (in the withdrawn token, SOL here) flat fee = 0.003 SOL ``` The circuit's conservation check enforces `Σ inputs = Σ outputs + fee_amount + change` over your note, so the fee is part of what your note pays for. On a 1 SOL withdraw that's ~0.0023 SOL of percentage fee plus the 0.003 SOL flat fee — roughly 0.0053 SOL total, or about 0.53% on a 1 SOL round-trip. The difference versus the old model: you pay nothing until you exit. ## Worked example: 100 USDC deposit You deposit 100 USDC (decimals = 6, so 100,000,000 base units): ``` amount = 100_000_000 base units = 100 USDC credited = 100_000_000 base units = 100 USDC (deposit is free) ``` The full 100 USDC enters the pool. When you withdraw, the percentage fee is taken **in USDC** (e.g. 0.23 USDC at 23 bps) and accrues to the USDC pool's token fee vault. The flat 0.003 SOL fee is still paid in SOL: the relayer pays it from its own wallet and recoups it by keeping a small USDC tip slice out of the withdrawn amount, so the recipient does not need to hold any SOL beforehand. The recipient receives their USDC net of the percentage fee and that tip. ## Where the fees go Both fees flow to a single configurable fee destination, set at initialization and rotatable by admin. The flat SOL fee transfers directly to it as a lamport transfer inside the withdraw handler. The percentage fee accumulates in each pool's token fee vault and only moves to the fee destination when a sweep is triggered. ## Sweep mechanics Sweeping is a user-facing instruction. It checks the fee vault balance against the pool's configured sweep threshold; if the threshold is met, the full balance transfers to the fee destination's token account via a pool-PDA-signed transfer. If not, it errors. The threshold is per-pool and admin-adjustable. Sweep is **not** automated by the program — there is no built-in cron or keeper. The intended workflow is an off-chain job that checks each pool's fee vault and calls sweep when the threshold is met, paying gas for the sweep transaction. If that job stops, anyone can call sweep and accomplish the same thing; the funds always go to the configured fee destination regardless of who triggers the call. There is no path by which fees stay stuck in a fee vault indefinitely. ## What the fee model is not It is not a yield-sharing mechanism. Depositors do not receive a portion of fees. They are protocol revenue. It is not progressive. A 0.05 SOL withdraw pays the same 23 bps as a 5,000 SOL withdraw. The flat-percentage model was chosen for legibility — you can predict your fee in your head, and the math is identical across assets. Charging it on exit means deposits cost nothing and you only pay when value actually leaves the pool. -------------------------------------------------------------------------------- title: "Overview" description: "What SolMask is, who it's for, and what it is not. A five-paragraph orientation before you dive into the rest of the docs." last_updated: "2026-05-26" source: "https://solmask.xyz/en/docs/overview" -------------------------------------------------------------------------------- SolMask is a privacy protocol on Solana. It is a shielded pool: you deposit a supported SPL token into a per-asset pool, wait, then withdraw the same asset to a different wallet. The on-chain link between deposit and withdraw is hidden by a Groth16 zero-knowledge proof that the withdraw wallet has a secret matching one of the deposits — without revealing which deposit. The pool itself is a Poseidon Merkle tree over the BN254 curve, depth 20, with a capacity of `1 << 20` = 1,048,576 leaves per asset. SolMask exists for users who have an ordinary reason to keep their balances and transfers off public chain-analytics dashboards: payroll, treasury operations, OTC settlement, donations, personal transactions. It is not a tool for laundering proceeds of crime. Every deposit passes through a banlist check at the program boundary. Wallets added to the on-chain ban registry — typically flagged by an address-risk screening provider before deposit, with admin-curated additions — are rejected. The deposit transaction reverts; no commitment lands. The user flow is two instructions. **Deposit** transfers `amount` of the asset into the shielded vault fee-free — the full amount is credited — and appends `commitment = Poseidon(secret, nullifier_secret, amount, unlock_slot)` to the Merkle tree. Only the commitment hash is public; the four pre-images are derived deterministically from your wallet and never leave your browser. The deposit also publishes an encrypted recovery blob on-chain (carried in the deposit event), so the same wallet can rediscover the note on any device — there is no passphrase to choose and no note file to back up. **Withdraw** submits a Groth16 proof together with a `nullifier_hash`. The verifier checks that the proof binds against a recent Merkle root (the most recent roots are retained in a ring buffer), that the nullifier has not been seen before, that the requested release amount does not exceed the note's value, and that the current slot is at or past your declared unlock slot. The released amount goes to a recipient ATA; any change becomes a fresh commitment appended to the tree in the same transaction. What SolMask is **not** is worth being explicit about. It is not a generic privacy tool for arbitrary funds — the banlist is a hard precondition at deposit time and is non-bypassable from the user side. It is not a guarantee of anonymity if your withdraw lands in an empty pool: privacy is statistical, bounded by the anonymity set, and SolMask's UI surfaces the current set size before you commit a deposit amount. It is not custodial — your note secrets are derived from your wallet (no passphrase), and the on-chain encrypted recovery blob can be decrypted only by that same wallet, so reconnecting it on any device restores your balance while SolMask sees nothing; the flip side is that if you lose access to the wallet itself, the notes are gone and SolMask cannot recover them. It is not metadata-private at the network layer — a passive observer of your RPC connection can still time-correlate a withdraw to a TCP connection from your IP, which is why SolMask's hosted relayer is the default path and the self-relay docs spell out the IP-correlation trade-off. The protocol is **permissionless on the withdraw side**: anyone with SOL to pay gas can submit a valid proof. The relayer signer is unconstrained beyond being someone willing to pay to initialize the nullifier record and forward the 0.003 SOL protocol fee to the fee destination. The hosted relayer is a UX layer, not a gatekeeper. If it ever censors a user, the [self-relay path](/docs/relayer) is available; the only thing it gives up is the IP-correlation protection. The protocol is **deliberately small** for v1, covering deposit, withdraw, banlist, pause, fee sweep, and admin rotation. The withdraw circuit is a single file. The trusted setup was a phase-2 ceremony with a Bitcoin-block beacon; a public multi-party ceremony is planned as the protocol matures (see [Trusted setup](/docs/trusted-setup)). Every simplification in v1 is documented. Read next: [Architecture](/docs/architecture) for the four subsystems and what each one knows; [ZK circuits](/docs/circuits) for the public-input vector and the circuit constraints; [Threat model](/docs/threat-model) for the explicit list of attacks SolMask defends against and the ones it does not. If you want the plain-English version first, start at [How it works](/learn/what-is-solmask). -------------------------------------------------------------------------------- title: "Self-relay guide" description: "How to bypass SolMask's hosted relayer: build, sign, and submit a withdraw transaction with your own wallet. Trade-offs and step-by-step procedure." last_updated: "2026-05-26" source: "https://solmask.xyz/en/docs/relayer" -------------------------------------------------------------------------------- SolMask ships with a hosted relayer at `relayer.solmask.xyz`; the current endpoint is announced in the [SolMask Discord](https://discord.gg/cef6tqKqEJ). It is the default, it is the easiest path, and it is what 99% of users will use. But the protocol does not require it. Any signer with enough SOL to cover gas plus the 0.003 SOL protocol fee can submit a withdraw transaction directly. This page covers when to self-relay, what you give up, and how to do it. ## When self-relay makes sense Use the hosted relayer if: - You do not want to expose any of your wallets to RPC providers — the relayer's RPC connection sees the IP of the relayer server, not yours. - You do not want to fund your recipient wallet with any SOL up-front. The relayer pays gas and the 0.003 SOL protocol fee; the protocol fee is recouped from the tip portion of the withdraw quote, and the relayer absorbs the network gas itself. Self-relay if: - You are operating from an environment that does not trust the hosted relayer (regulatory restrictions, threat models where any third-party intermediary is unacceptable, or you want to verify the protocol works without us). - You have a separate signer that already has SOL and does not need privacy for itself — a treasury operator, an automation key, a service account. - You are integrating SolMask into another product and want to control submission timing yourself. ## What you give up | Property | Hosted relayer | Self-relay | |---|---|---| | Who pays gas | Relayer (absorbs the network gas cost) | Your signer | | Who pays the 0.003 SOL fee | Relayer (forwards to the fee destination) | Your signer (directly to the fee destination) | | Whose IP touches the RPC | Relayer's | Yours | | Recipient must hold SOL beforehand | No (USDC withdraws auto-convert) | Yes, for non-SOL withdraws — or you bundle a Jupiter swap manually | | Address-risk recipient pre-check | Yes (before broadcast) | No (your responsibility) | | Tx fits in one transaction | Always (relayer uses ALT) | Maybe; complex withdraws may exceed the 1232-byte limit without an ALT | The headline trade-off is the IP correlation. Even with a perfect zero-knowledge proof, a passive observer who can correlate your destination wallet's first appearance with your IP touching `api.mainnet-beta.solana.com` has the same information as if the chain had logged it explicitly. The hosted relayer breaks that correlation because the relayer's IP is what touches the RPC, and many users share the relayer. If you are running a Tor circuit, a residential VPN per session, or your own private RPC node, the IP-correlation concern shrinks. If you are submitting from your normal home or office connection, the hosted relayer is materially safer for your privacy. ## Custody and the recipient binding The relayer is **liveness-trusted, not asset-custodial in the general case**, but the precise guarantee depends on the withdraw shape: - **Plain same-asset withdraw, no swap, no in-asset tip** (e.g. a SOL→SOL self-relay, or any same-mint withdraw the client marks `enforce_recipient`): the proof binds the payout to your recipient on-chain, so the program pays your recipient directly and the relayer never holds the funds — it only fronts gas. - **Cross-asset (Jupiter) swaps, withdraws that recoup the fee as an in-asset tip, and native-SOL unwraps**: the released funds pass through a relayer-controlled escrow for the moment it takes to swap or forward them. In these cases the hosted relayer is trusted for liveness — it could, in principle, fail to forward. It can never release more than the proof's amount, and **self-relay removes this trust entirely.** If you do not want to trust the hosted relayer for a swap/tipped withdraw, self-relay and run the swap yourself. ## The withdraw instruction's account list The withdraw instruction's accounts: ```rust pub struct Withdraw<'info> { pub config: Account<'info, Config>, pub mint: InterfaceAccount<'info, Mint>, pub pool: Account<'info, Pool>, pub vault: InterfaceAccount<'info, TokenAccount>, pub merkle_frontier: Account<'info, MerkleFrontier>, pub root_history: Account<'info, RootHistory>, pub nullifier_record: Account<'info, NullifierRecord>, // init'd here pub fee_recipient_token_account: InterfaceAccount<'info, TokenAccount>, pub fee_collector: AccountInfo<'info>, pub relayer: Signer<'info>, // <-- this is YOU when self-relaying pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } ``` The `relayer` account is the signer who pays for the `init` of the nullifier PDA, pays compute units, and pays the 0.003 SOL protocol fee. In a self-relay flow, this is your wallet. ## Step-by-step procedure The web app's withdraw client is the reference implementation. The high-level shape: ### 1. Have the note ready You need the four pre-images of your commitment: `secret`, `nullifier_secret`, `amount`, `unlock_slot`. These are derived deterministically from your wallet — connect the same wallet you deposited with and SolMask re-derives them locally from the deposit's on-chain recovery blob (no passphrase, no note file). Export the decoded values from the web app's note view and load them into your script. ### 2. Fetch the current Merkle state ```ts import { Connection, PublicKey } from '@solana/web3.js'; import { Program } from '@coral-xyz/anchor'; const conn = new Connection(RPC_URL); const program = /* anchor Program built from the IDL + provider */; const poolPda = derivePool(MINT); const frontier = await program.account.merkleFrontier.fetch(deriveFrontier(poolPda)); // frontier.frontier is the array of right-most filled nodes per level // frontier.next_leaf_index is your insertion-slot count // Fetch the inclusion path for your specific leaf via the relayer's read API, // or rebuild it yourself by replaying all DepositEvents from the program. const path = await fetchInclusionPath(yourLeafIndex); ``` ### 3. Generate the proof in the browser (or your script) ```ts import { groth16 } from 'snarkjs'; const input = { // public root, nullifier_hash, recipient, fee_recipient, current_slot, mint, amount_to_recipient, change_commitment, // private secret, nullifier_secret, amount_total, unlock_slot, merkle_path: path.elements, merkle_path_indices: path.indices, change_secret, change_nullifier_secret, change_unlock_slot, }; const { proof, publicSignals } = await groth16.fullProve( input, '/circuits/withdraw.wasm', '/circuits/withdraw_final.zkey', ); ``` `current_slot` must be the slot you expect the transaction to land in. Use a recent slot and ensure the tx lands within the accepted drift window — `SlotMismatch` rejects anything outside it. The standard practice is: fetch the latest slot via `connection.getSlot()`, build for that slot, and submit immediately. If the tx fails with `SlotMismatch`, rebuild for the new latest slot. ### 4. Build and sign the transaction ```ts import { TransactionInstruction, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; const ix = await program.methods .withdraw( proofA, // [u8; 64] proofB, // [u8; 128] proofC, // [u8; 64] { root, nullifierHash, recipient, feeRecipient, currentSlot, mint, amountToRecipient, changeCommitment }, ) .accountsStrict({ config: configPda, mint: MINT, pool: poolPda, vault: poolVault, merkleFrontier: frontierPda, rootHistory: rootHistoryPda, nullifierRecord: deriveNullifier(poolPda, nullifierHash), feeRecipientTokenAccount: yourRecipientAta, feeCollector: config.feeCollector, relayer: yourWallet.publicKey, // YOU tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId, }) .instruction(); // Use the deployed Address Lookup Table to fit into a v0 transaction const alt = await conn.getAddressLookupTable(SOLMASK_ALT).then((r) => r.value!); const blockhash = (await conn.getLatestBlockhash()).blockhash; const msg = new TransactionMessage({ payerKey: yourWallet.publicKey, recentBlockhash: blockhash, instructions: [ix], }).compileToV0Message([alt]); const tx = new VersionedTransaction(msg); tx.sign([yourWallet]); ``` The Address Lookup Table is created at deploy time and includes the program ID, the token program, the system program, and the well-known pool/vault PDAs. Without it, complex withdraws (especially with a Jupiter swap appended) overflow the 1232-byte transaction size limit. ### 5. Submit and confirm ```ts const sig = await conn.sendRawTransaction(tx.serialize(), { skipPreflight: false, maxRetries: 3, }); await conn.confirmTransaction(sig, 'confirmed'); ``` On success: the protocol fee (3,000,000 lamports = 0.003 SOL) is debited from your wallet and transferred to the fee destination. The `amount_to_recipient` is debited from the pool's vault and credited to the fee-recipient token account. The nullifier record is created; the change commitment, if any, is appended to the tree. On `SlotMismatch`: rebuild for the new latest slot. On `UnknownRoot`: rebuild the proof against the current recent-root window. On `AccountAlreadyInitialized` (for the nullifier): your note was already spent. ## What you do NOT need - You do not need to register as a relayer with SolMask. There is no operator allowlist on `withdraw`; the `relayer` account is unconstrained beyond being a `Signer`. - You do not need any approval from SolMask ops. The protocol is permissionless on the withdraw side as long as the proof verifies. - You do not need to use the hosted relayer's quote API. The on-chain instruction is the source of truth; the quote is a UX layer on top. What you DO need is to construct a valid proof against a real note, and to budget ~0.0035 SOL per withdraw (the 0.003 SOL protocol fee plus a few hundred thousand lamports of tx fees + rent for the new nullifier PDA, which the `relayer` signer pays). -------------------------------------------------------------------------------- title: "Threat model" description: "What SolMask protects against, what it explicitly does not, and which protocol element provides each guarantee." last_updated: "2026-05-26" source: "https://solmask.xyz/en/docs/threat-model" -------------------------------------------------------------------------------- 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](/docs/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](/docs/relayer): 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. | Guarantee | Circuit layer | Protocol layer | |---|---|---| | Deposit ↔ withdraw unlinkability | Merkle-membership proof | cryptographic property (no on-chain check) | | Double-spend protection | nullifier bound to nullifier secret | nullifier-record PDA init | | Privacy delay | `unlock_slot ≤ current_slot` | `current_slot` within `[clock_slot − 150, clock_slot]` | | Cross-pool separation | — | mint check + per-pool nullifier PDA seeding | | Stale-root rejection | — | root-history window check | | Recipient binding | witness presence | Groth16 public input + runtime ATA check | | fee_recipient binding | witness presence | Groth16 public input + runtime ATA check | | mint binding | witness presence | Groth16 public input + mint check | | Banlist enforcement | — | ban-record check at deposit | | Emergency stop | — | paused-flag constraint on deposit + withdraw | A claim not in this table is not a claim SolMask makes. -------------------------------------------------------------------------------- title: "Trusted setup" description: "The Groth16 phase-2 ceremony behind SolMask's proving and verification keys, why a public beacon is used, and how to verify the artifacts yourself." last_updated: "2026-05-26" source: "https://solmask.xyz/en/docs/trusted-setup" -------------------------------------------------------------------------------- Groth16 needs a per-circuit phase-2 trusted setup. That setup produces the proving key the browser uses and the verification key the on-chain verifier embeds. If anyone who participated in the setup retained their contribution randomness — the so-called "toxic waste" — they could forge withdraw proofs. The point of a public ceremony is that the result is independently verifiable and the trust assumption is explicit. ## What the ceremony produced A phase-2 ceremony anchored to a public **Bitcoin-block beacon**. The beacon's role is unpredictability: because the block was mined *after* the contribution was recorded, no contributor could have known the future block hash, so no contributor could have crafted their entropy to bias the result. | Input | Artifact | |---|---| | Phase-1 input | Hermez Powers of Tau (`powersOfTau28_hez_final_18.ptau`), the public, multi-party Hermez ceremony | | Compiled circuit | the withdraw circuit's R1CS | | Beacon | a public Bitcoin block hash | | Proving key | `withdraw_final.zkey` — served to the browser | | Verification key | `verification_key.json` — embedded as constants in the on-chain verifier | Phase-1 reuse of the Hermez Powers of Tau is industry standard: it is a large, public, already-multi-party ceremony. ## Roadmap A public multi-party MPC ceremony — many independent contributors — is planned as the protocol matures. Distributing the setup across many independent contributors makes the trust assumption robust. The migration produces a new proving key and verification key, deployed together: the on-chain verifying key and the client-side proving key must come from the same ceremony, or proofs fail at submission. ## How to verify the ceremony yourself The whole point of a public ceremony is that anyone can replay the math: you can independently verify that the final proving key corresponds to the public phase-1 input and the published circuit. ### Step 1: Get the public inputs ```bash # The phase-1 Powers of Tau file (the Hermez ceremony, depth 18) curl -fL https://storage.googleapis.com/zkevm/ptau/powersOfTau28_hez_final_18.ptau \ -o pot18_final.ptau # Compile the circuit's R1CS from the published circuit source circom withdraw.circom --r1cs --wasm --sym -o build/ ``` ### Step 2: Verify with snarkjs ```bash yarn dlx snarkjs zkey verify build/withdraw.r1cs pot18_final.ptau withdraw_final.zkey ``` A passing verification means the final proving key is a valid Groth16 setup for this exact circuit, derived from this exact phase-1 file, with a valid history of contributions and beacons. It does NOT prove that a contributor discarded their entropy — that part is unverifiable for any phase-2 ceremony and is exactly what a multi-party MPC ceremony addresses. ### Step 3: Verify the on-chain verifying key matches The verification key embedded in the on-chain verifier is generated deterministically from the published `verification_key.json`. Re-running the same generation against the published JSON and diffing against the deployed verifier confirms the on-chain verifier uses the same key as the proving key — the three artifacts form a chain you can walk end-to-end. ### Step 4: Verify the beacon The beacon is a public Bitcoin block hash, fetchable from any block explorer. Hashing it into the snarkjs beacon step and confirming the result matches the published `withdraw_final.zkey` closes the loop. ## Threat model notes | Risk | Mitigation | |---|---| | Contributor retains entropy | Beacon randomization is unpredictable at contribution time; a public multi-party MPC ceremony distributes trust across many independent contributors | | Beacon chosen too late | The beacon block is fixed publicly at finalize time | | Compromised phase-1 input | Reuse of the large, public, multi-party Hermez Powers of Tau | | Mismatched proving key ↔ verifying key | The artifacts are diff-able end to end (Step 3) | If you find a discrepancy at any step of the verification, file an issue and **do not deposit**. The verification artifacts are the only thing standing between honest users and a forged-proof drain. Treat them like you would a kernel checksum. -------------------------------------------------------------------------------- title: "How to choose a recipient address" description: "The withdraw address is the easiest place to leak your identity. Here's how to generate a fresh wallet, fund it correctly, and avoid the common mistakes that turn a private transfer into a public one." last_updated: "2026-05-26" source: "https://solmask.xyz/en/learn/choosing-a-recipient-address" -------------------------------------------------------------------------------- # How to choose a recipient address The privacy you get from SolMask is bounded by the worst-linked address in the transaction. The cryptography can break the on-chain link between your deposit and your withdraw, but it cannot un-leak an address that's already publicly tied to you. The withdraw destination is the easiest place to make this mistake, and it's the one we see most often from new users. Picking the right recipient address is the single most important step in actually getting the privacy you came for. ## The whole point is a fresh address A fresh address is one that has no on-chain history at all before your withdraw lands. No incoming deposits from your CEX. No outgoing votes in any DAO. No interactions with your main wallet. Nothing. If you withdraw to a wallet that the chain already associates with you, an observer doesn't even need to attack the SolMask pool. They can just look at the destination of your withdraw, see that it's "your" wallet by some prior off-chain knowledge or on-chain heuristic, and conclude that whoever deposited into SolMask with a matching amount and timing was probably also you. The shielded pool's math is undamaged; your operational security is what failed. Concretely, here are the addresses you should not withdraw to: Your CEX deposit address. Exchanges KYC their users and will happily tell anyone with a subpoena who that address belongs to. Withdrawing from SolMask to a CEX deposit is a textbook deanonymisation. Your existing main wallet. If you've ever named it on Twitter, used it for an NFT mint, voted in a DAO, or received airdrops, it's effectively a public identifier. Withdrawing the same amount you recently deposited from a different wallet ties the two together. Any wallet that has interacted with the wallet you deposited from. Same-cluster heuristics in chain-analytics tools will flag the two as related, regardless of what passed between them. ## Generating a fresh wallet The simplest path is to create a new wallet in your existing wallet software (Phantom, Backpack, Solflare) and use it exclusively for the SolMask withdraw. Most wallets let you derive multiple accounts from the same seed phrase; the on-chain addresses are independent, so an observer cannot tell they share a seed. If you want stronger separation, generate the keypair in a fully separate wallet application, or with `solana-keygen new` on a clean machine. Save the seed phrase somewhere safe before you do anything else. If you lose it, the funds you withdraw to that address are gone. The new wallet has no SOL, no tokens, no history. That's exactly what you want. ## You'll need a little SOL for fees SolMask's relayer handles the gas for the withdraw transaction itself, so the fresh wallet does not need to be pre-funded to receive a withdraw. The 0.003 SOL withdraw fee is paid by the relayer up front and recouped from the withdraw amount, not from your wallet's balance. What the fresh wallet **does** need SOL for is whatever you plan to do next. If you want to swap the withdrawn USDC for something else, you'll need a tiny amount of SOL for transaction fees. If you want to forward the funds elsewhere, same story. The amount is small — a hundredth of a SOL is plenty for ordinary use — but it has to come from somewhere. This is the moment most people accidentally compromise themselves. The natural instinct is to send 0.01 SOL from your main wallet to the fresh wallet "just to cover gas." Doing that re-links the two addresses on-chain in the most obvious way possible. You just spent the privacy you were buying. The clean solution is to fund the fresh wallet's gas with a second SolMask withdraw, from a separate deposit, ideally of SOL. Two small SOL deposits feed two unrelated fresh wallets: one to receive your main funds, one to provide gas. Both are unlinkable to your origin. A cheaper-and-easier alternative: use the relayer's built-in SOL-gas tip. The SolMask UI offers a checkbox at withdraw time that requests the relayer leave a small amount of SOL in the destination ATA on top of the token withdraw. This works when the asset you're withdrawing has Jupiter liquidity, because the relayer can route a portion through a SOL conversion in the same transaction. ## Don't reuse the fresh wallet Once you've received a withdraw to a fresh wallet, the wallet has on-chain history. It is no longer fresh. Treat it as single-use for the purpose of unlinking from your origin: use it for the immediate next transaction (a swap, a forward, a payment), and don't bring it back later for a second SolMask withdraw. A fresh wallet is fresh exactly once. If you do this consistently — fresh wallet, used once, funded cleanly — the on-chain picture an observer sees is two unrelated wallets making two unrelated transactions, with no provable bridge between them. That's the goal. -------------------------------------------------------------------------------- title: "What are the fees and where do they go?" description: "SolMask deposits are free. The protocol fee — a small percentage in the withdrawn asset plus a flat 0.003 SOL — is charged at withdraw time. Here's a worked example, who pays what, and where the fees end up." last_updated: "2026-05-31" source: "https://solmask.xyz/en/learn/fees-and-where-they-go" -------------------------------------------------------------------------------- # What are the fees and where do they go? **Depositing into SolMask is free.** The protocol charges its fee when you withdraw, in two parts — both on-chain, both enforced by the program, both admin-tunable. There are no hidden charges, no spread on the swap path, and no per-asset variation. The numbers below are enforced on-chain by the protocol. ## Deposits: free When you deposit into a SolMask pool, the full amount enters the shielded vault. Nothing is skimmed on the way in — the amount you commit to in your shielded note is exactly what you deposited. Deposit 1 SOL and your note is worth 1 SOL; deposit 1,000 USDC and your note is worth 1,000 USDC. ## The withdraw percentage fee: in the withdrawn asset When you withdraw, the protocol charges a small percentage — `Config.withdraw_fee_bps`, an admin-tunable rate capped at 100 basis points (1.00%). At the recommended 23 bps (0.23%) it works out to `fee = withdrawn_amount * 23 / 10_000`. The fee is taken in whatever asset you're withdrawing — SOL from a SOL withdraw, USDC from a USDC withdraw — and accrues to a per-pool **token fee vault** PDA owned by the pool. This percentage is **bound inside the withdraw zero-knowledge proof**: the circuit proves that your spent notes equal the released output plus the fee plus any change, and the program independently recomputes the expected fee from the on-chain rate and rejects any mismatch. Neither you nor the relayer can understate it. ## The withdraw flat fee: a flat 0.003 SOL On top of the percentage, every withdraw pays a flat protocol fee that **defaults to 0.003 SOL** — `Config.withdraw_fee_lamports`. It is an admin-tunable on-chain value (capped at 0.1 SOL), not a hardcoded constant, and it does not scale with the withdrawn amount. Mechanically, the relayer that submits your withdraw pays that SOL fee out of its own wallet at execution time and recoups exactly it by keeping a small tip slice — in the withdrawn asset — out of the released amount before routing the rest to your recipient. The Solana network gas is a separate cost the relayer absorbs; it is not added to your fee. This design is what lets you withdraw to a freshly created, unfunded wallet — the receiving wallet needs no SOL of its own. ## A worked example, end to end Say you deposit **1.0000 SOL** into the SOL pool. The deposit is free: the full 1.0000 SOL enters the pool vault, and your shielded note records 1.0000 SOL as the spendable amount. You wait through your privacy delay. When you withdraw the 1 SOL (at the recommended 23 bps), the protocol takes ~0.0023 SOL as the percentage fee into the pool's token fee vault and 0.003 SOL as the flat fee to the fee collector. Your recipient lands roughly **0.9947 SOL**. Total cost on a 1 SOL round-trip: about 0.0053 SOL, or ~53 basis points — but you pay none of it until you exit. On a 100 SOL withdraw the percentage dominates and the flat fee is negligible (~23.3 bps total). On a 0.1 SOL withdraw the flat fee dominates (~3.2%). SolMask is more cost-effective for larger amounts. ## Where the fees ultimately go Both fees ultimately funnel to a single on-chain account — `Config.fee_collector` — configured by the SolMask admin at deployment. The flat SOL fee goes there directly, in SOL, on every withdraw. The percentage fees accumulate in per-pool `fee_vault` accounts in the pool's native asset, and are swept by the admin to the fee collector when a pool's vault crosses a configurable `sweep_threshold`. Sweeping is a manual operation, not automatic. The fee collector destination is published in the deployed `Config` PDA, and both fee rates (`withdraw_fee_bps` and `withdraw_fee_lamports`) are readable there too. You can verify where the fees go and what they are without trusting our word for it. ## What the fees pay for The withdraw fees are the protocol's revenue: they pay for the infrastructure (the relayer, the indexer, the trusted-setup ceremony costs) and for ongoing development. They are not network gas fees — the actual Solana priority fees on your transactions are separate and go to validators in the normal way. Charging on exit rather than entry means deposits cost nothing, and you only pay when value actually leaves the pool. Because the flat portion is fixed, it makes very small withdraws economically unattractive but doesn't penalise large ones — which keeps the anonymity set populated with meaningful amounts rather than dust. There are no other fees. No subscription, no deposit fee, no withholding on the swap side beyond what Jupiter itself charges for cross-asset routing. What you see in the UI is what you pay. -------------------------------------------------------------------------------- title: "How zero-knowledge proofs work (without the math)" description: "An accessible explanation of zero-knowledge proofs and how SolMask uses Groth16 + Poseidon Merkle trees to hide the link between deposits and withdrawals." last_updated: "2026-05-25" source: "https://solmask.xyz/en/learn/how-zk-proofs-work" -------------------------------------------------------------------------------- # How zero-knowledge proofs work A zero-knowledge proof lets you prove a statement is true **without revealing why it's true.** The canonical example: imagine a colorblind friend holding two identical-looking balls — one red, one green. You can prove to them that the balls really are different colors, without ever telling them which is which. You do this by having them swap (or not swap) the balls behind their back, then telling them whether they swapped. After enough rounds, they're convinced you can tell red from green, but they still don't know which ball is which color. That's the whole idea. SolMask uses zero-knowledge proofs for one specific statement: > "I know a secret that corresponds to one of the deposits in this shielded pool, and I've authorized a withdrawal of N tokens to wallet X." The proof says nothing about *which* deposit. The chain sees a valid withdrawal but cannot match it to any specific deposit. ## What goes into the proof When you deposited, SolMask computed a commitment: ``` commitment = Poseidon(secret, nullifier_secret, amount, unlock_slot) ``` Only the commitment lands on-chain. The four inputs — your secret, the nullifier, the amount, and the time you've agreed to wait — never leave your browser. They aren't saved to a file you have to guard: your wallet re-derives them on demand by signing a fixed message, and the deposit publishes a wallet-encrypted recovery blob on-chain so the same wallet can rediscover the note anywhere. When you withdraw, your browser generates a Groth16 proof that, in math terms: 1. The commitment is one of the leaves in the pool's Merkle tree (without saying which leaf). 2. The nullifier you're revealing now is the hash of the nullifier secret you committed to. 3. Enough time has passed (`current_slot >= unlock_slot`). 4. The amount you're withdrawing is consistent with the commitment's value. 5. Any change is hashed into a fresh commitment you've added to the tree. The proof is ~200 bytes. The Solana program verifies it in a few milliseconds. ## Why nullifiers? Each deposit comes with a unique secret. When you withdraw, you reveal a *hash* of that secret — the **nullifier**. The chain records every nullifier seen so far; if you try to withdraw the same deposit twice, the second attempt reveals the same nullifier and is rejected. Crucially, the nullifier is generated from your private secret — so only *you* can produce it for your deposit, but the chain can verify it without learning the secret itself. ## Why Poseidon? Standard hashes like SHA-256 are wildly expensive to express as zero-knowledge proofs (think: hundreds of thousands of constraints). Poseidon is a hash designed to be cheap inside ZK circuits — about 5× fewer constraints than Keccak for the same security level. The trade-off: it's slower than SHA-256 if you computed it on a CPU directly, which doesn't matter for our use case. ## Why Groth16? Groth16 is the most production-tested proving system in ZK. Its proofs are tiny (3 elliptic-curve points, ~200 bytes) and verify in constant time. The catch: it needs a one-time **trusted setup ceremony** to generate public parameters. SolMask's ceremony is documented in [our trusted setup notes](/docs/trusted-setup). ## What you don't need to trust You don't need to trust SolMask's operators. The verifier code is on-chain and public. The circuit is public and reproducible. The relayer can refuse to broadcast your withdraw, but it cannot steal your funds — the proof binds the recipient address, so any attempt to redirect would invalidate it. ## What you do need to trust The trusted setup. If anyone who participated in the ceremony retained the secret "toxic waste", they could forge proofs and drain the pool. SolMask's ceremony uses a public Bitcoin-block beacon as a randomness anchor; a public multi-party ceremony is planned as the protocol matures. See [Trusted setup](/docs/trusted-setup). → [Launch the app](/) -------------------------------------------------------------------------------- title: "The Tornado Cash alternative on Solana" description: "Tornado Cash was the best-known way to make a transfer private on Ethereum. SolMask brings the same shielded-pool idea to Solana — and adds the compliance and usability pieces a modern protocol needs." last_updated: "2026-05-31" source: "https://solmask.xyz/en/learn/tornado-cash-solana-alternative" -------------------------------------------------------------------------------- If you searched for a "Tornado Cash for Solana," you already understand the problem you are trying to solve: every Solana transfer is public, and you want to move funds from one wallet to another **without leaving an on-chain link between them.** This page explains where that idea came from, why a straight copy of Tornado Cash is the wrong thing to want in 2026, and how SolMask delivers the same privacy with the pieces a modern protocol needs. ## What Tornado Cash actually did Tornado Cash was a set of smart contracts on Ethereum. You deposited a fixed amount — say 1 ETH — into a pool, and the contract recorded a cryptographic commitment to a secret only you held. Later, you could withdraw that 1 ETH to a brand-new address by submitting a **zero-knowledge proof** that you owned one of the secrets in the pool, *without revealing which one*. Because hundreds of other people had deposited the same fixed amount, an observer could not tell which deposit your withdrawal corresponded to. The link between your old wallet and your new one was broken. That core mechanism — a **shielded pool** built from commitments, nullifiers (to stop double-spends), a Merkle tree, and a zero-knowledge proof — is genuinely good cryptography. It is the foundation that SolMask and every other serious privacy pool still builds on today. ## Why you don't want a literal Tornado Cash clone Two things changed after Tornado Cash launched, and both matter for what you should use now. **First, fixed denominations are clumsy.** The original design forced you into fixed amounts (1, 10, 100 ETH). Moving an arbitrary balance meant juggling multiple deposits and withdrawals, and any odd amount stood out. Modern pools use a **note model** with arbitrary amounts and change, the same way physical cash lets you break a note and get change back. **Second, compliance is now table stakes.** In August 2022, the U.S. Office of Foreign Assets Control sanctioned the Tornado Cash contracts, citing their use by sanctioned actors to launder funds. The lesson the industry took away was not "privacy is illegal" — privacy is a legitimate and legal need — but that a pool with **no screening and no ability to keep bad actors out** becomes a target and a liability for everyone in it. A protocol built today has to be privacy-preserving for ordinary users *and* able to refuse known-bad addresses. ## How SolMask is the Solana alternative SolMask is a shielded pool on Solana that keeps the proven Tornado Cash mechanism and fixes both of the problems above. - **Arbitrary amounts, not fixed denominations.** SolMask uses an **atomic join-split**: a single zero-knowledge proof can spend up to four of your notes, pay up to two recipients, and write a shielded change note back into the pool — in one transaction, for any amounts. You are never forced into round numbers. - **Compliance built in.** SolMask screens addresses with risk intelligence (CipherOwl) and enforces an **on-chain banlist** at the program boundary, so listed wallets cannot deposit in the first place. Privacy for legitimate users; a door that closes on known-bad actors. - **Non-custodial and client-side.** Your note secrets are derived from your wallet signature and never leave your device. SolMask — and the relayer that submits your withdrawal so you don't need gas in the fresh wallet — never holds your spend secret. - **Built for Solana economics.** Deposits and withdrawals settle in seconds for a fraction of a cent, and cross-asset withdrawals can route through Jupiter so you can deposit SOL and have a recipient receive USDC. If you want the deeper mechanics, read [what a shielded pool is](/learn/what-is-a-shielded-pool) and [how ZK proofs work](/learn/how-zk-proofs-work). ## How to make a private transfer on Solana 1. **Deposit.** Connect your wallet, pick SOL, USDC, or USDT, choose an amount and a privacy delay, and sign. Your note is created locally; its commitment goes into the pool. 2. **Wait.** The delay is what grows your anonymity set — every other deposit in the same window becomes a plausible source for your withdrawal. Longer is stronger. 3. **Withdraw to a fresh wallet.** Reconnect, paste a recipient address that has no public link to you, and SolMask builds the proof in your browser. The recipient receives funds with no on-chain edge back to your deposit. ## Use privacy responsibly Financial privacy is legitimate — protecting your salary, your business counterparties, and your safety are normal reasons to not broadcast every transfer. SolMask is built for those uses, which is exactly why it screens addresses and enforces a banlist. It is not a tool for evading sanctions or laundering proceeds of crime, and the compliance layer exists to keep it that way. Read [what SolMask cannot protect you from](/learn/what-solmask-cannot-protect-you-from) so your expectations match reality. Ready to try it? [Start with the interactive tutorial](/tutorial) or open the app and make a small first transfer. -------------------------------------------------------------------------------- title: "How do I verify my deposit landed?" description: "Three independent checks you can run after depositing into SolMask: the on-chain confirmation, the relayer's leaf endpoint, and wallet recovery from the on-chain encrypted blob. None of them require trusting our UI." last_updated: "2026-05-26" source: "https://solmask.xyz/en/learn/verifying-your-deposit" -------------------------------------------------------------------------------- # How do I verify my deposit landed? After you click deposit, three independent artifacts come into existence. Any one of them is enough to confirm your funds are in the pool. Together they let you verify the deposit without trusting the SolMask UI at all — which is the right level of paranoia for a privacy protocol. You should check at least the first and the third. If you're being careful, check all three. ## Check one: the transaction signature on-chain The deposit instruction is a normal Solana transaction. When it confirms, the UI shows you a signature — a base58 string roughly 88 characters long. That signature is the canonical receipt that the deposit happened. You can verify it with the Solana CLI: ``` solana confirm ``` A `Finalized` response means the transaction is permanent and irreversible. Anything else (`Confirmed`, `Processed`, or no result) means the cluster is still catching up; wait a few more seconds. You can also plug the signature into any Solana block explorer — Solscan, SolanaFM, or the official explorer.solana.com — and inspect the instruction. You should see a single CPI that transfers your asset from your wallet's token account to the pool's `vault` — the full amount, since deposits are fee-free (the protocol fee is charged at withdraw time). You should also see a `DepositEvent` log line that includes your commitment hash, the leaf index, the amount that was actually shielded, and a wallet-encrypted recovery blob (`encrypted_note`) — the ciphertext that lets the same wallet rediscover this note on any device. If the transaction is on-chain and finalized, your funds are in the pool. The cryptography doesn't depend on the UI's belief about what happened — it depends on what's actually on-chain. ## Check two: the leaf in the relayer's Merkle endpoint Within a few seconds of confirmation, the SolMask indexer picks up your deposit's `DepositEvent` and adds the commitment to its mirror of the Merkle tree. You can verify this directly by hitting the relayer's leaves endpoint for your asset's mint: ``` GET /merkle//leaves?from=&limit=1 ``` The leaf index was printed in the deposit event and shown in the UI's "deposit succeeded" toast. The response is a small JSON blob containing your commitment as a hex string and the slot it was indexed at. If the commitment in the JSON matches the commitment the UI showed you at deposit time, the indexer has it and your withdraw will be possible as soon as your unlock slot is reached. On a healthy mainnet RPC the indexer typically catches up within about five seconds of finalization. Quiet pools and weekend traffic dips can push that up to fifteen or twenty seconds. If your leaf hasn't appeared after a minute, the relayer may be lagging — your deposit is still safe on-chain, but you'll need to wait for the indexer to catch up before you can withdraw. There's a parallel endpoint that returns just the current `tree_size` of a pool — useful if you want to confirm your leaf index is within the live tree's bounds, without enumerating leaves. ## Check three: wallet recovery This is the check that proves you can actually *spend* the deposit later — and unlike older designs, there is no file to keep and no passphrase to remember. When you deposit, your browser derives the note's four secrets — the spend secret, the nullifier secret, the amount, and the unlock slot — from your wallet. It does this by asking the wallet to sign one fixed message; the signature is deterministic, so the same wallet always reproduces the same secrets, and nothing is written to a file. The deposit also encrypts a small recovery blob — `{ leaf_index, amount, unlock_slot }` — to a key only your wallet can derive, and that ciphertext is published on-chain inside the `DepositEvent` as `encrypted_note`. To verify recovery works, disconnect and reconnect your wallet (or open the app in a fresh browser profile) and let it re-scan. SolMask fetches every recovery blob for the pool, trial-decrypts them, and the ones encrypted to your wallet light up your balance — including the deposit you just made. If your balance appears on a device that never saw the deposit, recovery is confirmed end-to-end. Because the authority is your wallet, the failure mode is different from a passphrase scheme: there is nothing to lose except access to the wallet itself. Keep the wallet's seed phrase safe the way you always would — that single secret reconstructs every note. SolMask stores nothing that could recover your notes; only your wallet can decrypt them, which is exactly what keeps the protocol non-custodial. ## What "verified" looks like You've verified a deposit when all three of these are true: the transaction signature finalizes on-chain, the relayer's leaves endpoint returns your commitment at your leaf index, and your balance reappears when you reconnect the same wallet — proving the on-chain recovery blob decrypts for you. At that point you can close the tab. The pool will hold your funds for as long as you like — there is no expiry — and the only thing standing between you and a withdraw is the privacy delay you chose at deposit time, plus reconnecting the same wallet when you're ready to go, which re-derives the note automatically. -------------------------------------------------------------------------------- title: "What is a shielded pool?" description: "A shielded pool is an on-chain smart contract that holds everyone's deposits together and breaks the link between deposits and withdrawals. Here's how it works in plain English." last_updated: "2026-05-26" source: "https://solmask.xyz/en/learn/what-is-a-shielded-pool" -------------------------------------------------------------------------------- # What is a shielded pool? A shielded pool is the part of SolMask that holds the money. Every deposit goes into the same on-chain account, and every withdraw comes out of that same account. The pool itself is fully public — you can read its balance, see every deposit transaction, and see every withdraw transaction. What you cannot read is **which deposit corresponds to which withdraw.** That single missing piece is the entire product. ## A shared vault, with no operator to trust The intuition is pooling. Imagine everyone deposits the same denomination of identical bills into one shared vault, and later each person withdraws the same amount back out. Because the bills are commingled, an outside observer watching the vault can't tell which bills came from whom — as long as whoever holds the vault keeps no records. The problem with the old-world version of this is the operator holding the vault. They know exactly who deposited what and who withdrew what. If they keep records — or get subpoenaed for them — the privacy collapses retroactively. A shielded pool is that shared vault with **nobody holding it.** The "operator" is a smart contract on Solana. It has no human in the loop, no off-chain database of who-deposited-what, and no ability to censor or seize funds. Its rules are public, its code is public, and the only thing it can do is what the rules let it do. When you deposit into SolMask's SOL pool, your SOL joins the same vault as every other depositor's SOL. When you withdraw, the vault sends SOL to your destination address. The vault's bookkeeping doesn't track "who owns which SOL"; it only tracks "how much total SOL is in here, and how much can be legitimately withdrawn." Your right to withdraw is proven cryptographically (see [how zero-knowledge proofs work](/learn/how-zk-proofs-work)) rather than by name. ## Both ends are on-chain. Only the middle is hidden. A subtle point that trips up new users: SolMask does **not** hide your deposit. Your deposit transaction is a perfectly ordinary Solana transaction. It shows your wallet, the amount, and the pool address it went to. Anyone reading the chain can see that wallet A sent 5 SOL to the SolMask pool at slot 312,488,712. Similarly, SolMask does not hide your withdraw. The withdraw is also a perfectly ordinary Solana transaction. It shows the pool address paying out some amount of SOL to recipient wallet B. What's hidden is the **link**. There's no field in either transaction, no shared identifier, no signature pattern, no timing fingerprint (if you wait long enough) that ties A's deposit to B's withdraw. To an observer, you have a public list of deposits, a public list of withdraws, and a mathematically enforced shuffle in between. This is why privacy in a shielded pool depends on the crowd. If you're the only person who ever deposited 5.000 SOL and then someone withdraws 5.000 SOL an hour later, you've identified yourself by elimination. The pool's job is to keep enough activity flowing that any given withdraw could plausibly correspond to any of dozens or hundreds of recent deposits. ## What the contract actually stores Concretely, when you deposit, SolMask writes a single 32-byte hash — a **commitment** — into an on-chain Merkle tree. The commitment is a one-way function of four secrets your browser derives from your wallet: a spend secret, a nullifier secret, the deposit amount, and the unlock slot. You don't choose or store them — the same wallet reproduces them deterministically whenever you reconnect. From the commitment alone, nothing can be reconstructed. When you withdraw, your browser generates a proof that says "I know one of the commitments in the tree, and I'm authorizing a withdraw of X tokens to address Y." The proof reveals X and Y, but it does not reveal which commitment it's about. The chain stores a small marker called a **nullifier** to prevent the same deposit from being withdrawn twice, and that's the end of the transaction. ## What this gets you, and what it doesn't A shielded pool gives you on-chain unlinkability between your sending wallet and your receiving wallet. It does not give you off-chain anonymity. If you tell the world "I just deposited into SolMask" on Twitter, the cryptography can't help you. If the address you withdraw to is already public as yours, the cryptography can't help you. And if you withdraw the same unusual amount that you deposited thirty seconds earlier, the math says you're hidden but the timing says you're not. The shielded pool is the mathematical engine. Using it well is its own skill, and we cover that in the rest of the Learn section. -------------------------------------------------------------------------------- title: "What is SolMask?" description: "SolMask is a privacy protocol on Solana. Deposit assets into a shielded pool, withdraw later to a fresh address — with no on-chain link between the two." last_updated: "2026-05-25" source: "https://solmask.xyz/en/learn/what-is-solmask" -------------------------------------------------------------------------------- # What is SolMask? SolMask is a privacy protocol on Solana. It lets you send and swap SOL, USDC, USDT and other tokens **without revealing the on-chain link between your sending wallet and your receiving wallet.** If you have ever wished you could move funds from one Solana wallet to another without making the transfer publicly visible on the blockchain — SolMask is built for that. ## How transfers normally work on Solana Every Solana transfer is permanently recorded on a public ledger. Anyone — an exchange, a chain-analytics firm, a curious onlooker — can read it. They can see: - Which wallet sent the funds - Which wallet received them - Exactly how much, in which token, and when That works fine for some use cases. But payroll, treasury operations, donations, B2B settlement, OTC trades, and many ordinary personal transactions don't benefit from being broadcast to the world. ## How SolMask works Three pieces: 1. **Deposit.** You pick an asset, an amount, and a privacy delay (anywhere from 10 minutes to a week). SolMask generates a shielded note locally in your browser and writes only its commitment — a cryptographic hash — to the chain. The link between your wallet and the note is public; the link between the note and its eventual withdraw is what we hide. 2. **Wait.** While your note sits in the shielded pool, other deposits land alongside it. By the time you withdraw, you're hidden in a crowd. Longer wait times mean larger crowds and stronger privacy. 3. **Withdraw.** You generate a zero-knowledge proof in your browser — a short cryptographic statement that says "I own one of the notes in this pool, but I won't tell you which one." A relayer submits the withdraw transaction, paying gas on your behalf, so your destination wallet never has to be funded by your origin wallet. Optionally, the same transaction can swap your withdrawn asset to a different one via Jupiter. The chain sees: a deposit from wallet A, and a withdraw to wallet B. It can't tell whether they're the same person or not. ## What SolMask is not - **Not a tool for laundering tainted funds.** Wallets flagged as sanctioned or high-risk by an address-risk screening provider are blocked at the deposit instruction. SolMask exists to give honest users privacy — to protect payroll, treasury operations, and ordinary personal transactions from public surveillance — not to help bad actors obscure proceeds of crime. - **Not an absolute privacy guarantee.** Privacy depends on the size of the anonymity set. If you're one of three depositors in an asset, three of you have the same withdraw — the anonymity-set math is weak. SolMask's UI warns you when a deposit amount and asset don't yet have a healthy crowd. - **Not custodial.** Your secrets are derived from your wallet — no passphrase, nothing to back up. The deposit publishes a wallet-encrypted recovery blob on-chain, so reconnecting the same wallet on any device restores your notes. Only your wallet can decrypt or spend them; lose access to the wallet itself and SolMask cannot recover them. ## Supported assets SOL, USDC, USDT, plus any SPL token the admin adds to the registry. Cross-asset swaps inside the withdraw transaction use Jupiter v6. ## What's next - [How zero-knowledge proofs work](/learn/how-zk-proofs-work) — the math behind the privacy, in plain English. - [Launch the app](/) -------------------------------------------------------------------------------- title: "What SolMask cannot protect you from" description: "An honest accounting of the threats SolMask's cryptography does not defend against. Read this before you assume the protocol is doing more than it is." last_updated: "2026-05-26" source: "https://solmask.xyz/en/learn/what-solmask-cannot-protect-you-from" -------------------------------------------------------------------------------- # What SolMask cannot protect you from The honest version of any privacy protocol's pitch includes a list of attacks it does not defend against. We'd rather you read that list now than discover it the hard way. SolMask breaks the on-chain link between a deposit and a withdraw using zero-knowledge proofs over a shielded pool. That is a real, mathematically grounded property. What follows is what that property does **not** include. ## Timing correlation if you withdraw too quickly The privacy delay is a minimum, not a recommendation. If you deposit a distinctive amount and then withdraw the same amount one hour and one slot later — exactly the minimum the protocol allows — an observer with a stopwatch and a chain explorer will probably guess correctly that the two are linked. The math says the chain doesn't know; the timing says you do. A passive timing-correlation attack costs nothing to run. Anyone with access to a Solana RPC can do it retroactively, days or months later, by aligning the public list of deposits and the public list of withdraws. The defence is your delay choice and the size of the anonymity set in the window you waited through. The protocol cannot pick those for you. If your deposit amount is unusual (say, 7.3219 SOL), the timing problem gets sharper still, because the universe of plausibly matching withdraws is much narrower than it is for round amounts. Use round numbers, use the longest delay you can stomach, and avoid the temptation to withdraw the moment your unlock slot ticks over. ## RPC-level IP tracking When your browser talks to a Solana RPC endpoint to build your deposit or withdraw transaction, your IP address is visible to the RPC operator. The same is true of any indexer your wallet pings, the relayer your withdraw routes through, and any third-party analytics your wallet quietly phones home to. The blockchain doesn't record IPs, but the infrastructure that ferries your transaction to the chain does. A determined adversary who can correlate RPC logs from the wallet that deposited and from the fresh wallet that withdrew may be able to tie them together, even though the chain itself doesn't. The defences here are not cryptographic. They live at the network layer: route your traffic through Tor or a non-logging VPN, ideally using different exit nodes for your deposit and withdraw sessions; avoid running both sessions back-to-back from the same IP; consider using a self-hosted RPC or one with documented log-retention policies. We provide the math; you bring the network hygiene. ## Recipient address linkability This one is covered in detail in [how to choose a recipient address](/learn/choosing-a-recipient-address), but it bears repeating here. If the wallet you withdraw to has any on-chain history that ties it to your real identity — your exchange deposit address, your main wallet, your DAO voter address — the privacy of the SolMask withdraw collapses. The chain doesn't know who you are. But it knows everything about what every address has ever done. If a target address is publicly "yours" for any reason, the withdraw to that address is effectively a withdraw to your name. A fresh, never-used wallet, funded cleanly without touching your origin wallet, is the only configuration where the cryptography buys you what it claims to. ## Admin compromise in v1 SolMask has an admin account. The admin can pause the protocol in an emergency, add or remove supported assets, ban specific wallets at the deposit layer (as part of the high-risk-address controls), and sweep accumulated fees. The admin **cannot** decrypt your notes, alter your withdraw, freeze a specific deposit, or extract funds from a pool without going through the same proof-verifying withdraw instruction that every user uses. But "pause" is a real lever. If the admin key is compromised — or if the holder is coerced — withdraws can be halted across the whole protocol until the admin acts again. Your funds are not lost in this scenario; they remain in the pool, recoverable when the pause is lifted. They are, however, illiquid until then. Admin authority is held by a multisig, with governance hardening over time. Size your usage to your comfort with the admin's pause power. ## Trusted-setup compromise before the v2 ceremony Groth16, the proof system SolMask uses, requires a one-time **trusted setup** to generate the public parameters every proof refers to. If anyone who participated in the setup retained the secret randomness ("toxic waste") used during the ceremony, that person could in principle forge proofs and drain the pool. The ceremony's soundness rests on contributors discarding their entropy. A public Bitcoin-block beacon serves as the randomness anchor and makes the ceremony non-replayable. A public multi-party ceremony — many independent participants — is planned as the protocol matures. A multi-party ceremony distributes trust across many independent contributors, so no single party can compromise the setup. See [Trusted setup](/docs/trusted-setup) for the verification procedure and roadmap. ## Side channels we haven't thought of Privacy systems fail in creative ways. We have done what we can to test for the standard pitfalls — amount fingerprints, timing leaks, anonymity-set degradation, replay attacks — but the history of this corner of cryptography is mostly the history of unexpected leaks. We invite anyone to look at our circuits, our verifier, and our relayer code and tell us what we missed. If you find something, please report it before publishing. The cryptography is strong. The system around the cryptography is what you should worry about. Use SolMask for the threats it was built to address, and don't assume it covers more than this page admits. -------------------------------------------------------------------------------- title: "Why is there a privacy delay?" description: "Timing is the easiest way to deanonymise a shielded transaction. Here's what the privacy delay buys you, and how to think about the trade-off between convenience and anonymity-set size." last_updated: "2026-05-26" source: "https://solmask.xyz/en/learn/why-privacy-delay" -------------------------------------------------------------------------------- # Why is there a privacy delay? The math behind SolMask makes a withdraw cryptographically unlinkable to its deposit. But math is not the only signal an observer has. The simplest, cheapest, and most powerful deanonymisation tool is a clock. If you deposit 5 SOL at 14:02:11 and someone withdraws 5 SOL at 14:02:23, an outside analyst doesn't need to break any cryptography. They just need to notice that you were the only depositor in those twelve seconds, and the only withdraw is yours by process of elimination. The zero-knowledge proof did its job perfectly; you just stood on stage alone. The privacy delay is what we use to make sure you're not standing alone. ## What the delay actually does When you deposit, you pick an **unlock slot** — a Solana slot number in the future before which your funds cannot be withdrawn. The withdraw circuit enforces this directly. The relevant line in `circuits/withdraw.circom` reads, in spirit, `unlock_slot ≤ current_slot`. If you (or anyone with your note) tries to generate a proof before the unlock slot, the constraint fails and there is no valid proof to submit. This is enforced inside the zero-knowledge proof itself, not by a separate timer the protocol could turn off. The chain has no idea what your unlock slot is — it stays inside your commitment — but it knows the proof would not exist unless the timing rule was respected. The default privacy delay in SolMask is **one hour**, which on mainnet is roughly 9,000 slots. The UI offers shorter and longer options (10 minutes, 6 hours, 1 day, 3 days, 1 week). Longer is always more private. The recommended default is one hour because that's where, in our judgement, the crowd-size returns start flattening for most assets and amounts. ## What "more time" buys you The anonymity-set size — the number of plausible deposits your withdraw could correspond to — grows with the time window you wait through. If five other people deposited SOL in the hour after your deposit, your withdraw is one-of-six. If fifty did, it's one-of-fifty-one. The relationship is not perfectly linear. Anonymity-set growth depends on the pool's traffic, which varies by asset, by hour, and by week. Quiet pools grow slowly. The USDC pool will reliably accrue more deposits in an hour than an obscure SPL token's pool, so the same delay buys you more privacy in USDC than in a long-tail asset. The SolMask UI shows you the live anonymity-set estimate before you commit to a delay. If the estimate looks weak — say, fewer than ten plausible deposits in your selected window — the right answer is to pick a longer delay, or to wait and deposit later when traffic is healthier. ## The trade-off is real Privacy delay is friction. You're locking up funds you can't access. For payroll or treasury operations that's usually fine — those operations were always scheduled in advance. For an urgent personal transfer, a one-week delay is unhelpful no matter how excellent the anonymity-set math. The right framing is: pick the longest delay you can tolerate. If you can afford to wait a day, wait a day. If you genuinely need the funds in an hour, pick the one-hour delay and accept that the anonymity set will be smaller than if you'd waited longer. The protocol cannot force you to be patient; it can only enforce the minimum you committed to at deposit time. ## Why we don't enforce an even longer minimum We considered making the minimum delay 24 hours by policy. We decided against it because privacy-as-friction tends to push users into worse alternatives — bridges, OTC desks, or simply giving up. A one-hour minimum is short enough to handle most ordinary use cases and long enough that the chain has churned through millions of transactions, dozens of blocks, and (in healthy pools) tens of unrelated deposits since yours. If you want stronger privacy than the one-hour minimum, the dial is in your hands. The UI surfaces the actual anonymity-set numbers so you can pick a delay informed by reality, not by superstition. ## What the delay does not solve A delay alone is not sufficient privacy. If your withdraw address is already linked to your identity through some other on-chain activity, or if your RPC provider logs your IP, or if you broadcast on social media that you used the protocol, no delay can save you. The delay protects against the specific attack of timing-correlation by a passive on-chain observer. The other articles in this section cover the other attack surfaces. -------------------------------------------------------------------------------- title: "Announcing SolMask" description: "A zero-knowledge shielded pool on Solana with cross-asset withdrawals via Jupiter." last_updated: "2026-05-25" source: "https://solmask.xyz/en/blog/announcing-solmask" -------------------------------------------------------------------------------- **Privacy belongs on Solana.** Today we are launching SolMask — a zero-knowledge shielded pool with cross-asset withdrawals via Jupiter. Deposit any supported token, wait, send to a fresh address in the same or a different asset. The chain sees a deposit from wallet A and an unrelated withdraw to wallet B. That is the entire trail. SolMask v1 ships with per-asset Merkle trees for SOL, USDC, USDT, sub-five-second browser-side proof generation, and a relayer that pays gas. v2 will open the relayer set and run a public multi-party ceremony. High-risk wallets flagged by industry address-risk feeds are blocked at the deposit instruction. SolMask is a tool for honest users who do not want their payroll, treasury operations, or personal transactions broadcast to the world — not a way to obscure proceeds of crime. Read the protocol docs, try the interactive tutorial, and join us in making Solana private by default. -------------------------------------------------------------------------------- title: "Anonymity sets on Solana: what actually makes a shielded pool private" description: "How the size and composition of a shielded pool's anonymity set determines how private your transfer actually is, and how SolMask's delay and deposit cadence shape it." last_updated: "2026-05-26" source: "https://solmask.xyz/en/blog/anonymity-sets-on-solana" -------------------------------------------------------------------------------- **Privacy on a public chain is not a property of one transaction — it is a property of a crowd.** When you deposit into SolMask's shielded pool and later withdraw, what protects you is not the zero-knowledge proof on its own. The proof says, mathematically, "I own one of the unspent notes in this Merkle tree." It does not say which note. The set of notes the proof could plausibly refer to — the anonymity set — is what hides you. If that set is small, the math is excellent but the privacy is thin. If the set is large and well-mixed, the same math becomes meaningful. Everything we do in protocol design after the circuits are written is aimed at growing and shaping that set. ## What exactly is the anonymity set? The anonymity set for your withdraw is every other unspent note in the same pool whose `unlock_slot` has elapsed by the time your withdraw lands. That's the operative definition — not "every deposit ever made" and not "every note still in the Merkle tree." The two filters matter. The first filter, "unspent," excludes notes whose owners have already withdrawn. SolMask enforces single-use by recording a `nullifier` PDA for each spent note; the on-chain check is binary and irreversible. Once a peer has withdrawn, their note is no longer a candidate sender for your withdraw, because the verifier would catch the double-spend. From an observer's standpoint, spent notes drop out of the suspect set. The second filter, `unlock_slot` elapsed, exists because every deposit picks a privacy delay at insertion time. The delay is encoded as a slot number written into the note's commitment; the withdraw circuit enforces that the chain's current slot is past that value. A peer who deposited five minutes ago with a one-week delay is not yet a valid sender — their note exists in the tree, but no proof that references it can verify yet. To an outside observer trying to deanonymise you, they remove that peer from your candidate set because they can see the deposit timestamp and the minimum delay any pool member chose. Put concretely: if the pool contains 4,000 unspent notes, 600 of which are still inside their delay window, your anonymity set at withdraw time is 3,400. That is the right number to reason about — not the deposit count, not the tree size, not the TVL. ## How does the math of one-in-N actually behave? A pool with 10 viable peer notes gives any observer roughly 1-in-10 deniability for who is behind a given withdraw. With 10,000, it's 1-in-10,000. The marginal value of one additional peer falls off quickly — going from 10 to 100 peers is a ten-fold improvement; going from 10,000 to 10,100 is barely a rounding error on the observer's posterior. But the function never reaches zero, and the floor matters more than the ceiling. The right intuition is not "more is better forever." It's "below a certain threshold, the math is informally broken; above it, you are paying diminishing returns to push a number that's already protective." For most realistic threats — block-explorer correlation, casual chain analysis, a counterparty trying to map your treasury — a few hundred peers in the right amount band is already strong. For nation-state-grade adversaries with auxiliary data, no public anonymity set is enough on its own; you need operational hygiene around timing and recipients as well, which we'll come to. The number to watch is not pool TVL or total deposit count. It is the count of unspent peers within your amount band whose delays have elapsed. SolMask's UI exposes this number on the withdraw screen because it is the single most useful piece of information you can have before signing a proof. ## Why does the privacy delay matter so much? The privacy delay is the most underrated control on a shielded pool. It directly determines how many other deposits arrive between yours and your withdraw, which is exactly how the set grows. SolMask's default delay is 10 minutes. You can choose longer — up to one week, in increments — and the longer you wait, the more peer deposits get queued behind yours. A pool that accepts five deposits per hour gives a 10-minute window almost no growth headroom; the same pool gives a one-day window 120 fresh peers. For depositors who can tolerate latency, longer is unambiguously stronger. The default exists because most depositors will not choose a setting that costs them latency, even when the privacy gain is meaningful. Ten minutes is the floor we picked as a reasonable balance: long enough that an observer watching the mempool cannot trivially correlate your deposit and withdraw inside the same minute, short enough that the UX is not painful. If you care about privacy more than latency — and for treasury operations, payroll, or anything not personally urgent, you should — pick a longer delay. The `unlock_slot` field on your note is set at deposit time and is enforced by the circuit at withdraw, so the delay is non-negotiable once you've committed. One subtle point: the delay does not retroactively grow your set. A 10-minute delay you chose at deposit time is binding regardless of how many peers arrive after the unlock slot. If you want to benefit from a peer who deposits two hours after you, your own withdraw must happen after both their deposit and after your own unlock — but your unlock has already elapsed by hour two, so what you actually need is patience between the unlock moment and the withdraw moment. Many users unlock and immediately withdraw. That throws away the largest source of free anonymity growth: the time after you are eligible to withdraw but choose not to yet. ## How does the amount shape your set? If your withdraw is an outlier amount, the amount itself partially deanonymises you. A pool where every note is 0.05 SOL and you withdraw 1 SOL is, from a chain-analysis standpoint, telling an observer almost everything they need to know: only one deposit could have funded a 1 SOL note, and that deposit is yours. The proof is still valid, the nullifier still hides which specific note was spent, but the conditional distribution over candidate notes collapses to one. The defensive posture is to deposit in amounts that match what others deposit. SolMask does not enforce fixed denominations — the pool accepts arbitrary amounts because rigid denominations create UX friction and split liquidity across many under-populated pools — but you should think of your effective anonymity set as "peers in roughly the same amount band as you." If the peer median is 0.5 SOL and you deposit 47 SOL, your set is functionally peers in the 10+ SOL band, which is much smaller than the total peer count. The same applies to withdraws. If you deposit 100 USDC and withdraw 47 USDC, the partial amount makes you slightly more distinctive than if you withdrew the full 100 or a round denomination. Round amounts compose well with peer behaviour; oddly precise amounts do not. If you can split a large withdraw into rounded sub-withdraws across several days, you gain on both axes. ## What does timing leak that the proof does not hide? The proof hides which note was spent. It does not hide that a deposit happened at slot X and a withdraw happened at slot Y. If those two events are close in time and no peer activity occurred between them, an observer with no special privilege — just block explorer access — can rule out almost every other candidate sender by elimination. The proof's denominator is anonymity-set-sized; the timing correlation is what shrinks the numerator. Consider a worked example. Three depositors — A, B, C — each deposit 5 SOL into the pool within the same minute. Ten minutes later, three withdraws of 5 SOL each leave the pool to three fresh addresses within the same minute. From the chain's view, anybody can see three deposits in, three withdraws out, all the same amount, all clustered. The anonymity set within this micro-window is the three peers themselves: A is one of {A, B, C} for sure, but the observer's prior collapses to a uniform distribution over those three, not over the thousands of peers in the broader pool. The three depositors did almost nothing for one another because they acted simultaneously. The proof technology was fully engaged and the privacy outcome was a 1-in-3 set. The fix is decorrelation in time. Deposit, then forget. Come back in hours or days, ideally during a period of pool activity, and withdraw. The longer your deposit has been sitting in the pool with other deposits flowing in around it, the larger the peer set you blend into and the harder the timing correlation gets for the observer. A second timing leak worth naming: same-epoch behaviour by the same off-chain identity. If you deposit from a wallet linked to your Twitter, and you withdraw to a wallet that immediately purchases an NFT also linked to that Twitter within the same hour, the on-chain proof is fine and the off-chain inference is fatal. Privacy is a pipeline; the chain is one stage. Keep the off-chain hygiene in mind, or none of this work matters. ## What does SolMask not fix? The threat model is honest about its limits, and so should this post be. Three things SolMask cannot do for you: Network-layer observation. If you submit your withdraw proof from an IP address that an adversary can correlate with your deposit's IP address, you are linked at the transport layer regardless of the chain. The relayer mitigates this — your withdraw is broadcast from the relayer's network identity, not yours — but the relayer is one entity, and a sufficiently resourced adversary could correlate even relayer-submitted traffic if they have a vantage point. Use a different network for deposit and withdraw if your threat model includes network observers. Recipient-side address reuse. If the address you withdraw to has any prior on-chain history that ties it to you — a previous SOL transfer from a known wallet, an NFT mint, an exchange withdrawal — the destination identifies you regardless of how clean the SolMask leg was. The mitigation is to use a fresh address for each withdraw. We have a page on this, [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address); it's the single most important hygiene step after the deposit itself. Adversaries with auxiliary data. If a counterparty knows you sent exactly 47.231 SOL into the pool at a specific time because you transferred it from an exchange they can subpoena, no shielded pool fixes that. The anonymity set is large but the conditional set, given the auxiliary observation, may be one. Privacy is conditional on what your adversary already knows. The protocol gives you the structural primitives — the proof, the nullifier, the Merkle tree, the delay. The operational discipline around them is yours. We document the circuit details at [/docs/circuits](/docs/circuits) and the full threat model at [/docs/threat-model](/docs/threat-model); the [/glossary/nullifier](/glossary/nullifier) and [/glossary/shielded-pool](/glossary/shielded-pool) entries are the short versions if you just want the vocabulary. ## FAQ **Q. What is the anonymity set of SolMask's SOL pool right now?** **A.** The withdraw UI shows it live — the count of unspent peer notes whose unlock slots have elapsed. The number changes with every deposit, withdraw, and unlock. We do not publish a TVL-equivalent privacy metric because TVL conflates spent and unspent notes; the live UI number is the one that matters. **Q. Does a larger pool guarantee better privacy for my withdraw?** **A.** Larger is better all else equal, but composition matters more at the margin. A 10,000-note pool where 9,900 notes are in a different amount band than yours gives you a 100-note effective set. Match peer amounts and choose a non-zero delay; raw pool size is the floor, not the ceiling, of your privacy. **Q. If I deposit and withdraw with a 10-minute delay, is that enough?** **A.** It's the default, not the recommendation. Ten minutes is fine for casual privacy. For meaningful privacy against a motivated observer, choose hours or days, and let your `unlock_slot` elapse well before you actually withdraw. The marginal cost of patience is zero; the marginal gain in set size is everything. **Q. Do fixed denominations protect me better than arbitrary amounts?** **A.** Fixed denominations give a perfectly uniform amount distribution, which is the strongest case. SolMask chose arbitrary amounts for UX and liquidity reasons, and the practical loss is small as long as you stick to round denominations close to peer behaviour. Watch the pool's withdraw history and don't be an obvious outlier. **Q. Can the relayer see which note I'm spending?** **A.** No. The relayer sees the proof, the nullifier, and the public inputs (Merkle root, withdraw amount, recipient). The proof reveals nothing about which leaf of the tree it commits to. The nullifier is a one-way function of your `nullifier_secret`, which the relayer never learns. The relayer's view is roughly what any block explorer sees post-broadcast. **Q. If I deposit into the SOL pool and the SOL pool's USDC twin separately, do I get a combined anonymity set?** **A.** No. Each mint has its own pool, its own Merkle tree, its own anonymity set. A SOL deposit cannot be referenced by a USDC withdraw's proof. If you want privacy in both assets, you build the set in both pools independently. **Q. What's the minimum delay I can choose?** **A.** The current default and floor is 10 minutes. We considered shorter; the privacy gain from lowering it further was negligible and the correlation risk was high. If your use case truly cannot tolerate ten minutes, the right answer is probably not a shielded pool — it's a different tool entirely. -------------------------------------------------------------------------------- title: "SolMask fees explained: free deposits, fee on withdrawal" description: "Deposits are free. How the withdrawal fee — a percentage in the withdrawn asset plus a flat 0.003 SOL — works, what it pays for, and where it goes." last_updated: "2026-05-31" source: "https://solmask.xyz/en/blog/fee-model-explained" -------------------------------------------------------------------------------- **Depositing into SolMask is free, and the protocol fee is charged when you withdraw — in two parts, both enforced on-chain by the program.** Deposit 10 SOL and your shielded note is worth the full 10 SOL; deposit 10,000 USDC and your note is worth 10,000 USDC. Nothing is skimmed on the way in. The amount you can later spend privately is exactly what you put in. **First part: a percentage fee on withdrawal, taken in the asset you withdraw.** It lives in the on-chain `Config` (`withdraw_fee_bps`), is admin-tunable without a redeploy, and is capped at 100 basis points (1.00%). At the recommended 23 bps (0.23%), withdrawing 10 SOL costs ~0.023 SOL, routed to a per-pool token fee vault owned by the protocol. Crucially, this fee is **bound inside the withdraw zero-knowledge proof** — the circuit proves your spent notes equal the released output plus the fee plus change, and the program recomputes the expected fee on-chain and rejects any proof that doesn't match. The relayer cannot tamper with it. **Second part: a flat withdraw fee that defaults to 0.003 SOL, paid to the fee collector on every withdraw.** The amount lives in the on-chain `Config` (`withdraw_fee_lamports`) and is admin-tunable without a redeploy. It is fixed in SOL regardless of which asset you are withdrawing or how much. On a 1 SOL transfer it is 0.3% of the move; on a 100 SOL transfer it is 0.003%. This is the right shape for privacy-as-utility: large transfers — where privacy matters most — pay the smallest percentage. The relayer pays that SOL fee to the fee collector up front and recoups exactly it by keeping a small tip slice in the withdrawn asset. The Solana network gas — compute units plus account rent for the new nullifier PDA — is a cost the relayer absorbs; it is not reimbursed out of this fee. **Why charge on the way out?** Deposits cost you nothing, so funding the pool is frictionless and you only ever pay when value actually leaves the shielded set. It also keeps the fee proportional to what you withdraw rather than what you parked. **The fees were chosen for simplicity.** No minimum, no maximum beyond the on-chain caps, no slab structure, no surge pricing, no different rate per asset, no time-of-day premium. Whether you withdraw 0.05 SOL or 5,000 SOL, the percentage is the same. The model is meant to be predictable in your head; you should be able to estimate the cost without a calculator. **For withdraws into non-SOL assets — USDC, USDT, anything Jupiter-routable — you never need to hold SOL to pay the fee.** The relayer pays the flat SOL fee from its own wallet and recoups it by keeping a small tip slice of the withdrawn asset (a fraction of a USDC, say) before forwarding the rest. The recipient still ends up with the asset they expected, net of the percentage fee and that tip. That's why you can withdraw USDC to a brand-new wallet that holds zero SOL. **Fees accumulate in per-pool fee vaults and are swept to the configured fee destination permissionlessly.** Any signer can call the sweep instruction once the per-pool balance exceeds its configured threshold; if an automated job stops tomorrow, the funds are not stuck. The fee destination is published on-chain and reachable from any block explorer. Every cent of SolMask's revenue is on-chain, traceable to a single fee-destination address, and was deducted by code the verifier can replay. ## FAQ **Does it really cost nothing to deposit?** Yes — the full amount you deposit enters the pool and backs your note. The only Solana network gas on a deposit is the normal priority fee that goes to validators, not to SolMask. **Does the relayer pay gas for me?** Yes — the relayer is the transaction's fee payer on a withdraw, so it covers the Solana network gas and account rent out of its own SOL, and it fronts the flat protocol fee (0.003 SOL by default). It recoups only the protocol fees, via a tip slice in the withdrawn asset; the network gas it absorbs. That's why your recipient never needs SOL. **What does the withdrawal fee actually go toward?** Protocol revenue. Audit costs, the ceremony, ongoing development, infrastructure (indexer, relayer, RPC). It is not a network gas fee. **Is the percentage fee taken in the asset I withdraw, or in SOL?** In the asset you withdraw. A SOL withdraw pays the percentage in SOL; a USDC withdraw pays it in USDC. The flat fee is always in SOL. **What if I self-relay?** You sign and submit the withdraw yourself, so you pay the network gas and the protocol fees (the current `withdraw_fee_bps` and `withdraw_fee_lamports`) directly. There is no asset-denominated tip in that case — you are the relayer — but you take on the gas the hosted relayer would otherwise have absorbed. **Where can I verify all of this?** Deposits are free, the withdraw percentage is `Config.withdraw_fee_bps`, and the flat withdraw fee is `Config.withdraw_fee_lamports` — both defined and enforced on-chain in the program's deposit and withdraw instructions. The full breakdown is at [/docs/fees](/docs/fees). -------------------------------------------------------------------------------- title: "How zero-knowledge proofs actually work inside SolMask" description: "A non-mathematical walkthrough of what SolMask's Groth16 withdraw proof asserts, what the verifier sees, and where the privacy actually comes from." last_updated: "2026-05-26" source: "https://solmask.xyz/en/blog/how-zero-knowledge-proofs-work-on-solmask" -------------------------------------------------------------------------------- ## What does a zero-knowledge proof actually prove? A zero-knowledge proof is a way to convince someone that you know a particular piece of information without showing them the information itself. That is the entire idea. There is no magic — just arithmetic that, when arranged carefully, produces a short string of bytes that a verifier can check in fixed time, and that string can only have been produced by someone who actually possessed the secret. If the prover did not know the secret, they could not have constructed the bytes. If they did, the bytes are produced and look like every other valid proof. The verifier learns one bit: "you know it, or you don't." Nothing else leaks. Inside SolMask, the secret is the contents of a shielded note. A note is the on-chain object that represents your deposit inside the pool. It does not appear in plaintext anywhere on Solana — only its commitment does, which is a Poseidon hash of the note's fields. The fields include a `secret`, a `nullifier_secret`, the deposited `amount`, the chosen `unlock_slot`, and a mint identifier. The commitment is a 32-byte leaf in a Merkle tree that lives in a program-owned account. Every deposit appends one leaf; the tree grows; the root changes; the new root is what later proofs reference. When you withdraw, you do not point to your leaf. Pointing to it would be the whole problem — it would link the deposit to the withdraw and undo the privacy. Instead, you build a proof that asserts, in one mathematical statement, four things at once. First, that you know the preimage of *some* commitment in *some* recent Merkle tree state — that is, you can reconstruct the path from a leaf up to a root the program has recorded. Second, that the current Solana slot is greater than or equal to the `unlock_slot` baked into that note. Third, that the amounts you are routing out — to a recipient, to a change-note, and to the protocol fee — sum correctly to what was originally deposited in that note. Fourth, that the `nullifier_hash` you are publishing was derived from the same `nullifier_secret` sitting inside the note, and that the chain has not seen this nullifier before. Those four facts, in one proof. The Merkle path is the part most people find counterintuitive. You give the circuit your leaf, the sibling hashes at each level, and a claim about which root the path resolves to. The circuit recomputes the Poseidon hash up the tree using your siblings, ends up with a 32-byte value, and asserts that value equals the public root that the on-chain program will look up. The circuit does not know which leaf — it just knows the leaf hashes upward to the root. From outside, every withdraw of a given mint and amount looks identical. There is no observable difference between someone withdrawing the note from block 100 and someone withdrawing the note from block 250,000, as long as both roots are valid. ## What does the verifier on Solana actually see? The verifier is a Solana program — the SolMask withdraw instruction. It runs inside a normal transaction. It does not run the entire Groth16 prover; that would be impossibly expensive on-chain. It runs the *verification* algorithm, which for Groth16 is a small fixed-cost computation involving a handful of elliptic-curve pairings. On Solana, those pairings are exposed as syscalls (`alt_bn128`) and consume a known, bounded amount of compute units regardless of how complicated the underlying statement was. That is the whole point of Groth16: the proof and the verifier cost are tiny, while the statement being proved can be as gnarly as the circuit designer wants. What the verifier reads from the transaction is a short list of public inputs and the proof itself. The public inputs are: the Merkle root the proof claims membership in, the `nullifier_hash`, the recipient address, the fee recipient (the fee destination), the current slot bound, the mint, the `amount_to_recipient`, and the `change_amount` that goes back into the pool as a fresh note. The proof is 256 bytes — three elliptic-curve points encoded compactly. The verifier checks the pairing equation. If it holds, it then performs a few additional non-ZK checks: the nullifier has not been used before (it inserts the nullifier hash into the on-chain nullifier set, which is a hash-map-backed PDA), the root is one the program has recorded within its rolling history window, and the slot bound is consistent with the actual current slot. If anything fails, the instruction aborts. Nothing else is logged. Nothing else is revealed. Notice what is *not* in that public-input list. The secret. The nullifier secret. The original deposit's leaf index. The original deposit's slot. The depositor's wallet. The amount deposited (versus the amount being withdrawn, which is public). The change note's commitment is public, but it is a fresh Poseidon hash with new randomness, so it links to nothing observable. The link between the deposit transaction and the withdraw transaction is the thing that has been cut. That cut is the privacy. ## Why Groth16 and not something newer? Groth16 is the oldest of the modern proving systems, dating from 2016, and it has been the workhorse for production privacy systems ever since. SolMask uses it deliberately. Two reasons. First, proof size: 256 bytes. That fits comfortably inside a Solana transaction's data budget, leaving room for the public inputs, the recipient transfer, the change-note insertion, the nullifier insertion, and the optional Jupiter swap that converts the withdrawn asset into the recipient's chosen asset. A larger proof would crowd the transaction; a 2 KB proof from a more modern system would force splitting the withdraw across multiple transactions, and split flows are an information leak in themselves. Second, verifier cost: the on-chain pairing check is roughly 100k-200k compute units on Solana with the `alt_bn128` syscalls. Plonk is comparable but with a different cost curve; STARKs are larger and slower to verify on-chain; Halo2 has improving tooling but the verifier is heavier. The trade-off Groth16 demands is the trusted setup. We accepted that trade because it puts the cost on us once and the savings on every user forever. A Groth16 setup is circuit-specific. Each time the withdraw circuit changes, the setup has to be redone. That is annoying in practice but acceptable in a system where the circuit changes slowly — the SolMask withdraw circuit is intended to be stable for years, with multi-recipient and shielded-transfer extensions added as separate circuits with their own setups rather than as edits to the core one. See [/docs/circuits](/docs/circuits) for the full circuit catalog and which setup covers which. ## What does the trusted setup actually mean? The trusted setup is the one piece of SolMask that is not pure math you can re-derive from first principles. To build a Groth16 prover and verifier for a given circuit, you need a "proving key" and a "verifying key" — large structured blobs of elliptic-curve points generated from a random secret called the toxic waste. If a single party generates the keys alone and keeps the toxic waste, that party can forge proofs forever. If the toxic waste is destroyed, no one can forge anything; the keys become trustworthy artifacts. The trick is convincing everyone that the toxic waste was actually destroyed. The standard answer is a multi-party computation. Many participants each contribute randomness, in sequence, to the setup. Each participant performs a transformation on the running state of the keys, mixes in their own secret, and then destroys their secret. The cryptographic property is: the final keys are safe as long as *at least one* participant was honest. You only need one. If five hundred people contribute and four hundred and ninety-nine of them are compromised, the setup is still secure, because the one honest participant's randomness scrambled the toxic waste irrecoverably. SolMask's withdraw circuit went through a phase-2 ceremony with a public participant list, transcript hashes published per round, and a Bitcoin block beacon at the end. The beacon is a final non-malleable randomness source: we declared in advance that the hash of a future Bitcoin block at a specified height would be mixed in as the last contribution. That block has now been mined; its hash is in our transcript; no one could have manipulated the result without controlling Bitcoin mining, which is to say no one could have manipulated it. The full transcript, every participant's attestation, and the final keys are at [/docs/trusted-setup](/docs/trusted-setup). That is the one thing you trust. After that, every withdraw proof is verifiable math. ## What is the proof actually doing, with a concrete example? Say you deposit 1 SOL. Deposits are free, so the deposit instruction credits the full amount and writes a note with `amount = 1 SOL` (the protocol fee is charged later, at withdraw time). You chose a 10-minute delay; the note's `unlock_slot` is set to the current slot plus 1,500 slots (10 minutes at ~400 ms/slot). The note's commitment — Poseidon of `(secret, nullifier_secret, 1 SOL, unlock_slot, mint)` — is appended to the Merkle tree. The note's secrets are derived from your wallet (no file to save), and the deposit publishes a wallet-encrypted recovery blob on-chain so any device with the same wallet can rediscover it; the indexer publishes the new root. Ten minutes pass. Meanwhile, three other people deposit. The tree now has four new leaves added since yours. Roots have been advancing. Now you want to withdraw to a fresh wallet that has never touched the pool. In the browser, snarkjs and the circuit WASM run for somewhere between 5 and 30 seconds depending on hardware. They take your note, the current Merkle path from your leaf to a recent root, the recipient address, your chosen `amount_to_recipient` (let's say all of it: 0.9947 SOL after the 0.003 SOL withdraw fee, with `change_amount = 0`), and produce 256 bytes of proof plus the derived `nullifier_hash`. You send those to the relayer. The relayer builds a transaction: invoke the withdraw instruction with the proof, public inputs, and accounts. It submits the transaction as the fee payer, so it covers the SOL the network charges to land it — that network gas is a cost the relayer absorbs. It also fronts the on-chain 0.003 SOL withdraw fee to the fee destination and recoups exactly that fee via a tip slice in the deposit asset; the fee itself is protocol revenue. The transaction lands. The chain sees: a 256-byte proof, a never-before-seen nullifier hash entering the nullifier set, 0.9947 SOL flowing out of the program vault to your fresh wallet, 0.003 SOL flowing to the fee destination. The chain does not see which of the recent deposits your withdraw corresponds to. Anyone watching has four candidates plus every prior deposit still in the active root window. That is the anonymity set. That is what the proof bought you. ## What does ZK *not* do? A zero-knowledge proof cuts one specific edge in the on-chain transaction graph. It does not make you invisible in any larger sense. It does not hide your IP from the relayer or the RPC you sent the transaction to — the relayer sees your origin IP unless you used Tor, a VPN, or your own infrastructure. It does not hide the fact *that* a withdraw happened — every withdraw is publicly visible, just unlinked from a specific deposit. It does not hide off-chain correlations: if you tell three people you withdrew exactly 0.9947 SOL at 3:14pm, and only one withdraw of that amount happened at 3:14pm, you have leaked the link yourself. It also does not protect you against analysis at the edges — the moment you spent the withdrawn SOL on something identifying, the privacy of *what you do next* depends on your behavior, not on the proof. Choose a fresh recipient that has no history. Do not immediately bridge to a known KYC address. Do not deposit and withdraw round-number amounts that match exactly. The proof gives you a clean cut; what you do with the clean side is up to you. See [/learn/verifying-your-deposit](/learn/verifying-your-deposit) for the user-facing checklist, and [/glossary/nullifier](/glossary/nullifier), [/glossary/commitment](/glossary/commitment), and [/glossary/groth16-proof](/glossary/groth16-proof) for the term definitions. ## FAQ **How long does proving take in the browser?** Between 5 and 30 seconds on a recent laptop, dominated by the WASM-based Groth16 prover from snarkjs. Older machines and mobile take longer; we cap the UI at 60 seconds and surface a progress estimate. The proving happens entirely client-side — no witness leaves your device. **How big is the proof on the wire?** 256 bytes for the proof itself, plus the public inputs (root, nullifier hash, recipient, fee recipient, slot, mint, amounts) which add up to a few hundred bytes more. The whole withdraw transaction comfortably fits inside a single Solana transaction. **Can the verifier program be tricked into accepting a bad proof?** Not if the pairing equation holds, which it can't unless the prover knew the secret or the elliptic-curve discrete log problem is broken. The verifier code itself is short and audited; it is essentially a wrapper around the `alt_bn128` pairing syscall. **What happens if someone tries to withdraw twice with the same note?** The first withdraw inserts `nullifier_hash` into the on-chain nullifier set. The second withdraw, even with a freshly generated proof, derives the same `nullifier_hash` from the same `nullifier_secret` — the insertion fails, the instruction aborts. The note is single-use by construction. **Does the trusted setup need to be redone if SolMask adds features?** Only for circuits that change. The withdraw circuit's setup is independent of any other circuit; adding a multi-recipient or shielded-transfer circuit means a new, separate ceremony for that circuit. Existing notes and existing withdraw proofs are unaffected. **Is Groth16 quantum-safe?** No — it relies on elliptic-curve cryptography, which a sufficiently large quantum computer would break. No production privacy system on Solana is quantum-safe today. Post-quantum proving systems exist (some STARK variants) but are not yet practical for on-chain verification on Solana. We track this and will migrate when the trade-offs make sense. **Is the proof itself ever stored on-chain after verification?** No. The proof is consumed by the verifier inside the withdraw instruction and discarded. What persists is the inserted nullifier hash, the change-note commitment, the recipient transfer, and the fee transfer. The proof bytes are visible in the transaction's instruction data on chain history, but the program does not retain them after the slot is processed. -------------------------------------------------------------------------------- title: "Private payroll on Solana: paying contractors without leaking the graph" description: "How to run a recurring private payroll on Solana that pays contractors in USDC without exposing your full payment graph on a public block explorer." last_updated: "2026-05-26" source: "https://solmask.xyz/en/blog/private-payroll-on-solana" -------------------------------------------------------------------------------- **A treasury wallet paying twenty contractors every month leaks the entire payroll graph forever.** Anyone with a block explorer can enumerate every recipient, every amount, every cadence. A contractor curious about peers can map the whole roster in a minute. A competitor pulling the public history can infer headcount, comp bands, and which contractor got a raise three months ago. The chain is a permanent, indexed record of relationships you almost certainly did not intend to publish. For most companies that hire on a public blockchain, that is the worst part of the deal — not the volatility, not the gas, but the unilateral disclosure of every payment relationship to anyone who asks. This is the case SolMask was designed to solve. The protocol gives you a structural way to pay people from a treasury without creating a public on-chain edge between the treasury and the recipient. The mechanics are not exotic. The discipline is mostly operational. ## What does the on-chain payroll problem actually look like? Picture a treasury wallet `Treasury7…xK` holding 100,000 USDC of operating budget. On the first of every month, it sends 2,500 USDC to each of twenty contractor wallets. The Solana ledger now contains, for each of those twenty payments: sender, recipient, amount, timestamp, and a transaction signature that anyone can quote. The wallet's transaction history is a permanent enumeration of "everyone we pay, how much, when." Three concrete leaks follow. First, every contractor can see every other contractor's address and amount the moment they look at the sender's history — peer comp is exposed by default. Second, anyone with one contractor's address (a freelancer profile, a public PR, a leaked invoice) can pivot to the treasury and from there to every other contractor. Third, the cadence itself reveals operational rhythm: a payment on the first of every month is a payroll signature; a one-off bonus is a one-off bonus. The graph tells the story even if the amounts are pseudonymous. You cannot fix this with multiple treasuries — multiple treasuries just spread the same graph across more nodes. You cannot fix it with new contractor addresses every month — contractors would need to pre-register a fresh address each cycle, and the on-chain edge from treasury to that month's address is still visible. The only structural fix is to put a privacy layer between the treasury and the recipient. ## How does a private payroll actually work with SolMask? The pattern is the simplest one the protocol supports. Each pay cycle, the treasury deposits the gross payroll amount into the SolMask USDC pool. The pool ingests USDC, returns a note that commits to the deposited amount, and from the chain's view the treasury has interacted with a public protocol — nothing else is visible. Later, after the deposit's `unlock_slot` has elapsed, the treasury sends a withdraw transaction for each contractor, naming that contractor's recipient address as the destination. The withdraw transaction, from the chain's view, originates from the SolMask withdraw relayer and lands at the recipient. There is no on-chain edge from `Treasury7…xK` to any contractor. The graph is broken at the pool. Step by step, with the actual operational shape: The treasury wallet should be the wallet you already use for treasury — no new infrastructure is needed. On payday-minus-one, the treasury calls deposit with the full payroll amount. For a $50,000 payroll, that is a single 50,000 USDC deposit. Deposits are free, so the note that lands in the shielded pool commits to the full 50,000 USDC of spendable value (the protocol fee is charged later, when you pay out). Ownership of the deposit is bound to the treasury wallet itself — the note's secrets are derived deterministically from that wallet's signature, and the deposit publishes a wallet-encrypted recovery blob on-chain, so any device that reconnects the same wallet can authorize a future withdraw. There is no note file to save. Set the privacy delay to at least one day, ideally longer. For payroll, latency is not a problem — you know the schedule months in advance. The longer the delay, the more peer deposits accumulate behind yours and the larger the anonymity set when you withdraw. A treasury that deposits on the 25th to withdraw on the 1st is doing this right; a treasury that deposits at 09:00 to withdraw at 09:15 is throwing away most of the privacy. When the unlock slot has elapsed, generate one withdraw proof per contractor. SolMask's current circuit takes one recipient per withdraw — multi-recipient withdraws are a planned circuit upgrade — so a twenty-contractor payroll is twenty separate withdraw transactions. Each one decrements the available balance of the deposit note, produces a new note for the remainder, and emits a payment to the named recipient. The contractor sees an inbound USDC transfer from the relayer; they do not see who paid them in any chain-accessible way. The whole flow takes about ten minutes of human time per cycle once you've done it once. The proof generation runs in the browser or on a service of your choice; the relayer handles transaction submission; the contractor sees money arrive. ## What does the operational hygiene look like? The protocol gives you privacy. Three operational practices give you reliability. **Wallet custody.** With SolMask there is no separate note passphrase to manage. A deposit's spendable secrets are derived from the treasury wallet that made the deposit, and the encrypted recovery blob lives on-chain — so your entire custody surface collapses to one thing: the treasury wallet's own keys. Whoever can sign with that wallet can re-derive every note and withdraw it; whoever cannot, can't. Treat the treasury wallet exactly as you already treat a multisig signing key — hardware-backed, geographically distributed backups of the seed, the same operational severity you apply to any treasury key. Lose access to the wallet and you lose the deposits, because only that wallet can decrypt the on-chain recovery blobs. We have written about recipient-side key hygiene at [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address) — the same discipline applies symmetrically to your treasury wallet. **Cross-device recovery.** Because notes are recovered from the wallet plus the on-chain encrypted blobs, there is nothing to copy between machines. To run payroll from a different operator's laptop, or to recover after a machine is wiped, you connect the same treasury wallet and SolMask re-scans the pool: it pulls every recovery blob, trial-decrypts the ones belonging to your wallet, and reconstructs the full set of unspent notes. No JSON files to ship around, no shared secret to leak in transit. The only thing that must be present is the treasury wallet's signing capability — which your multisig or HSM already governs. **Accounting reconciliation.** The deposit is a public on-chain transaction — your books reconcile against it normally. The withdraws are also public on-chain transactions, each visible at the relayer's transaction history, each landing at a recipient address you keep in your internal payroll system. The link between the deposit and the withdraws is what is hidden from the public, not from you. Your internal records are unchanged: you know which contractor was paid, when, how much. Your CFO can reconcile the same way they always did. The auditor can verify the cash left the treasury and arrived at the recipient; the public block explorer cannot. ## What does the round-trip actually cost? For a $50,000 monthly payroll across twenty contractors at $2,500 average per contractor, the costs break down predictably. The deposit is free — the full $50,000 enters the shielded pool. The protocol fee is charged when you pay out: a 23 bps withdraw fee taken in USDC — that's $115 across the payroll — plus a flat 0.003 SOL per withdraw (both admin-tunable on-chain values, not hardcoded constants). The relayer pays the SOL fee up front and recoups it by keeping a small USDC tip slice out of each withdrawal, so the contractor receives the expected USDC net of the fee and your treasury never has to hold SOL for gas. At a SOL price of $60, twenty withdraws cost twenty × 0.003 SOL × $60 = $3.60. Total round-trip cost: $118.60 on $50,000 — twenty-three and a half basis points all-in, none of it paid until you actually disburse. The full fee breakdown is at [/docs/fees](/docs/fees); deposits are free, the withdraw percentage fee is `Config.withdraw_fee_bps` (23 bps) and the flat withdraw fee defaults to 0.003 SOL (`Config.withdraw_fee_lamports`), both admin-tunable and enforced on-chain. Compare to the alternative. Twenty international wires from a US bank to twenty independent contractors run $15-$45 per wire on the sending side, often another $15 per wire on the receiving side, plus currency conversion spreads of 1-3% on top. The same $50,000 payroll over twenty international wires runs $1,500-$4,500 in fees alone before any FX spread. SolMask's $118.60 is roughly 3% of the cheap end of that range, and you keep the privacy. The compliance posture is different in detail; the privacy posture is incomparably better. For larger payrolls the percentage gets even better because the dominant cost is the linear 23 bps on the withdrawals, not the flat per-withdraw fee. A $500,000 payroll across the same twenty contractors costs $1,150 + $3.60 = $1,153.60 — still 23 bps all-in. The flat fees are essentially noise. ## How is this defensible from a compliance standpoint? The most common objection to private payroll is the compliance posture. The answer is structural and was designed into the protocol from day one. The deposit instruction checks every depositor wallet against the on-chain banlist before accepting funds. The banlist is maintained from industry-standard address-risk feeds, refreshed on a regular cadence, and enforced on both the deposit and withdraw paths. A wallet that appears on the banlist cannot deposit at all. A wallet that became sanctioned after a clean deposit cannot withdraw to a banned recipient, because the recipient address is also screened at withdraw time against the same banlist. The full mechanics are at [/compliance](/compliance) and the banlist's structure and data sources are documented at [/compliance/banlist](/compliance/banlist). This is the structural compliance that makes private payroll defensible. A treasury operator using SolMask for payroll can demonstrate to their counsel that the protocol they're using does the bilateral screening at the deposit and withdraw layers — exactly the same screening a traditional payment processor does at deposit and disbursement. The privacy is privacy from the public, not from the protocol's compliance rails. The deposit instruction will reject your treasury if it is sanctioned; the withdraw instruction will reject your contractor if they are. That is the same posture as a regulated payment processor, but enforced in deterministic on-chain code instead of in an off-chain risk team's discretion. This is also why SolMask is not the right tool for paying people you do not intend to pay legally. The screening is structural and bilateral. You cannot get a payment to a sanctioned address through the protocol any more than you could through a bank. The privacy SolMask offers is the privacy a legitimate business has every right to expect — not anonymity from law enforcement, not anonymity from sanctions enforcement, not a workaround for KYC. It is the privacy of "my payroll graph is my business, not the public's." Most regulated companies should already think about their on-chain operations that way, and very few do, because until recently the tools did not exist. The USDC pool details are at [/private/usdc](/private/usdc); the per-pool config (including which fee_collector the deposit and withdraw fees flow to) is the same `Config` PDA published on-chain that we describe in the fee post. ## FAQ **Q. Can I pay all twenty contractors in one transaction?** **A.** Not today. The current withdraw circuit takes one recipient per withdraw. A twenty-contractor payroll is twenty withdraw transactions, each with its own proof, each paying its own 0.003 SOL withdraw fee. Multi-recipient withdraws are on the circuit roadmap; until they ship, the operational pattern is to generate the twenty withdraws in a batch and let the relayer submit them in parallel. **Q. Does the relayer see who I'm paying?** **A.** The relayer sees the public inputs to your withdraw proof — Merkle root, nullifier, recipient address, amount. It does not see which deposit you are spending or which treasury originated the funds. From the relayer's view, each withdraw is an independent proof against the shielded pool. The relayer cannot link your twenty withdraws to each other beyond the trivial observation that they all happened in the same broadcast batch. **Q. What happens if I lose access to the treasury wallet?** **A.** Then the deposits are unrecoverable — but note what changed versus older designs: there is no separate note passphrase or note file to lose. A note's secrets are derived from the treasury wallet, and the encrypted recovery blob sits on-chain, decryptable only by that wallet. So your entire recovery story collapses to "keep the treasury wallet safe," exactly as you already do for any treasury key. SolMask holds no user data and cannot recover anything on your behalf. **Q. Can I deposit weekly instead of monthly?** **A.** Yes, and there is a privacy benefit to it. Smaller, more frequent deposits blend into a larger flow of peer deposits and produce less of a recognisable "first of the month" timing fingerprint. The cost is twenty-three bps per deposit regardless of frequency, so weekly versus monthly is no more expensive in aggregate, just more granular. **Q. Do contractors need to do anything different to receive payment?** **A.** They give you a recipient address. That is the whole interface. They do not need a SolMask account, a wallet integration, or any knowledge that the privacy protocol exists. They receive a USDC transfer from the SolMask relayer; their wallet shows it normally. We recommend they use a fresh address for each cycle — that page is at [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address) — but the protocol does not require it. **Q. How is this different from just running payroll through a payment processor?** **A.** A payment processor knows every payment, can freeze any payment, charges 2-4% on average, and takes 1-5 business days to settle internationally. SolMask knows only what the chain knows — that a deposit happened and that withdraws happened — and the public knows even less. Settlement is instant; cost is twenty-three bps. The trade-off is that you handle the operational side (treasury-wallet custody and accounting reconciliation) instead of the processor. For a treasury that already does multisig signing and key management, that is not a step up in operational burden. **Q. Can my auditor verify the books against the chain?** **A.** Yes. The deposit is a public transaction from your treasury to the SolMask pool — your auditor can verify the amount left the treasury and the protocol fee was paid. The withdraws are public transactions from the relayer to each contractor address — your auditor can verify each contractor received the expected amount. The link between deposit and withdraws is what is hidden from the public; your auditor has your internal payroll register, which is the same document they have always reconciled against bank withdrawals. The audit posture is unchanged. The public's view is what changed. -------------------------------------------------------------------------------- title: "Receiving SOL privately to a fresh wallet" description: "How to set up a fresh Solana wallet that can privately receive funds without exposing the sender link, and without pre-funding the wallet with SOL." last_updated: "2026-05-26" source: "https://solmask.xyz/en/blog/receiving-sol-privately-to-a-fresh-wallet" -------------------------------------------------------------------------------- **Most privacy-protocol writing is about sending.** Sending is the easy half — you control the timing, amount, and source wallet. Receiving is harder, because the address you publish to whoever is paying you becomes the anchor for everything that follows. If the recipient address has any prior on-chain history, the privacy you bought on the sender side is mostly wasted. If it doesn't, you have to make sure the very first transaction it sees — the SolMask withdraw — is also the first transaction the rest of the world sees. This post is about how to do that correctly: how to generate a fresh recipient wallet, why SolMask withdraws don't require it to hold any SOL, and what not to do afterwards. ## What does "fresh" actually mean for a recipient address? A fresh Solana address, for the purposes of a SolMask withdraw, is one that has never appeared as a signer or a writable account in any prior transaction. No prior deposits. No NFT mints. No airdrop claims. No DAO votes. No "test transaction" of 0.001 SOL from your main wallet. No staking history. No Jupiter swaps. Nothing. The reason this matters is structural. SolMask's withdraw breaks the on-chain link between the depositor's funding wallet and the recipient address by spending a zero-knowledge proof against a Merkle root and revealing only a nullifier — `nullifier_secret` hashed against the leaf — that proves "one valid note was burned" without revealing which. The protocol cannot tell you which deposit funded which withdraw; neither can a chain analyst observing the pool from the outside. That guarantee is what you are paying 23 bps and 0.003 SOL for. But the guarantee terminates at the moment the withdraw lands. From the next slot onward, the recipient address has a permanent, public on-chain footprint that starts with "received X from the SolMask withdraw vault." If that address already had a history — say it received 0.5 SOL from your treasury wallet last week — the chain analyst's job becomes trivial. They don't need to break the cryptography. They look at the address's pre-existing edges in the transaction graph and read your treasury wallet straight off the screen. This is the most common deanonymisation mistake in shielded-pool usage. It's users handing over the answer for free because they reused a wallet they already had. The rule: one fresh address per private receive. Generated cleanly. Never funded from a wallet linked to you. Used once, or only for purposes intended to be linked together. ## How do I generate a clean Solana receiving wallet? There are three reasonable paths, with increasing operational separation and increasing operational cost. Pick the one that matches the sensitivity of the receive. **Path one: a new subaccount inside Phantom or Solflare.** In Phantom, "Manage Accounts" → "Add / Connect Wallet" → "Create new wallet" generates a new keypair derived from the same seed phrase as your other Phantom accounts. Solflare is similar — "Add wallet" → "Create new wallet" derives a fresh BIP-44 index. Easiest option, genuinely fine for many cases. The new address has no on-chain history; from Solana's perspective it is brand new. The catch is that the wallet UI on your local device knows the two accounts share a seed, so if your device is ever compromised — malware, screen recording, a forensic image — an examiner can see the "fresh" address belongs to the same human as your daily-driver wallet. Local-device threat, not a chain-analysis one. Sufficient for keeping a contractor payment off your treasury's public balance; not sufficient against a forensic examiner. **Path two: a separate wallet app or browser profile.** Install Solflare alongside your daily Phantom, or use a Chrome profile dedicated to private receives. Generate a new seed entirely inside the second app. The two wallets share no derivation history; the only artefact tying them together is that they exist on the same machine. Meaningfully stronger, roughly five minutes of setup. **Path three: full keypair generation on a separate device or via the CLI.** Run `solana-keygen new --outfile ~/receive-only.json` on a machine you do not use for other crypto activity, or better, on a fresh user account on a clean OS. Import the keypair into Phantom or Solflare only when you need to spend, or never import it and sign onward transactions via the CLI. Maximum separation, appropriate when the threat model includes subpoena power or device-level access. Overkill for a routine private payment. A note on hardware wallets. Phantom + Ledger and Solflare + Ledger both work normally as withdraw destinations. The Ledger derives a fresh Solana address on-device under whatever derivation path you select; that address has no on-chain history until you use it. The withdraw transaction does not require the Ledger to sign anything — the relayer signs and submits — so you can configure the receiving Ledger address, hand it to the sender, and not touch the device again until you want to spend. The privacy properties of the withdraw are identical whether the recipient is a hot wallet or a hardware wallet. For walkthroughs of these flows with screenshots, see [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address). ## How can a fresh wallet receive funds with zero SOL in it? The single most useful operational property of SolMask withdraws is that they do not require the recipient address to hold any SOL whatsoever. A wallet that has never existed on-chain — zero balance, no account rent paid, no associated token accounts created — can receive 1,000 USDC privately, in one transaction, with no prior funding step. This is not magic; it is just careful instruction layout inside the withdraw, and it is worth understanding because the alternative — pre-funding the fresh wallet with a little SOL "for gas" — is the second most common way users deanonymise themselves. Here is what happens inside a single SolMask withdraw transaction when the asset is USDC and the recipient address is brand new: 1. The relayer signs the transaction. The relayer is the fee payer for the transaction's base Solana fee. 2. The program verifies the Groth16 proof against the current Merkle root and the supplied nullifier. If the proof is valid and the nullifier is unspent, execution continues. 3. The program checks the recipient's associated token account (ATA) for USDC. If it doesn't exist, the program creates it idempotently in the same transaction. The rent for the ATA — currently around 0.00203928 SOL — is paid by the relayer, not by the recipient. 4. The relayer pays the 0.003 SOL on-chain withdraw fee (the current `withdraw_fee_lamports`) from its own wallet and recoups it by keeping a small USDC tip slice out of the withdrawn amount. The recipient receives the USDC they were expecting, net of that small tip; no SOL ever has to touch the recipient's wallet. 5. The withdraw releases the USDC into the relayer-controlled account, and the recipient's ATA receives it net of the small tip slice the relayer keeps to cover the protocol fee. 6. The program records the nullifier as spent, preventing double-spend of the same note. 7. The `unlock_slot` field on the note — the timelock that prevents instant deposit-to-withdraw correlation — has already been checked against the current slot by step 2. If the unlock has not passed, the proof verification fails earlier. The recipient holds USDC. The recipient's ATA exists. The recipient's SOL balance is still zero — they neither paid the ATA rent nor paid the 0.003 SOL fee out of their own SOL. The relayer absorbs the network gas and the rent; it is made whole only on the protocol fee, which it recoups from the USDC tip slice. Everyone is whole. The deposit asset shape changes the details but not the principle. For SOL deposits, the relayer's tip slice is taken in SOL directly. For USDC and USDT, the tip is taken in that same asset. If the recipient wants a *different* asset than was deposited, a Jupiter swap inside the same withdraw converts the delivered amount — but the fee itself is always recouped from a small slice of the deposit asset. The recipient never needs to fund anything. This is the property that makes private receives operationally clean. You can ask someone to pay you in USDC, give them a fresh Phantom subaccount address, and the moment the withdraw lands, you can use the funds. No pre-funding, no on-chain breadcrumb from your main wallet to the fresh one, no waiting for SOL to arrive from somewhere first. ## Why is funding a fresh wallet for gas the worst thing you can do? Imagine you've done the work. You generated a clean Phantom subaccount. You handed the address to a contractor over Signal. They deposit 1,000 USDC into the SolMask USDC pool (deposits are free), wait for the unlock slot, and request a withdraw to your fresh address — the 23 bps fee is taken on the way out, so you receive 1,000 USDC less the fee. The withdraw is queued. Then you have a thought: "I should probably send 0.01 SOL from my main wallet to the fresh one, just so it has gas." You do this. Twelve seconds later, the SolMask withdraw lands. Now your fresh wallet has 1,000 USDC plus 0.01 SOL. You have destroyed the privacy of the entire flow. The on-chain footprint of the fresh address now reads, in order: (1) 0.01 SOL inbound from `your-main-wallet.sol`, (2) 1,000 USDC inbound from the SolMask withdraw vault. The graph edge between your main wallet and the fresh wallet is permanent and public. Any observer with prior context on your main wallet — and that is anyone who has ever seen you tweet your address, anyone you have ever paid from your main wallet, the recipient of any donation you've made, the chain-analysis dashboards that already cluster your main wallet under your real identity — can now trivially identify the fresh wallet as yours. The SolMask withdraw broke the link from the contractor's deposit to your fresh wallet; you re-established the link to yourself in a different direction by funding the gas. The fix is the one we already covered: don't fund the fresh wallet at all. The withdraw creates the ATA, pays the rent, covers the on-chain fee, and delivers the asset, all without the recipient providing any SOL. For a pure receive flow — money in, sit on it — you are done. There is nothing to fund. If you need ongoing SOL on the fresh wallet because you intend to swap the received USDC into something else, do not transfer SOL in from your main wallet. The cleaner options are: - Do a second, smaller SolMask deposit + withdraw of SOL, into the same fresh wallet, from a depositor source that is not linked to you. (If you do it from your own funds via a different deposit, the depositor side is private from the public chain, but you should still pause on whether the operational separation gets you what you want.) - Receive a small SOL top-up from the same sender via SolMask, in a separate deposit, using the same fresh address. - Use a sibling fresh wallet for the swap, funded the same way. The common pattern across these is: never let the on-chain graph see an edge between a wallet you control under your real identity and the fresh receiver. The whole point of the withdraw is to give you a wallet with exactly one inbound edge, originating from the protocol vault. Don't add a second edge. For the full framing of which adversaries the protocol is designed to defeat and which it isn't, see [/docs/threat-model](/docs/threat-model). ## A worked example: a $1,000 contractor payment Concrete walkthrough. A contractor — call her Maya — is owed $1,000 for a piece of work. The payer is a small company that would prefer Maya's wallet not be visible on their treasury's public on-chain trail; Maya, separately, would prefer her wallet not be linked to the payer either. SolMask is the right tool for the job. Here is the full flow. Maya opens Phantom. "Manage Accounts" → "Add / Connect Wallet" → "Create new wallet." Phantom derives a fresh keypair and shows her a new public address. The address has never appeared on-chain. Balance is zero. No USDC ATA exists yet. Maya sends the address to the payer over Signal. Not email, not Slack, not a Notion doc — the only artefacts linking the recipient address to her identity should be in places she can audit and delete. Signal isn't perfect; it's a smaller surface than the payer's corporate email retention policy. The payer receives the address. From their treasury wallet — which holds USDC — they call SolMask's deposit instruction with 1,000 USDC. Deposits are free, so the protocol commits a shielded note worth the full 1,000 USDC to the Merkle tree (the 23 bps fee is charged later, at withdraw time). The depositor's wallet shows a 1,000 USDC outflow to the SolMask pool; the chain knows the pool received 1,000 USDC; the chain does not know what the note's value is, who can spend it, or where it's going. The note's `unlock_slot` is set to the current slot plus the configured delay — typically a few minutes in slots, long enough to make timing-based correlation against the deposit transaction noisy. The payer's part is now done. After the unlock slot has passed, the withdraw is constructed. The depositor has shared the `nullifier_secret` and the recipient address with the relayer via the standard withdraw request (the relayer cannot derive identity from these; it only sees what it needs to build the proof and submit the transaction). The relayer builds the Groth16 proof against the current Merkle root, signs the transaction, and submits. The withdraw executes. The program verifies the proof, computes the nullifier from `nullifier_secret`, checks it's unspent, marks it spent. It creates Maya's USDC ATA. The relayer pays the 0.003 SOL withdraw fee from its own wallet and keeps a small USDC tip slice to recoup it. The vault releases 1,000 USDC; Maya's ATA receives that amount net of the tiny tip. Her wallet shows ~1,000 USDC. Her SOL balance is still zero. The chain now shows: (1) the payer's treasury sent 1,002.30 USDC to the SolMask pool, (2) the SolMask withdraw vault sent 1,000 USDC to Maya's fresh address. No on-chain link between (1) and (2). The Merkle root has been updated; the nullifier set grew by one. A chain analyst drawing an edge between the payer and Maya finds the pool in the way and the cryptography unbroken. Maya now treats her fresh wallet like a one-time envelope. If she wants to swap the USDC, she can do so directly on the fresh wallet — but the moment she does, the wallet's purpose becomes more visible on-chain. If she sends the USDC onward to a centralised exchange under her name to off-ramp to fiat, the privacy ends there (which is fine; that's the standard endpoint). If she sends it onward to her main wallet, she has just linked her main wallet to a SolMask withdraw, which is recoverable in the chain analyst's graph and at minimum tells the world "Maya received funds privately from someone." Whether that's a problem depends on her threat model. For a sanity-checking flow that confirms the withdraw landed in the correct address and the note was spent exactly once, see [/learn/verifying-your-deposit](/learn/verifying-your-deposit). ## What does this not solve? Network-layer linkage. SolMask breaks the on-chain link between deposit and withdraw; it does not break the network-level link between you and whatever RPC endpoint you use to query Solana. If the relayer submits the withdraw via a major public RPC, and five seconds later you open your fresh wallet and it pings the same RPC from your home IP to fetch the new balance, an adversary with access to RPC logs sees both endpoints and can correlate. Not unique to SolMask — it's a property of any system using shared network infrastructure for the private operation and what follows. The mitigation, in increasing order of effort: use a different wallet app on a different device to check the balance and to make the first onward transaction; use a different RPC endpoint than the relayer's (Phantom and Solflare both let you configure custom RPCs); use a different network entirely (a different ISP, a VPN with a clean exit, Tor for the wallet's RPC traffic). For most users this is overkill. For a sufficiently determined adversary it is necessary. The other thing this does not solve is the out-of-band linkage. The payer knows the recipient address because you told them. If the payer's records are subpoenaed, or their email is breached, or they tweet about it, the link from the recipient address to your identity exists in their world. The protocol's guarantee is that the on-chain link is broken; the off-chain link is your problem to manage. Channel selection matters: Signal beats email; in-person beats Signal; never writing the address down anywhere except where you have to is best. For the SOL-specific flow rather than USDC, see [/private/sol](/private/sol). For the USDC pool details, see [/private/usdc](/private/usdc). ## FAQ **Does the recipient need any SOL in their wallet to receive a SolMask withdraw?** No. The withdraw transaction creates the recipient's ATA if missing and pays the rent from the relayer's account. The relayer also fronts the 0.003 SOL protocol fee and recoups it from a small tip slice in the withdrawn asset, while absorbing the network gas itself. A wallet with literally zero balance can receive USDC, USDT, or SOL. **Should I fund the fresh wallet with a little SOL from my main wallet, just to be safe?** No, and this is the single most common deanonymisation mistake. Funding the fresh wallet from your main wallet creates a permanent, public on-chain edge from a wallet linked to your identity to the wallet you just took pains to keep clean. The withdraw transaction handles all gas requirements internally; you do not need to pre-fund anything. **Can I receive to a Ledger-derived address?** Yes. The withdraw does not care whether the recipient is a hot wallet, a Phantom subaccount, a Solflare subaccount, or a Ledger-derived address. The privacy properties are identical. The Ledger just adds operational security to whatever you do with the funds after they arrive. **What if the sender already knows my main wallet — does that ruin the privacy?** It depends on whether they make the link explicit. The on-chain withdraw goes to your fresh address, not to your main wallet, so the chain itself does not show "sender paid main wallet." If the sender keeps the fresh address private and doesn't, for example, send a second payment from the same source to your main wallet later, the off-chain link stays with them and the on-chain trail stays clean. The protocol cannot stop someone you've told from talking; it can stop the chain from showing. **Once I receive the funds, can I keep using the fresh wallet?** You can, but every subsequent transaction it makes is publicly linked to the SolMask withdraw that funded it. Treat it as a one-purpose envelope. For ongoing private operations, do another deposit + withdraw to a different fresh address. Reusing the receiving wallet for unrelated activity gradually builds a profile that may not be what you want. **What about the `unlock_slot` delay — does that affect receiving?** The unlock slot is checked at withdraw time, not receive time. If the deposit's unlock slot has not yet passed when the withdraw is attempted, the proof verification fails. From the recipient's perspective this just means there's a short delay between the sender depositing and the funds arriving at the fresh address. The delay is what prevents instant timing-based correlation between the deposit and the withdraw. **Can I give the same fresh address to multiple senders?** Technically yes, but you should not. Multiple senders depositing to fund withdraws to the same recipient address creates a pattern — same destination, different sources, similar amounts — that is observable on the chain even though each individual link is broken. If you are receiving from multiple parties, use a fresh recipient address for each, and treat each receive as an independent flow. -------------------------------------------------------------------------------- title: "The privacy delay, explained: why SolMask makes you wait" description: "Why every SolMask deposit picks a delay between 10 minutes and 1 week, what the unlock_slot actually buys you, and how to choose for your use case." last_updated: "2026-05-26" source: "https://solmask.xyz/en/blog/the-privacy-delay-explained" -------------------------------------------------------------------------------- ## What is the SolMask delay, mechanically? When you deposit into the SolMask shielded pool, the UI asks you to pick a delay. The default is 10 minutes; the dropdown offers everything from "Instant" up to 1 week. That choice is not cosmetic. The frontend takes your chosen delay, converts it from wall-clock time to Solana slots — currently 1,500 slots for 10 minutes, 216,000 for 1 day, 1,512,000 for a week, computed against the rolling average slot time — and bakes that slot count into the note. Specifically, the deposit instruction reads `Clock::get()?.slot`, adds your delay-in-slots, and stores the result as the note's `unlock_slot` field inside the Poseidon commitment. The commitment, with `unlock_slot` already inside, is what lands on-chain as a leaf in the Merkle tree. From that moment on, your `unlock_slot` is part of the note's mathematical identity. You cannot edit it. You cannot lower it. The on-chain withdraw verifier receives the current slot as a public input to the proof, and the circuit asserts `current_slot ≥ unlock_slot`. If you try to withdraw early, the proof generation itself either fails (snarkjs detects the constraint violation client-side) or, if you manually constructed a proof claiming an inflated current slot, the on-chain verifier rejects it because the slot in the public input is checked against the actual `Clock` sysvar. There is no admin override. There is no fast-forward. The delay is a contract you sign with the protocol at deposit time, and the protocol enforces it without exception. This applies per note. If you make three deposits with different delays, each note has its own `unlock_slot`. A 10-minute deposit unlocks ten minutes after it lands; a 1-week deposit unlocks a week after it lands; they do not interfere with each other. You can interleave them freely and withdraw each on its own schedule. The pool does not track delays per user — it cannot; users are not a concept the protocol observes. It tracks them per note, encoded inside each individual commitment. ## What does the delay actually buy you? The delay buys time for other deposits to accumulate. Privacy in a shielded pool is statistical: when you withdraw, your withdraw is mathematically indistinguishable from any of the other valid notes you *could* have been withdrawing. The set of all such notes is your anonymity set. The longer you wait between deposit and withdraw, the more notes have landed in the pool that match your mint and amount-band, and the larger your anonymity set is when you finally pull funds out. Consider a chain analyst watching the pool. They see deposit transactions land. They see withdraw transactions land. They cannot read the proofs to figure out which deposit corresponds to which withdraw — the Groth16 proof reveals nothing about the leaf. What they can do is filter by timing. If wallet A deposits 1 SOL at 14:02:00 and a fresh wallet B receives 0.9947 SOL out of the pool at 14:02:15, the analyst can shortlist the candidate set to deposits that landed before 14:02:15 and have not yet been withdrawn. If only wallet A's deposit fits, the analysis is trivial. The delay is what defeats this. With a 10-minute delay, any deposit made in the same 10-minute window is a candidate; with a 1-day delay, any deposit in the same day is a candidate; with a 1-week delay, any deposit in the same week is a candidate. The bigger the window, the more deposits, the more candidates, the less the analyst learns. Note that anonymity-set size is not literally "all deposits in the window." It is the set of *unspent* notes of the same mint with the same `unlock_slot ≤ current_slot` bound. Notes that have already been withdrawn are removed from the candidate set, because their nullifiers are now in the on-chain nullifier set and the analyst can see that. So a deposit made an hour ago, withdrawn forty minutes ago, no longer contributes to your anonymity set. This is why a high-throughput period is more valuable than a low-throughput one: lots of *currently unspent* notes is what matters, not lots of historical activity. The delay is the lever you have over this. You cannot control how many other people deposit; you can only control how long you are willing to wait, in exchange for whatever growth the pool happens to produce in that window. On a typical day the SolMask pool sees enough activity that 10 minutes covers a non-trivial set of candidates for sub-1-SOL deposits; for larger amounts you want longer, because the amount-band slice of the pool is thinner. ## Why is the default 10 minutes, and not zero or an hour? The default exists to protect users who pick the default. That is most of them. So the default has to be defensible without any thought from the user. Zero — or "Instant" — is a foot-gun for someone who actually needs privacy. The naive timing-correlation attack is so easy that any analyst with a CSV of deposits and withdraws can run it in seconds. Same-amount, same-asset, same-second matches reduce to a one-line filter. If we made "Instant" the default we would be selling users a privacy product that does not deliver privacy. We refuse to do that. An hour, on the other hand, is too punishing for the most common case: a first-time user testing a 0.1 SOL send. If the test takes an hour to complete, the user leaves. Worse, an hour-long default trains users to think SolMask is slow when in fact it does not have to be — the underlying proving and on-chain settlement take seconds. Ten minutes is the smallest delay that defeats the naive timing attack while still letting a user complete a deposit-and-withdraw cycle in under twenty minutes end-to-end. It is what we recommend as the floor for any send where you actually care about the link being broken. Below that, you are essentially saying "I want the deposit and the withdraw to look related to anyone watching, but I want the proof anyway" — which is occasionally a real use case (testing, internal accounting) and which we support via the "Instant" option, but it is not what most users want. ## When should you use a longer delay? Three categories of sends should never use the default. Payroll. If you are paying employees from a treasury wallet, the source side of every transfer is identifiable — your treasury wallet has a public history. The whole point of routing payroll through SolMask is to sever the link between "this treasury paid out 4.2 SOL" and "this contractor received 4.197 SOL." With a 10-minute delay, an analyst correlating the two is looking at a window of, optimistically, dozens of deposits. With a 1-day delay, that window contains thousands. Always pick at least a 1-day delay for payroll. 3 days is better if you can absorb the latency. Donations from public-facing wallets. Same logic. If your wallet is identified — as a project treasury, a founder address, a known philanthropist — and you want the recipient not to be link-identified to you, the source side is the high-value secret. Use a delay long enough that the destination cannot be timing-correlated back. A day at minimum; a week for high-stakes donations where the link is the actual sensitive thing. OTC settlement and treasury operations. Anything an investigator might look at retroactively, asking "where did this counterparty's funds come from?" deserves the maximum delay. The cost is calendar time, not money. Three days or a week. The expense feels worse than it is — you are not blocked from doing anything else with the originating wallet during the delay, and the destination wallet does not need to exist yet at deposit time. A common mistake is to underweight the delay because the wait *feels* like the cost. It is not. The cost is the calendar time, not your attention. You set the delay, you close the tab, you come back later, the funds are withdrawable. Treat the delay as a free resource you happen to be denominating in hours instead of dollars. ## When does "Instant" actually make sense? It makes sense when you do not need privacy and you know that you do not need privacy. Concretely: testing the protocol on devnet or mainnet with a tiny amount; debugging a relayer integration; moving funds between two wallets that you already publicly control and where the link being visible is a non-issue. We support Instant because being honest about what the product does requires being honest about what it does not do — sometimes a user does not need privacy, and forcing them to wait 10 minutes to confirm a click is hostile. Use Instant when the privacy is not the point. Do not use it when it is. We have considered, and rejected, the idea of hiding the Instant option behind a "developer mode" toggle. The rationale: someone who wants Instant can find it; someone who does not want it would not pick it; and surfacing it explicitly is more honest than burying it. See [/docs/threat-model](/docs/threat-model) for the broader articulation of what assumptions a SolMask deposit makes about the user. ## What does the delay *not* do? The delay does not change anything about the network layer. Your IP is visible to whatever RPC you sent the deposit through, and to the relayer (if you used one) at withdraw time. A patient observer who controls or subpoenas the relayer's logs can correlate IPs across deposit and withdraw regardless of how long the delay was. If the network layer is part of your threat model, route through Tor, a trusted VPN, or your own infrastructure — the delay does not help with this and was never intended to. The delay does not change the recipient address. You picked a recipient at withdraw time, not at deposit time. The note does not commit to a destination — only to an amount, a `secret`, a `nullifier_secret`, an `unlock_slot`, and a mint. The recipient is a public input to the withdraw proof, chosen freely at that moment. This is a deliberate design: locking the recipient in at deposit time would leak the relationship if someone observed the deposit and the recipient's later receipt; leaving it free lets you decide who gets the funds long after the deposit has gone cold. See [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address) for the guidance on what makes a good recipient address. The delay does not influence anonymity-set composition beyond timing. It does not, for instance, force the pool to wait until enough other deposits have landed before yours becomes withdrawable. The slot bound is a wall-clock bound, not an activity bound. If the pool is dead for the entire week of your 1-week delay, your withdraw will be lonely; the delay does not detect or compensate for that. In practice this is rarely an issue — the pool sees enough activity at common amount-bands — but it is the correct mental model: the delay buys you a *window of opportunity*, not a guaranteed crowd. ## A practical recommendation matrix The mapping we recommend, in plain English: **Instant.** Use for testing, debugging, and intra-account moves where you genuinely do not need privacy. Treat as a developer affordance, not a privacy mode. **10 minutes (default).** Use for casual sends, drop-off transfers, and anything where the recipient is short-lived and not high-value. The default exists because it is good enough for the median case; if you do not have a specific reason to pick differently, pick this. **1 hour.** Use for sensitive personal sends where the link matters but the latency does not — paying someone whose receipt you do not want correlated with your name, for instance. The hour buys you a meaningfully larger candidate set than 10 minutes without costing you a workday. **6 hours to 1 day.** Use for payroll, donations from public-facing wallets, and most treasury operations. This is the right floor for any send where the source side is publicly identifiable and the destination side is meant to be unlinked. A day is rarely too long; an hour is usually too short. **3 days to 1 week.** Use for high-stakes treasury, OTC settlement, and any send that a sophisticated investigator might examine retroactively. The cost is wall-clock time you would not be using anyway; the benefit is a candidate set thick enough to make timing analysis hopeless. The slot-conversion constants and unlock-slot enforcement live in the on-chain program's deposit and withdraw paths. See [/glossary/unlock-slot](/glossary/unlock-slot) and [/glossary/shielded-pool](/glossary/shielded-pool) for the canonical definitions, and [/docs/threat-model](/docs/threat-model) for what assumptions each delay setting implicitly makes about your adversary. ## FAQ **Can I lower my delay after depositing?** No. The `unlock_slot` is committed inside the Poseidon hash that became your note's on-chain leaf. Changing it would require regenerating the leaf, which would require knowing the secret to construct a new commitment, which would be a different note — not the one you deposited. The chain enforces the original. **What happens if I try to withdraw early?** The proof generation fails client-side because the constraint `current_slot ≥ unlock_slot` is violated. If you somehow constructed a proof with a forged current slot, the on-chain verifier rejects it: the public-input slot is checked against `Clock::get()?.slot`, which the program reads directly. **Does the delay affect the withdraw fee?** No. Deposits are free, and the withdraw fees (the 23 bps percentage fee and the 0.003 SOL flat fee) are independent of delay. Picking a longer delay does not cost more or less. See [/docs/fees](/docs/fees). **If I pick 1 week, can I withdraw any time after a week, or only at exactly 1 week?** Any time after. The constraint is `current_slot ≥ unlock_slot`, not equality. Once the unlock slot is reached, the note stays withdrawable indefinitely — there is no expiry. **Does the pool keep my note safe during the delay?** Yes. The note's commitment is a permanent leaf in the on-chain Merkle tree; the funds are held by the program vault. No one can withdraw your note without knowing your `secret` and `nullifier_secret`. The delay only restricts *when* you can withdraw, not the safety of the funds in between. **Can a longer delay actually hurt me if the pool goes quiet?** In theory yes — if no other deposits land during your entire delay window, your withdraw is correlatable to the deposits that did land. In practice the active windows for common amount-bands are dense enough that this is not a real concern at 10 minutes or above. If you are depositing an unusual amount (e.g., 47.3 SOL), pick a longer delay specifically to give the amount-band time to fill out. **Is the delay visible on-chain?** Indirectly. The `unlock_slot` is inside the commitment, not exposed. But an observer can see *when* you deposited and *when* you withdrew, and infer a lower bound on your chosen delay from the gap. The choice itself is not separately published. You cannot, however, hide that the gap was at least that long. -------------------------------------------------------------------------------- title: "Why we built on zero-knowledge proofs" description: "Public chains broadcast everything. ZK is how you get privacy back without giving up self-custody." last_updated: "2026-05-25" source: "https://solmask.xyz/en/blog/why-zero-knowledge" -------------------------------------------------------------------------------- **Solana, like every public ledger, broadcasts everything.** Every transfer, every balance, every interaction is permanently visible to anyone with an internet connection. That is a feature for auditability — and a liability for ordinary financial privacy. Zero-knowledge proofs change the geometry of what is provable. With a ZK proof you can demonstrate that you own one of the deposits in a shielded pool without revealing which one. The proof is short, the verification is fast, and the secret never leaves your device. For SolMask, that means a withdraw is mathematically unlinkable to its corresponding deposit. There is no statistical trick, no timing leak, no amount fingerprint that an outside analyst can use to bridge the two — provided you used a sensible privacy delay so your withdraw lands in a crowd. SolMask uses Groth16 over a BN254 Poseidon Merkle tree. Proofs are around 200 bytes, generate in three to five seconds in your browser, and verify on-chain in single-digit milliseconds. The verifier code is public and on-chain; you do not need to trust us. -------------------------------------------------------------------------------- title: "Accepting crypto donations privately on Solana" description: "A public donation address turns every supporter's wallet — and your total raise — into open data. How creators, nonprofits, and open-source projects can take donations on Solana without exposing donors or treasury." last_updated: "2026-05-30" source: "https://solmask.xyz/en/blog/accepting-crypto-donations-privately" -------------------------------------------------------------------------------- **The moment you publish a donation address, you've published a public ledger of everyone who supports you and exactly how much you've raised.** That's the part most projects don't think through. A single posted address is convenient, but every contribution to it is permanent, public, and attributable — anyone can see each donor's wallet, the amount, the timing, and your running total. For a creator that's a privacy problem for supporters; for a nonprofit or an open-source project it can be a safety problem for donors and a strategic one for the organization. This post is about taking donations on Solana without turning them into surveillance data — protecting both the donor's privacy and your treasury's. ## What a public donation address actually exposes Post one address and accept everything to it, and the chain now records: - **Every donor's wallet.** A supporter who donates from a wallet tied to their identity has just published "I support this project" forever. For politically sensitive causes, journalism, or anything controversial, that link can be a real risk to the donor. - **Your exact total, live.** Competitors, counterparties, and the curious can read your raise in real time. There's no "we don't disclose" — it's on the explorer. - **Your spending.** Every outflow from the donation address is equally public. Vendors, salaries, and grants paid from it are all attributable, which is the same problem treasuries face: [/blog/dao-treasury-privacy-on-solana](/blog/dao-treasury-privacy-on-solana). Encrypting amounts wouldn't fix the core issue here — the *relationships* (who supports you, who you pay) are the sensitive data, and those stay public even if the figures are hidden. [/blog/encrypted-amounts-are-not-private](/blog/encrypted-amounts-are-not-private) covers why. ## Two sides to protect: the donor and the recipient Donation privacy has two independent halves, and which one you care about shapes the setup. **Protecting donors** means a supporter can give without publishing the link between their identity-bearing wallet and your cause. The donor does this on their side: they deposit into the shielded pool from their own wallet, then have the withdraw delivered to your donation address. The chain shows their wallet depositing into a pool and an unrelated withdraw arriving at your address — no edge connecting the supporter to you. From your side, you just receive funds. (If you publish guidance for donors, [/blog/sending-sol-without-revealing-your-main-wallet](/blog/sending-sol-without-revealing-your-main-wallet) is the walkthrough to point them at.) **Protecting the organization** means your *total* and your *spending* aren't an open book. Rather than accumulating everything in one visible address, funds can be moved through the pool so that the inflows you control and the outflows you make aren't trivially tied into a single public balance and payment graph. ## A workable setup There's no single "donation button" that does all of this magically, but the pieces compose cleanly: 1. **Accept to a rotating set of fresh addresses, not one forever-address.** A single long-lived donation address is the worst case — it aggregates everything. Rotating destinations limits how much any one address reveals. Each should be fresh in the sense of [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address). 2. **Tell privacy-conscious donors they can give through the pool.** Donors who want to hide their support deposit into the shielded pool and direct the withdraw to your address. Their identity wallet never appears connected to you. 3. **Accept the asset you actually want.** If you'd rather hold USDC than SOL, donors (or you, on withdraw) can convert inside the private step — [/blog/swapping-sol-to-usdc-privately](/blog/swapping-sol-to-usdc-privately) — so you're not doing a public, correlatable swap of donation funds afterward. 4. **Move accumulated funds through the pool before spending.** When you pay a vendor or a contributor, routing through the shielded pool breaks the link between "donations in" and "this specific payment out," the same discipline as any private payout. ## The honest limits A few things to be straight about, because overpromising on donor safety is worse than useless: - **The donor controls their own privacy.** If a donor gives directly from an identity-linked wallet to your public address, *you* can't retroactively hide that for them. Donor-side privacy is a donor-side action. - **Off-chain disclosure still leaks.** If a donor publicly tweets "just gave 10 SOL to X" and the timing matches a pool withdraw to your address, the inference is theirs to avoid. Privacy is a pipeline; the chain is one stage. [/learn/what-solmask-cannot-protect-you-from](/learn/what-solmask-cannot-protect-you-from). - **Compliance and record-keeping are your responsibility.** Privacy of the public graph and your own bookkeeping obligations are separate things; this post is about the former, not advice on the latter. ## Where to start Read the [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist) for the operational discipline, then set up your first private receive flow at [/swap](/swap). If you run a treasury alongside donations, [/blog/dao-treasury-privacy-on-solana](/blog/dao-treasury-privacy-on-solana) extends the same ideas to vendor and contributor payments. ## FAQ **Q. Can I make my total raise completely invisible?** **A.** You can make it much harder to read by not aggregating everything in one public address and by moving funds through the pool, but on a public chain, perfect treasury opacity is not a guarantee anyone honest should promise. The goal is removing the easy, one-glance read. **Q. How does a donor donate privately?** **A.** They deposit into the shielded pool from their own wallet and have the withdraw delivered to your address. The connection between their wallet and your cause never becomes a public edge. Point them at [/blog/sending-sol-without-revealing-your-main-wallet](/blog/sending-sol-without-revealing-your-main-wallet). **Q. Should I publish one donation address or many?** **A.** Prefer rotating fresh addresses. A single forever-address aggregates every contribution and payment into one fully public balance and graph. **Q. Can I accept donations in USDC even if donors send SOL?** **A.** Yes — the conversion can happen inside the private withdraw so there's no separate public swap. See [/blog/swapping-sol-to-usdc-privately](/blog/swapping-sol-to-usdc-privately). -------------------------------------------------------------------------------- title: "Bridging to Solana and arriving on a private wallet" description: "Bridging assets to Solana lands them on an address tied to your source-chain identity and the bridge transaction. Here's how to bridge and end up on a fresh Solana wallet with no on-chain link back to where the funds came from." last_updated: "2026-06-01" source: "https://solmask.xyz/en/blog/bridging-to-solana-and-arriving-privately" -------------------------------------------------------------------------------- **A bridge moves your assets across chains, but it also carries your identity with them.** When you bridge from Ethereum, an L2, or another chain to Solana, the bridge records the source address, the destination address, the amount, and the time — on both sides. The Solana wallet your funds land on is now publicly tied to the source-chain wallet you bridged from, and that source wallet often has years of history, an ENS name, or an exchange link attached. You've changed chains, not identities. This is the practical guide to bridging to Solana and ending up on a fresh wallet that has no on-chain link to where the assets came from. For the underlying reason every transfer is a public link, see [/blog/what-the-blockchain-reveals-about-you](/blog/what-the-blockchain-reveals-about-you). ## Why bridging alone doesn't give you a clean wallet The bridge deposit on Solana is a normal, public transaction: it names the address that received the bridged funds. Spending from that address links everything you do to your cross-chain trail. Routing through a couple of Solana wallets afterward doesn't help — each hop is public. To get a genuinely fresh starting point you need a step where your funds mix with everyone else's: a shielded pool. You deposit the bridged funds behind a commitment and withdraw to a new wallet with a zero-knowledge proof, so the bridge-linked address and your spending address look unrelated on-chain. [/learn/what-is-a-shielded-pool](/learn/what-is-a-shielded-pool) is the plain-English version. ## The flow, step by step **1. Bridge to a dedicated landing wallet.** Bridge your assets to Solana as usual, into a wallet you'll treat as a disposable landing pad — not your long-term wallet. Expect this address to be publicly linked to your source chain; that's fine, because nothing will be spent from it. If you bridge to an asset SolMask doesn't pool directly, swap into SOL, USDC, or USDT first. **2. Deposit from the landing wallet.** Open [/swap](/swap), connect the landing wallet, choose the asset and amount, and deposit. Depositing is free — the full amount enters the pool, with only a commitment hash on-chain ([/blog/fee-model-explained](/blog/fee-model-explained)). **3. Set a privacy delay and let bridge activity settle.** Pick an unlock delay (10 minutes to a week). Bridge transactions have conspicuous timestamps, so an instant deposit-then-withdraw is easy to line up against the bridge event. A longer delay puts distance — and other people's deposits — between the bridge and your withdraw. [/blog/the-privacy-delay-explained](/blog/the-privacy-delay-explained) explains the trade-off. **4. Withdraw to your real fresh Solana wallet.** Generate a new wallet with no history, then withdraw to it. The proof is built in your browser and submitted through the relayer, which broadcasts and pays the network fee — so your fresh wallet needs no SOL and you never fund it from the bridge-linked landing pad ([/glossary/relayer](/glossary/relayer)). [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address) covers picking a clean destination. The result on-chain: a bridge into a landing wallet, a deposit from that wallet into the pool, and — separately — an unrelated withdraw to a fresh wallet you actually use. The cross-chain trail dead-ends at the pool. ## The mistakes that undo all of it - **Spending directly from the bridge landing wallet.** That address is linked to your source chain. Treat it as a one-way stop into the pool, never as a wallet you transact from. - **Funding the fresh wallet for gas from the landing pad.** A top-up reconnects them. The relayer covers the withdraw fee. - **Bridging an exact amount and withdrawing it instantly.** Distinctive amount plus tight timing re-links the bridge event to your withdraw. Use the delay. - **Carrying a labeled identity across the bridge.** If your source-chain wallet is doxxed and you immediately reuse the same handle or NFT on the fresh Solana wallet, the inference survives a clean on-chain leg. [/learn/what-solmask-cannot-protect-you-from](/learn/what-solmask-cannot-protect-you-from) is the honest list. For the condensed ruleset, see [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist). ## FAQ **Q. Can I hide the bridge transaction itself?** **A.** No — the bridge logs the transfer on both chains. What you break is the link between the bridge-linked landing wallet and the wallet you ultimately spend from, so the cross-chain trail stops at the pool. **Q. What if I bridge an asset SolMask doesn't support?** **A.** Swap it into SOL, USDC, or USDT on the landing wallet before depositing. You can also deposit one supported asset and withdraw another via a swap on the way out. **Q. Does my fresh Solana wallet need SOL before it can receive the withdraw?** **A.** No. The relayer pays the network fee, so the fresh wallet can start from zero — which is what keeps it unlinked from the bridge. **Q. Do I need a separate landing wallet, or can I bridge straight into the pool?** **A.** Bridges deliver to a normal wallet address, not into the pool, so you need a landing wallet to receive the bridged funds and then deposit. Keeping it disposable — used only to bridge in and deposit — is what isolates the cross-chain link. **Q. Will the landing wallet show that it deposited?** **A.** Yes — it publicly shows a deposit into the pool, the same as every other depositor. It does not reveal which fresh wallet later received the withdraw. -------------------------------------------------------------------------------- title: "DAO and treasury privacy on Solana" description: "A DAO treasury on Solana is a fully public bank statement — every vendor, salary, grant, and runway figure visible to competitors and counterparties. How to keep treasury operations confidential without giving up self-custody." last_updated: "2026-05-31" source: "https://solmask.xyz/en/blog/dao-treasury-privacy-on-solana" -------------------------------------------------------------------------------- **A DAO treasury is the only corporate bank account in the world where your competitors can read every transaction in real time.** The transparency that's a feature for governance is a liability for operations. Every vendor you pay, every contributor's salary, every grant, every market-making allocation, and your exact runway are all sitting on a public explorer, labeled by anyone who bothers to map your wallets. Competitors price against your runway. Counterparties see what you pay everyone else before they negotiate. Contributors see each other's compensation. None of that is a governance benefit — it's just leakage. This post is about keeping treasury *operations* confidential while keeping the treasury itself self-custodied and auditable to the people who should see it. ## What a public treasury actually leaks Point a block explorer at a known treasury wallet and you can read: - **Runway.** Balance plus burn rate equals a precise estimate of how long the organization survives. That's a negotiating weapon in anyone's hands. - **Every counterparty.** Vendors, auditors, exchanges, market makers, law firms paid in crypto — all attributable by following outflows. - **Compensation.** Contributor and core-team payments, often inferable down to individuals once one address is doxxed. - **Strategy.** A sudden large transfer to a market maker, a new grant program, an acquisition payment — visible before any announcement. This is the same exposure a public donation address creates, scaled up to an entire organization — see [/blog/accepting-crypto-donations-privately](/blog/accepting-crypto-donations-privately). And it's why amount-encryption alone doesn't solve it: the *relationships* (who you pay, who pays you) are the sensitive part, and those stay public even with hidden figures. [/blog/encrypted-amounts-are-not-private](/blog/encrypted-amounts-are-not-private). ## Confidential payments without giving up custody or auditability The goal isn't to make the treasury a black box to its own members — it's to stop the *outside* from reading every operational detail off-chain. The pattern is to route payments through a shielded pool so the public link between "treasury" and "this specific vendor payment" is broken, while the organization keeps its own records. A treasury deposits into the shielded pool, then pays each counterparty as a withdraw using a zero-knowledge proof. On-chain, an observer sees the treasury depositing into a pool and a set of unrelated withdraws landing at various addresses — but can't tie a given payment back to the treasury or to each other. [/learn/what-is-a-shielded-pool](/learn/what-is-a-shielded-pool) is the base mechanism; the [/glossary/relayer](/glossary/relayer) is why recipients don't need gas to be paid. Crucially: - **Self-custody is intact.** Funds are released only by a proof that the treasury's signers authorize; nobody else can move them. This isn't a custodial mixer, it's a self-custodied pool ([/glossary/shielded-pool](/glossary/shielded-pool)). - **Auditability is a choice, not a default.** The organization retains its own off-chain records and can disclose to auditors, members, or regulators selectively — instead of disclosing everything to everyone by default. - **Pay in the right asset.** Vendors invoicing in USDC can be paid in USDC even if the treasury holds SOL, with the conversion inside the withdraw rather than a public swap — [/blog/swapping-sol-to-usdc-privately](/blog/swapping-sol-to-usdc-privately). ## Common treasury flows - **Contributor payroll.** Pay a roster without publishing each person's compensation or the full team graph. This is the dedicated walkthrough: [/blog/private-payroll-on-solana](/blog/private-payroll-on-solana). - **Vendor and service payments.** Auditors, infra, legal, marketing — paid without exposing the relationship or the amount as a clean public edge. - **Grants and ecosystem funding.** Fund recipients without broadcasting the program's size and cadence before you're ready to announce it. - **Market-making and OTC allocations.** Move size to a counterparty without signaling it to the market — [/blog/private-otc-trades-on-solana](/blog/private-otc-trades-on-solana). ## Discipline at the org level The operational rules from [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist) apply, with a treasury twist: - **Don't pay everyone from one visible address in a tight batch.** A cluster of withdraws timed together, in distinctive amounts, can be grouped by an observer. Space and round payments. - **Use a real privacy delay** between funding the pool and paying out — [/blog/the-privacy-delay-explained](/blog/the-privacy-delay-explained). - **Fresh recipient addresses for counterparties** wherever feasible; a vendor wallet with attributable history re-links the payment ([/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address)). - **Match the crowd on amounts.** An outlier payment from a thin pool re-identifies the source — [/blog/anonymity-sets-on-solana](/blog/anonymity-sets-on-solana). ## The honest boundary Confidential operations are not the same as escaping obligations. A DAO still owes its members governance transparency where it has promised it, and may owe disclosures to auditors or regulators — privacy from the *public market* and selective disclosure to *the right parties* are compatible, but the second is the organization's job, not the protocol's. And no public anonymity set defeats an adversary who already has your off-chain records. [/learn/what-solmask-cannot-protect-you-from](/learn/what-solmask-cannot-protect-you-from) and [/docs/threat-model](/docs/threat-model). Start by mapping which treasury flows are most exposed today, then move those through the pool first. Begin at [/swap](/swap). ## FAQ **Q. Doesn't a DAO need a transparent treasury for governance?** **A.** It needs transparency *to its members and auditors* — which selective disclosure provides. It does not need to broadcast every vendor, salary, and runway figure to competitors and the open market. Those are different audiences. **Q. Is this custodial? Who controls the funds?** **A.** The treasury does. Funds leave only via a proof its signers authorize. It's a self-custodied shielded pool, not a third party holding your money. **Q. Can we still produce records for an audit?** **A.** Yes. The organization keeps its own off-chain records and discloses them to whoever it chooses. Public-graph privacy and internal record-keeping are independent. **Q. Can we pay vendors in USDC from a SOL treasury?** **A.** Yes — convert inside the withdraw so there's no separate public swap of treasury funds. See [/blog/swapping-sol-to-usdc-privately](/blog/swapping-sol-to-usdc-privately). **Q. What's the first flow to move private?** **A.** Usually payroll and recurring vendor payments — the ones that, repeated on a public schedule, leak the most about team size, cadence, and runway. Start there: [/blog/private-payroll-on-solana](/blog/private-payroll-on-solana). -------------------------------------------------------------------------------- title: "Encrypted amounts are not private: hiding the number isn't hiding the link" description: "Confidential-transfer schemes that encrypt the amount still leave the sender, the recipient, and the connection between them fully public. Why hiding the number is the easy half of privacy — and the half that matters least." last_updated: "2026-05-28" source: "https://solmask.xyz/en/blog/encrypted-amounts-are-not-private" -------------------------------------------------------------------------------- **There are two different things people mean by "private," and most tools only do the easy one.** You can hide *how much* you sent, or you can hide *who you sent it to*. These are not the same problem, and solving the first does almost nothing for the second. A growing class of confidential-transfer schemes encrypts the amount on a transfer — the number is hidden, the balances are encrypted — and presents this as privacy. It is a real and useful feature. It is also the half of the problem that leaks the least information. This post is about the other half: the link between sender and recipient, which amount-encryption leaves completely intact. ## What an encrypted amount actually hides Picture a normal Solana transfer with the amount blanked out. An observer still sees: wallet A sent *something* to wallet B, at this timestamp, in this token. The graph edge is there. The two addresses are there. The timing is there. The only thing missing is the figure in the amount field. For a lot of real-world deanonymization, the figure is the least valuable piece. If an investigator, a counterparty, or a curious stranger can already see that your wallet pays a specific recipient every two weeks, they have your relationship, your cadence, and — by cross-referencing other public transfers — usually a good estimate of the amount anyway. Encrypting the number raises the effort slightly. It does not break the connection. And on a transparent ledger, the connection is the sensitive part. Your counterparties, your payroll, your trading relationships, the fact that wallet A and wallet B are the same person — those are graph properties, not amount properties. They survive amount-encryption untouched. ## The thing that actually has to disappear: the edge Privacy on a public chain is fundamentally about the graph, not the labels on the edges. To get real unlinkability you have to remove the edge itself — make it so that no observer can draw a line from where funds went in to where they came out. That requires a shielded pool. Many people deposit behind cryptographic commitments; each later withdraws using a zero-knowledge proof that asserts "I own one of the unspent deposits in this pool" without revealing which one ([/learn/how-zk-proofs-work](/learn/how-zk-proofs-work)). The deposit and the withdraw are two separate transactions with no edge between them. The observer sees a deposit from wallet A and an unrelated withdraw to wallet B and genuinely cannot tell whether they're the same person — because the math that would let them tell was never published. Note what changed. It isn't that the amount is encrypted. It's that the *connection* is gone. That's a structurally harder thing to build — it needs a Merkle tree of commitments, a nullifier scheme to prevent double-spends, and a real proving system ([/glossary/shielded-pool](/glossary/shielded-pool), [/glossary/nullifier](/glossary/nullifier)) — which is exactly why amount-encryption is the more common offering. It's the easier engineering problem. ## "But everyone uses the same amounts" — why the pool still needs care Breaking the edge is necessary but not automatically sufficient, and this is where amounts come back into the story — from a completely different direction. Inside a shielded pool, the danger isn't that your amount is *visible*; it's that your amount is *distinctive*. If every deposit in the pool is roughly 0.5 SOL and you deposit 47, then when 47 comes back out the math points at you regardless of how good the proof was. The proof hides which note you spent; an outlier amount narrows the suspect list to one anyway. So in a shielded pool the goal is the opposite of encryption: you *want* your amount to look like everyone else's. Deposit and withdraw in amounts that match the crowd, in round figures, and you blend into the largest possible set. [/blog/anonymity-sets-on-solana](/blog/anonymity-sets-on-solana) works through exactly how amount bands shape who you hide among. This is a more honest treatment of amounts than encryption gives you: the number isn't the secret, the *similarity to others* is the protection. ## A quick way to tell which kind of "private" a tool offers Ask one question: **after I use it, can an observer still see that my wallet is connected to the recipient?** - If the answer is "yes, but they can't see how much" — it's amount privacy. Useful for hiding balances and payment sizes; useless for hiding relationships. - If the answer is "no, the connection itself is gone" — it's link privacy, which is what a shielded pool provides. Most things sold as confidential transfers are the first kind. For the threat models people actually care about — not wanting a counterparty to map your treasury, not wanting a recipient to see your whole net worth, not wanting your payroll graph to be public — you need the second. ## Where to go next If you want to see link privacy in practice, [/blog/sending-sol-without-revealing-your-main-wallet](/blog/sending-sol-without-revealing-your-main-wallet) is the step-by-step. If you want the honest boundaries — what even link privacy doesn't fix — [/learn/what-solmask-cannot-protect-you-from](/learn/what-solmask-cannot-protect-you-from) is the unvarnished list. And [/compare](/compare) lays out how different designs in this space actually differ. ## FAQ **Q. Isn't encrypting the amount still better than nothing?** **A.** Yes, for the narrow goal of hiding balances and payment sizes. Just don't mistake it for unlinkability — it leaves the sender, recipient, and timing fully public, which is usually the information people actually wanted to protect. **Q. If a shielded pool breaks the link, why does it care about amounts at all?** **A.** Because an unusual amount re-identifies you by elimination even when the proof is perfect. Inside a pool you want your amount to resemble the crowd, not to be hidden. It's the inverse of amount-encryption. **Q. Does SolMask encrypt amounts too?** **A.** SolMask hides the *link*, which is the harder and more valuable property. Deposit and withdraw amounts are part of how you blend in, so the guidance is to match the crowd rather than to stand out. See [/blog/anonymity-sets-on-solana](/blog/anonymity-sets-on-solana). **Q. Can I get both — hidden amounts and a broken link?** **A.** Link privacy is the property that defeats graph analysis, and it's what to prioritize. Combine it with good amount hygiene (round figures, match peers) and a fresh recipient address and you've covered what amount-encryption alone can't touch. -------------------------------------------------------------------------------- title: "Paying a freelancer or contractor privately" description: "Paying a contractor in crypto hands them a permanent view of your wallet — your balance, your other payments, who else you pay. Here's how to pay a one-off invoice on Solana without exposing your treasury or your main wallet." last_updated: "2026-06-01" source: "https://solmask.xyz/en/blog/paying-a-contractor-privately-on-solana" -------------------------------------------------------------------------------- **When you pay a contractor from your main wallet, you don't just send them money — you hand them a permanent window into your finances.** The instant the transfer lands, the freelancer (and anyone they share the address with) can open a block explorer and see your balance, every other payment you've made, who else you work with, and roughly what you pay them. For a business or an individual operator, that's a leak of competitive and personal information that has nothing to do with the invoice you're settling. This is the practical guide to paying a one-off invoice on Solana without exposing the wallet you pay from. If you run recurring salaries for a team, [/blog/private-payroll-on-solana](/blog/private-payroll-on-solana) covers that distinct case; this post is about ad-hoc contractor and freelancer payments. ## Why paying directly exposes you A direct transfer writes both addresses, the amount, and the timestamp to a public ledger — that's the link, and it can't be undone by routing through extra wallets first. To pay without exposing your wallet, you put a shielded pool between you and the contractor: you deposit behind a commitment, then withdraw straight to their address with a zero-knowledge proof, so the payment they receive has no on-chain edge back to your treasury. [/learn/what-is-a-shielded-pool](/learn/what-is-a-shielded-pool) is the explainer. This protects *your* side unconditionally. Whether the contractor's receiving address is also private is their choice — but you can pay cleanly regardless. ## The flow, step by step **1. Agree on the asset and a receiving address.** Most invoices are quoted in USDC; SolMask supports SOL, USDC, and USDT. Ask the contractor for a receiving address — and, if they care about their own privacy, suggest they give you a fresh one with no history ([/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address)). **2. Deposit the invoice amount from your wallet.** Open [/swap](/swap), connect the wallet you're paying from, choose the asset and amount, and deposit. Depositing is free — the full amount enters the pool, with only a commitment hash on-chain ([/blog/fee-model-explained](/blog/fee-model-explained)). Budget for the withdraw fee on top so the contractor receives the exact invoiced amount. **3. Use a privacy delay where the timeline allows.** Set an unlock delay at deposit (10 minutes to a week). If the invoice has a few days' runway, a longer delay means your deposit blends with more activity before the payment goes out. [/blog/the-privacy-delay-explained](/blog/the-privacy-delay-explained) explains why. For an urgent same-day payment, the 10-minute floor still breaks the direct link — you just get a smaller crowd. **4. Withdraw to the contractor's address.** Generate the proof in your browser and submit through the relayer, which broadcasts and pays the network fee — so you pay even a brand-new freelancer wallet that holds no SOL, without funding it yourself ([/glossary/relayer](/glossary/relayer)). The contractor receives the funds; on-chain, the payment traces to a pool withdraw, not to your business wallet. ## The mistakes that undo all of it - **Paying recurring invoices to the same fresh wallet on a fixed schedule.** Same amount, same payee, every two weeks, is a pattern even through a pool. For ongoing relationships, treat it like payroll and read [/blog/private-payroll-on-solana](/blog/private-payroll-on-solana). - **Depositing the exact, oddly-specific invoice amount and withdrawing it instantly.** A unique amount in and the same amount out moments later correlates by value and timing. Use the delay; consider depositing a rounder amount than the precise invoice. - **Funding the contractor's wallet for gas.** Never — the relayer covers it. A top-up from your wallet re-links you to the payee. - **Memos and invoices.** Putting an invoice number or your company name in a transaction memo, or emailing a receipt that names both wallets, leaks off-chain what you protected on-chain. [/learn/what-solmask-cannot-protect-you-from](/learn/what-solmask-cannot-protect-you-from) covers the rest. For a one-page summary of every rule here, see [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist). ## FAQ **Q. How is this different from private payroll?** **A.** Payroll is recurring payments to a known set of people on a schedule, which creates timing and amount patterns you have to manage deliberately. This post is for one-off or occasional contractor invoices, where a single clean deposit-and-withdraw is usually enough. See [/blog/private-payroll-on-solana](/blog/private-payroll-on-solana) for the recurring case. **Q. Can I pay a contractor who has no SOL for gas?** **A.** Yes. The relayer pays the network fee and broadcasts, so the contractor receives the full payment to an empty wallet — no need for you to fund it first. **Q. Will the contractor be able to see my main wallet?** **A.** No. They receive funds from a pool withdraw with no on-chain link to the wallet you deposited from. They can see they were paid through SolMask; they cannot see your balance or history. **Q. Can I pay in USDC if I only hold SOL?** **A.** Yes — deposit SOL and route the withdraw through a swap so the contractor receives USDC. See [/blog/swapping-sol-to-usdc-privately](/blog/swapping-sol-to-usdc-privately). **Q. Does my business wallet still show the payment?** **A.** It shows a deposit into the pool — not who you paid. The link between your deposit and the contractor's withdraw is what stays hidden. -------------------------------------------------------------------------------- title: "Private OTC trades on Solana: settling without broadcasting your size" description: "An OTC settlement on a public chain tells the whole market your counterparty, your size, and your timing. How to settle large Solana trades without handing observers a roadmap to your position." last_updated: "2026-05-30" source: "https://solmask.xyz/en/blog/private-otc-trades-on-solana" -------------------------------------------------------------------------------- **The whole point of trading OTC is to move size without moving the market — and then the on-chain settlement broadcasts the size anyway.** Desks and funds go off-exchange precisely to avoid signaling. But the final settlement transfer is a public Solana transaction: it names both counterparties' wallets, the exact amount, and the moment it happened. Anyone watching can see that a particular wallet just received a large block, infer the trade, and front-run your next move or simply map your book. The discretion you bought by going OTC evaporates at the settlement step. This post is about closing that gap — settling OTC blocks on Solana without publishing a roadmap to your position. ## What a public settlement gives away A plain settlement transfer hands an observer four things at once: - **Your counterparty.** The two wallets are now linked on-chain. If either is attributable — a known desk, a labeled fund wallet, an address that's touched an exchange — the relationship is exposed. - **Your size.** The exact block trades hands in the clear. For a fund accumulating or unwinding a position, the size *is* the signal. - **Your timing.** When you settled, and therefore roughly when you executed. - **A thread to pull.** From the receiving wallet, an observer can watch where the block goes next — into a DEX, into staking, split across wallets — and reconstruct your strategy. Hiding only the amount wouldn't save you: the counterparty link and the timing are still public, and those alone let a watcher map relationships and infer size from surrounding activity. The thing that actually has to disappear is the *edge* between the two wallets — see [/blog/encrypted-amounts-are-not-private](/blog/encrypted-amounts-are-not-private). ## Settling through a shielded pool The fix is to make the settlement a withdraw from a shared pool rather than a direct transfer. The paying side deposits into the shielded pool; the settlement is delivered to the counterparty as a withdraw using a zero-knowledge proof. On-chain there's a deposit from one wallet and an unrelated withdraw arriving at another — no edge tying the two desks together, no public "Desk A paid Desk B exactly X at time T." [/learn/what-is-a-shielded-pool](/learn/what-is-a-shielded-pool) is the mechanism; [/glossary/relayer](/glossary/relayer) explains why the recipient doesn't even need gas to receive. What this buys a trading operation specifically: - **Counterparty unlinkability.** The two wallets are never connected by a settlement edge. - **Size discretion.** Combined with amount hygiene (below), the block size isn't a clean, isolated signal. - **Asset flexibility.** Settle in the asset the deal calls for. If the trade is priced in USDC but funded in SOL, convert inside the withdraw rather than doing a public swap afterward — [/blog/swapping-sol-to-usdc-privately](/blog/swapping-sol-to-usdc-privately). ## Discipline that matters more at size OTC blocks are large, which makes the usual hygiene from [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist) more important, not less: **Don't be an amount outlier.** A 50,000 SOL withdraw from a pool whose typical note is 50 SOL points straight at the one deposit that could have funded it. Size is exactly where the anonymity set collapses if you're careless — [/blog/anonymity-sets-on-solana](/blog/anonymity-sets-on-solana) works through why. For large settlements, splitting into several rounded sub-settlements across time is often the difference between blending in and standing out alone. **Use the delay deliberately.** A deposit and a settlement clustered in the same minute correlate on timing regardless of the proof. Maturing the deposit in the pool, and spacing settlement from execution, is part of the discretion you're paying for. [/blog/the-privacy-delay-explained](/blog/the-privacy-delay-explained). **Fresh settlement addresses.** A counterparty wallet with prior attributable history re-links the trade no matter how clean the pool leg was. [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address). **Mind the post-settlement thread.** If the received block immediately moves into a recognizable strategy, the inference resumes downstream. Treat the settlement address as a clean starting point, not a hop on the way to your usual wallet. ## The limits, plainly A shielded pool removes the on-chain edge; it does not erase information your counterparty already has, and it doesn't help if size is so distinctive that elimination identifies you anyway. A nation-state-grade observer with off-chain data, or a settlement so large it has no peers in the pool, is past what any public anonymity set can do alone. [/learn/what-solmask-cannot-protect-you-from](/learn/what-solmask-cannot-protect-you-from) and [/docs/threat-model](/docs/threat-model) are the honest boundaries. For most desk-level discretion — not letting the open market read your counterparties, your timing, and your size off an explorer — settling through the pool with sensible sizing is exactly the tool. Start at [/swap](/swap). ## FAQ **Q. Doesn't going OTC already keep my trade private?** **A.** It keeps the *negotiation and execution* off-exchange, but the on-chain settlement transfer is public — counterparty, amount, and timing in the clear. Private settlement closes the step OTC alone doesn't cover. **Q. How do I settle a block too large to blend in?** **A.** Split it into rounded sub-settlements spaced over time, so no single withdraw is the obvious match for one deposit. An isolated outlier amount is where size discretion fails. **Q. Can I settle in USDC if I funded in SOL?** **A.** Yes — convert inside the withdraw so there's no separate, correlatable swap. See [/blog/swapping-sol-to-usdc-privately](/blog/swapping-sol-to-usdc-privately). **Q. Does the counterparty need SOL to receive the settlement?** **A.** No. The relayer broadcasts and pays the network fee, so the settlement address needs no gas — which also means neither side has to fund it from an attributable wallet. -------------------------------------------------------------------------------- title: "Spending from a KYC'd exchange wallet without linking every payment to you" description: "When you withdraw from Coinbase, Binance, or Kraken, the receiving wallet is tied to your verified identity. Every payment you make from it after that is linked to your real name. Here's how to spend without dragging your KYC identity along." last_updated: "2026-06-01" source: "https://solmask.xyz/en/blog/private-spending-from-a-kyc-exchange-wallet" -------------------------------------------------------------------------------- **The wallet you withdraw to from a KYC'd exchange is, for practical purposes, your legal name on a public ledger.** When you pulled SOL or USDC out of Coinbase, Binance, or Kraken, the exchange recorded which address received it — and that address is bound to your verified identity. From that point on, every payment you sign from that wallet is a line item anyone can attribute to you: who you pay, how much, how often, and how much you're holding. The exchange withdrawal itself is unavoidably public. What you can stop is letting it taint everything you do afterward. This is the practical guide to spending from exchange-funded balances without each payment pointing back to your KYC identity. For the bigger picture on what the chain exposes, see [/blog/what-the-blockchain-reveals-about-you](/blog/what-the-blockchain-reveals-about-you). ## What you can and can't hide here Be clear about the boundary. The exchange → your wallet withdrawal is logged by the exchange and visible on-chain; that link exists and isn't going anywhere. The goal isn't to erase it — it's to make sure your *spending* doesn't inherit it. You do that by putting a shielded pool between your KYC-linked wallet and the wallets you actually pay from, so the chain shows funds entering the pool from your exchange wallet and, separately, an unrelated wallet that the money later came out to. [/learn/what-is-a-shielded-pool](/learn/what-is-a-shielded-pool) is the plain-English explainer. ## The flow, step by step **1. Deposit from your exchange-linked wallet.** Open [/swap](/swap), connect the wallet that received the exchange withdrawal, choose the asset and amount, and deposit. Depositing is free — the full amount enters the pool, with only a commitment hash written on-chain ([/blog/fee-model-explained](/blog/fee-model-explained)). Your KYC wallet publicly shows a deposit into the pool, and nothing more. **2. Set a privacy delay — and don't rush it.** Pick an unlock delay (10 minutes to a week) at deposit. Because your exchange withdrawal has a known timestamp, an instant deposit-then-withdraw is easy to correlate with it. Letting time and other deposits pass widens the crowd you're hiding in. [/blog/the-privacy-delay-explained](/blog/the-privacy-delay-explained) explains the trade-off. **3. Create a fresh spending wallet.** Generate a wallet with no history — never funded by your KYC wallet, never used before. This becomes your day-to-day spending identity, deliberately disconnected from your verified one. [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address) covers how to get this right. **4. Withdraw to the fresh wallet, then spend from it.** Generate the proof in your browser and submit through the relayer, which broadcasts and pays the gas — so your fresh wallet needs no SOL and you never have to fund it from the KYC wallet ([/glossary/relayer](/glossary/relayer)). Now spend from the fresh wallet. On-chain, your payments trace back to a withdraw from the pool, not to your exchange identity. ## The mistakes that undo all of it - **Funding the fresh wallet from your KYC wallet.** Even one gas top-up re-links your spending identity to your real name. The relayer covers withdraw gas; never bridge the two yourself. - **Withdrawing the same amount you withdrew from the exchange.** If you pull exactly 3.27 SOL off Coinbase and minutes later an exactly-3.27-SOL withdraw appears, amount and timing tie them together. Use the privacy delay, and avoid mirroring distinctive amounts. - **Reusing the fresh wallet for KYC again.** If you ever deposit that wallet back to an exchange or link it to your identity, you collapse the separation retroactively. - **Off-chain identity leaks.** Spending from the fresh wallet on something publicly tied to you — an ENS-style handle, a doxxed NFT, a shipping address — defeats the on-chain work. [/learn/what-solmask-cannot-protect-you-from](/learn/what-solmask-cannot-protect-you-from) is the honest list. ## A note on compliance Breaking the link between your exchange wallet and your spending wallet is about *financial privacy*, not evasion. SolMask screens deposits against sanctioned-address lists and enforces an on-chain banlist, so the pool isn't a haven for flagged funds ([/glossary/banlist](/glossary/banlist), [/docs/threat-model](/docs/threat-model)). Privacy for ordinary users and compliance at the protocol boundary are designed to coexist. For the condensed ruleset, see [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist). ## FAQ **Q. Can I hide the fact that I withdrew from the exchange?** **A.** No — that withdrawal is logged by the exchange and visible on-chain. What you can do is prevent your later spending from being linked to it, so the exchange leg doesn't taint everything else. **Q. Doesn't depositing from a KYC wallet just expose me anyway?** **A.** Your KYC wallet only reveals that it deposited into the pool — the same thing thousands of other deposits show. It does not reveal which withdraw, to which fresh wallet, was yours. **Q. Does the fresh spending wallet need SOL to start?** **A.** No. The relayer pays the network fee on withdrawal, so the wallet can begin life with exactly the funds you withdrew and nothing traced from you. **Q. How long should I wait after the exchange withdrawal?** **A.** Long enough that the deposit isn't obviously the same money. Minutes is the floor; hours or days with other pool activity in between is far better. **Q. Is this legal?** **A.** Financial privacy is legal in most jurisdictions, and SolMask enforces sanctions screening and a banlist at the protocol level. As always, follow the tax and reporting rules that apply to you — privacy from the public is not exemption from the law. -------------------------------------------------------------------------------- title: "Withdrawing to a Ledger or hardware wallet privately" description: "Moving funds to a hardware wallet for cold storage usually re-links it to your main wallet on the first transfer. Here's how to fund a Ledger or other cold wallet so nothing on-chain ties it back to you." last_updated: "2026-06-01" source: "https://solmask.xyz/en/blog/private-withdrawals-to-a-hardware-wallet" -------------------------------------------------------------------------------- **A hardware wallet protects your keys, not your privacy.** A Ledger keeps your private key offline and safe from malware — but the moment you fund it with a normal transfer from your main wallet, the chain records an edge between the two, and your "cold" address is now permanently linked to your hot one. Anyone can read your cold-storage balance and trace exactly where it came from. The security and the privacy are different problems, and a hardware wallet only solves the first. This is the practical guide to funding a hardware wallet so the deposit address has no on-chain link to the wallet you moved funds from. If you want the background on why every transfer is a public link, [/blog/what-the-blockchain-reveals-about-you](/blog/what-the-blockchain-reveals-about-you) covers it. ## Why "just send it to cold storage" leaks The standard cold-storage move — transfer from your daily wallet straight to your Ledger address — writes both addresses, the amount, and the timestamp to a public ledger. That single transaction is what links them, and no amount of intermediate hops removes it. To break the link you need a step where your funds mix with everyone else's: a shielded pool. You deposit behind a commitment and later withdraw to your cold address with a zero-knowledge proof, so the deposit and the withdraw look unrelated on-chain. [/learn/what-is-a-shielded-pool](/learn/what-is-a-shielded-pool) explains the mechanism. ## The flow, step by step **1. Derive a fresh account on the hardware wallet.** In Ledger Live (or your wallet of choice) add a new Solana account — a new derivation path with zero history. A hardware wallet you've used before for airdrops, mints, or a prior transfer is *not* fresh; reusing it reconnects everything. The whole point is a clean destination, so generate a brand-new account for this. [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address) is the rule that matters most here. **2. Deposit from your main wallet.** Open [/swap](/swap), connect your hot wallet, pick the asset (SOL, USDC, or USDT) and amount, and deposit. Depositing is free — the full amount enters the pool and the note is generated locally in your browser, with only its commitment hash written on-chain ([/blog/fee-model-explained](/blog/fee-model-explained)). **3. Choose a privacy delay and wait past it.** Set an unlock delay at deposit (10 minutes to a week). Cold storage is patient money by definition, so this is free to you: the longer the deposit sits while others flow in, the larger your anonymity crowd. [/blog/the-privacy-delay-explained](/blog/the-privacy-delay-explained) explains why waiting well past unlock is free privacy. **4. Withdraw to the fresh hardware-wallet address.** Paste the new Ledger account's address as the recipient, generate the withdraw proof in your browser, and submit through the relayer. The relayer broadcasts and pays the network fee, so your cold wallet receives funds without ever needing SOL first — which is exactly what you want, since funding it for gas from a linked wallet would defeat the whole exercise ([/glossary/relayer](/glossary/relayer)). The result: a deposit from your hot wallet, and — later — an unrelated withdraw to a fresh cold address. Your hardware wallet now holds funds with no on-chain path back to you. ## The mistakes that undo all of it - **Reusing an existing hardware-wallet account.** If your Ledger address already appears on-chain, the withdraw links your deposit to its full history. Always derive a new account. - **Topping up the cold wallet for "gas."** A fresh wallet has no SOL, and the temptation is to send a little from your main wallet. Don't — the relayer covers the withdraw fee, and that top-up would re-link the two. - **Withdrawing immediately after depositing.** A deposit and withdraw seconds apart correlate by timing. Let real time pass. - **Consolidating later.** If you ever sweep this cold wallet back into a labeled wallet, you re-expose it. Treat it as a clean endpoint. [/learn/what-solmask-cannot-protect-you-from](/learn/what-solmask-cannot-protect-you-from) lists what's still on you. ## Verifying it arrived Because the withdraw lands on an air-gapped device you may not check often, confirm receipt on a block explorer rather than by connecting the hardware wallet to a dApp. [/learn/verifying-your-deposit](/learn/verifying-your-deposit) walks through reading the on-chain result. For the full one-page ruleset, see [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist). ## FAQ **Q. Does the hardware wallet need SOL before it can receive the withdraw?** **A.** No. The relayer pays the network fee and broadcasts on your behalf, so a brand-new account with zero balance can receive funds — which is precisely why you never have to fund it from a linked wallet. **Q. Do I sign the withdrawal with my Ledger?** **A.** No. The recipient doesn't sign anything — the proof is generated in your browser from the deposit you control, and the relayer submits it. Your Ledger only needs to *receive*, so it can stay offline. **Q. Can I reuse the same cold address for the next deposit?** **A.** It's safer not to. Each clean withdraw to the same address starts building that address a history; for maximum unlinkability, withdraw to a new account each time. **Q. Does this work for USDC and USDT, not just SOL?** **A.** Yes — deposit and withdraw any supported asset. You can also deposit one asset and have the cold wallet receive another via a swap on withdrawal. **Q. Will my hot wallet still show the deposit?** **A.** Yes — it publicly shows a deposit into the pool. What's hidden is the connection to the cold address that later received the withdraw. -------------------------------------------------------------------------------- title: "Sending SOL without revealing your main wallet" description: "A practical, step-by-step guide to moving SOL on Solana so the recipient can't trace it back to your primary wallet — what to do, what order to do it in, and the mistakes that quietly undo all of it." last_updated: "2026-05-28" source: "https://solmask.xyz/en/blog/sending-sol-without-revealing-your-main-wallet" -------------------------------------------------------------------------------- **Every SOL transfer you have ever signed is permanently readable by anyone with a block explorer.** Send 5 SOL from your main wallet to a friend, an exchange, or a new address, and the link between those two wallets is now a fact on a public ledger — amount, timestamp, both addresses, forever. If your main wallet has ever touched a centralized exchange, an NFT mint, or a Twitter-linked airdrop, that wallet is effectively your name. Anyone you pay can walk the graph backwards and see your balance, your history, and who else you transact with. This post is the practical version: how to move SOL so the receiving side carries no on-chain trail back to the wallet you started from. It is a how-to, not a theory piece. If you want the underlying reasoning, [/blog/what-the-blockchain-reveals-about-you](/blog/what-the-blockchain-reveals-about-you) covers why the chain is this transparent in the first place. ## Why a normal transfer can't be made private There is no setting on a standard SOL transfer that hides the sender or recipient. The transaction *is* the link. People sometimes try to launder the trail by hopping through several intermediate wallets — A pays B pays C pays D — but every hop is still public, so the chain of custody is fully reconstructable. Three hops doesn't break the link; it just adds three rows to the trail. Breaking the link requires a step where many people's funds become indistinguishable from each other — a shielded pool. You deposit into a shared pool behind a cryptographic commitment, and later withdraw to a destination using a zero-knowledge proof that says "I own one of the deposits in here" without revealing which one. The deposit and the withdraw are two unrelated-looking on-chain events. That's the mechanism; [/learn/what-is-a-shielded-pool](/learn/what-is-a-shielded-pool) is the plain-English explainer. ## The flow, step by step **1. Deposit from your main wallet.** Open [/swap](/swap), connect your main wallet, pick SOL and an amount, and deposit. Depositing is free — the full amount enters the pool ([/blog/fee-model-explained](/blog/fee-model-explained) covers why the fee lands on the way out, not the way in). Your wallet generates the note locally in the browser and writes only its commitment — a hash — to the chain. Nobody watching the deposit can tell what you'll eventually withdraw or where. **2. Choose a privacy delay, and actually use it.** At deposit time you set an unlock delay. The default is 10 minutes; you can choose up to a week. This is the single most underrated control, because the longer your deposit sits in the pool while other deposits flow in around it, the larger the crowd you blend into. Picking a delay and then withdrawing the instant it elapses throws away most of the benefit. [/blog/the-privacy-delay-explained](/blog/the-privacy-delay-explained) explains why patience after unlock is free anonymity. **3. Prepare a fresh recipient address.** The destination should be a wallet with zero prior on-chain history — never funded from your main wallet, never used to mint, claim, or trade. This is where most people quietly leak. If you withdraw cleanly to an address that already received SOL from your main wallet last month, you've reconnected both ends yourself. [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address) is required reading; it's the highest-leverage step here after the deposit. **4. Withdraw to the fresh address.** Generate the withdraw proof in your browser and submit it through the relayer. The relayer broadcasts the transaction and pays the network fee, so the fresh wallet doesn't need any SOL to receive funds — which matters, because funding the fresh wallet from your main wallet to cover gas would re-link them. The relayer sees the proof and the public inputs but never learns which deposit is yours ([/glossary/relayer](/glossary/relayer)). The result on-chain: a deposit from your main wallet, and — minutes or days later — an unrelated withdraw to a fresh address. No edge connects them. ## The mistakes that undo all of it The protocol gives you unlinkability; you can hand it back through operational slips. The common ones: - **Funding the fresh wallet from your main wallet.** Even a tiny "gas top-up" transfer reconnects the two. You don't need to — the relayer covers gas. - **Withdrawing the instant you unlock, right after depositing.** A deposit and a withdraw clustered in the same few minutes with no peer activity between them is trivially correlated by timing alone. Decorrelate in time. - **Reusing a "fresh" address that isn't fresh.** Any prior transaction on the destination defeats the purpose. One address, one use. - **Off-chain tells.** Withdraw to a new wallet and then immediately buy an NFT tied to your public identity, and the inference is fatal regardless of how clean the on-chain leg was. Privacy is a pipeline; the chain is one stage. [/learn/what-solmask-cannot-protect-you-from](/learn/what-solmask-cannot-protect-you-from) is the honest list of what's still on you. ## What if the recipient wants USDC, not SOL? You can deposit SOL and have the recipient receive a different asset entirely — the withdraw can route through a swap so the destination gets USDC while the link stays broken. That's a separate walkthrough: [/blog/swapping-sol-to-usdc-privately](/blog/swapping-sol-to-usdc-privately). If you'd rather see the whole picture as a checklist before you start, [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist) is the one-page version of everything above. ## FAQ **Q. Can't I just send through a few intermediate wallets instead?** **A.** No. Every hop is public, so the trail is fully reconstructable no matter how many wallets you chain. You need a step where your funds become indistinguishable from many others' — that's what the pool does and hop-chains don't. **Q. Does the recipient need SOL to receive the withdraw?** **A.** No. The relayer pays the network fee and broadcasts on your behalf, which is deliberate — it means you never have to fund the fresh wallet from a wallet linked to you. **Q. How long should I wait between deposit and withdraw?** **A.** Longer than the bare minimum. Ten minutes is the floor for casual use; for anything you care about, let hours or days pass with pool activity in between. The cost is calendar time, not effort, and the payoff is a much larger crowd to hide in. **Q. Is depositing really free?** **A.** Yes — the full amount enters the pool. The fee is charged on withdrawal, in the asset withdrawn, plus a small flat SOL amount. See [/blog/fee-model-explained](/blog/fee-model-explained) or [/docs/fees](/docs/fees). **Q. Will my main wallet still show the deposit?** **A.** Yes — your main wallet publicly shows that it deposited into the pool. What's hidden is the connection to where the funds came out. Anyone can see you used a shielded pool; nobody can see which withdraw was yours. -------------------------------------------------------------------------------- title: "Sending USDC privately on Solana" description: "How to move USDC on Solana so the recipient — and everyone watching the chain — can't tie the payment back to your main wallet. A step-by-step walkthrough, plus the slips that quietly undo it." last_updated: "2026-06-01" source: "https://solmask.xyz/en/blog/sending-usdc-privately-on-solana" -------------------------------------------------------------------------------- **A USDC transfer on Solana is just as public as a SOL transfer — the dollar amount, both addresses, and the timestamp are written to a ledger anyone can read forever.** Stablecoins feel like cash, but on-chain they behave like a bank statement stapled to your name. If the wallet you pay from has ever touched an exchange, a payroll deposit, or a public donation address, the recipient can read your USDC balance and your entire payment history the moment they receive funds from you. This is the practical guide to moving USDC so the receiving side carries no trail back to the wallet you started from. It's a how-to. If you want the reasoning behind why the chain is this exposed, [/blog/what-the-blockchain-reveals-about-you](/blog/what-the-blockchain-reveals-about-you) covers it. ## Why a direct USDC transfer can't be hidden There's no private mode on a standard SPL-token transfer. The transaction itself *is* the link between sender and recipient — sending USDC through two or three intermediate wallets first doesn't help, because every hop is public and the chain of custody stays fully reconstructable. To actually break the link you need a step where your funds become indistinguishable from many other people's: a shielded pool. You deposit USDC behind a cryptographic commitment, then later withdraw using a zero-knowledge proof that says "I own one of the deposits in here" without revealing which one. [/learn/what-is-a-shielded-pool](/learn/what-is-a-shielded-pool) is the plain-English version. SolMask runs a dedicated USDC pool, so you deposit USDC and the recipient receives USDC — no swap, no asset change, same dollar amount minus the withdraw fee. ## The flow, step by step **1. Deposit USDC from your main wallet.** Open [/swap](/swap), connect your wallet, choose USDC and an amount, and deposit. Depositing is free — the full balance enters the pool, and the fee is charged later on withdrawal ([/blog/fee-model-explained](/blog/fee-model-explained)). Your browser generates the note locally and writes only its commitment — a hash — on-chain. Nobody watching can tell how much you'll withdraw or where. **2. Set a privacy delay and let it breathe.** At deposit you pick an unlock delay (10 minutes minimum, up to a week). The longer your USDC sits while other deposits flow in around it, the larger the crowd you blend into. Withdrawing the second it unlocks throws most of that away — see [/blog/the-privacy-delay-explained](/blog/the-privacy-delay-explained). **3. Get a fresh recipient address.** The destination should have zero prior history: never funded by your main wallet, never used to trade or claim. This is where most people leak — withdraw cleanly to an address that already received a transfer from you last month and you've reconnected both ends yourself. [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address) is the highest-leverage step after the deposit. **4. Withdraw USDC to the fresh address.** Generate the proof in your browser and submit it through the relayer. The relayer broadcasts the transaction and pays the SOL network fee, so the fresh wallet needs no SOL to receive the USDC — which matters, because topping it up from your main wallet for gas would re-link them. The relayer sees the proof but never learns which deposit is yours ([/glossary/relayer](/glossary/relayer)). On-chain the result is a USDC deposit from your main wallet and, minutes or days later, an unrelated USDC withdraw to a fresh address. No edge connects them. ## The mistakes that undo all of it - **Funding the fresh wallet from your main wallet.** A fresh USDC wallet often has no SOL for rent or future fees — but a "gas top-up" from a linked wallet reconnects them. The relayer covers the withdraw gas; fund the wallet later from another unlinked source if needed. - **Withdrawing instantly after depositing.** A deposit and a same-size withdraw clustered in a few minutes is correlatable by timing alone. Decorrelate in time. - **Reusing a "fresh" address.** One address, one use. Any prior activity on it defeats the purpose. - **Round numbers and off-chain tells.** Withdrawing the exact unusual amount you deposited, or paying a fresh wallet and then using it for something tied to your identity, leaks through inference. [/learn/what-solmask-cannot-protect-you-from](/learn/what-solmask-cannot-protect-you-from) is the honest list of what's still on you. ## What if you hold SOL, not USDC? You can deposit SOL and have the recipient receive USDC instead — the withdraw routes through a swap so the destination gets the stablecoin while the link stays broken. That's a separate walkthrough: [/blog/swapping-sol-to-usdc-privately](/blog/swapping-sol-to-usdc-privately). For the one-page version of every rule above, see [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist). ## FAQ **Q. Does the recipient get the exact USDC amount I deposit?** **A.** Almost — they receive the deposited amount minus the withdraw fee (a small percentage in USDC plus a flat SOL component covered from the withdrawal). Depositing itself is free. See [/docs/fees](/docs/fees). **Q. Can I send USDC to someone who has no SOL at all?** **A.** Yes. The relayer pays the network fee and broadcasts for you, so a brand-new wallet with zero SOL can still receive the USDC. **Q. Is sending USDC through several wallets first just as good?** **A.** No. Every hop is public, so the full path stays traceable. Only a shielded pool makes your funds indistinguishable from everyone else's. **Q. Will my main wallet still show that I deposited USDC?** **A.** Yes — your wallet publicly shows a deposit into the pool. What's hidden is the link to where the USDC came out. Anyone can see you used the pool; nobody can see which withdraw was yours. **Q. How long should I wait between deposit and withdraw?** **A.** Longer than the minimum. Ten minutes is the floor; for anything you care about, let hours or days pass with other pool activity in between. The cost is calendar time, not effort. -------------------------------------------------------------------------------- title: "The Solana wallet privacy checklist" description: "A practical, ordered checklist for keeping a Solana transfer private — from depositing and choosing a delay to picking a clean recipient and avoiding the off-chain tells that quietly undo everything." last_updated: "2026-05-29" source: "https://solmask.xyz/en/blog/solana-wallet-privacy-checklist" -------------------------------------------------------------------------------- **Privacy on Solana is less about the cryptography and more about the order you do things in.** The proof system does its job automatically; the failures are almost always operational — a fresh wallet that wasn't fresh, a withdraw fired thirty seconds after a deposit, a "gas top-up" that quietly reconnected both ends. This is the one-page checklist. Each item links to the deeper write-up if you want the reasoning, but you can follow the list top to bottom and be in good shape. If you're new to *why* any of this is necessary, start with [/blog/what-the-blockchain-reveals-about-you](/blog/what-the-blockchain-reveals-about-you) and come back. ## Before you deposit - **Decide what "private" you actually need.** Hiding an amount and hiding a relationship are different problems. If you need the connection between sender and recipient gone, you need a shielded pool, not amount-encryption — [/blog/encrypted-amounts-are-not-private](/blog/encrypted-amounts-are-not-private) explains the distinction. [/learn/what-is-a-shielded-pool](/learn/what-is-a-shielded-pool) is the primer. - **Know what it won't fix.** Read [/learn/what-solmask-cannot-protect-you-from](/learn/what-solmask-cannot-protect-you-from) once. Network-level observation, recipient-side history, and adversaries with outside information are still on you. - **Choose an amount that resembles the crowd.** An outlier amount re-identifies you even with a perfect proof. Round figures close to what others deposit are best — [/blog/anonymity-sets-on-solana](/blog/anonymity-sets-on-solana) shows why amount bands matter as much as pool size. ## When you deposit - **Deposit from your main wallet — that part is fine.** Your wallet publicly shows it deposited into the pool; what stays hidden is the connection to the withdraw. Depositing is free; the full amount enters the pool ([/blog/fee-model-explained](/blog/fee-model-explained), [/docs/fees](/docs/fees)). - **Set a real privacy delay, not the bare minimum.** The default 10 minutes is a floor, not a recommendation. The longer your deposit matures in the pool while others arrive, the larger the crowd you hide in. [/blog/the-privacy-delay-explained](/blog/the-privacy-delay-explained) and [/learn/why-privacy-delay](/learn/why-privacy-delay). - **Confirm the deposit landed** before relying on it: [/learn/verifying-your-deposit](/learn/verifying-your-deposit). ## Between deposit and withdraw - **Wait. Then wait a bit more.** Don't withdraw the instant your delay elapses. A deposit and a withdraw clustered in time, with no peer activity between them, correlate on timing alone — no special access required. Patience after unlock is free anonymity. - **Let pool activity accumulate.** The peers who deposit after you only help if you're still in the pool when they arrive. The cost of waiting is calendar time, not effort. ## When you withdraw - **Use a genuinely fresh recipient address.** Zero prior history: never funded from your main wallet, never used to mint, claim, trade, or receive. This is the single most common deanonymization mistake. [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address) is the most important link on this page. - **Don't fund the fresh wallet for gas.** You don't need to — the relayer broadcasts and pays the network fee, so the destination needs no SOL to receive ([/glossary/relayer](/glossary/relayer)). Topping it up from your main wallet would reconnect both ends. - **If the recipient wants a different asset, convert inside the withdraw,** not in a separate swap afterward — [/blog/swapping-sol-to-usdc-privately](/blog/swapping-sol-to-usdc-privately). - **Keep withdraw amounts unremarkable.** Round, crowd-like figures. Split a large withdraw into rounded sub-withdraws across time if needed. ## After the withdraw - **Treat the fresh wallet as single-purpose.** The moment it interacts with anything tied to your public identity — an NFT buy, a known DEX position, an exchange deposit under your name — the off-chain inference can re-link it. Privacy is a pipeline; the chain is one stage. - **Don't consolidate cleaned funds back into your main wallet.** Sending the withdrawn funds onward to a wallet that's already linked to you defeats the whole exercise. - **Mind the network layer.** If your threat model includes someone watching your IP, use different network paths for deposit and withdraw. The relayer helps, but it's one entity ([/docs/threat-model](/docs/threat-model)). ## Use-case shortcuts Depending on what you're doing, there's a tailored walkthrough: - Moving SOL to a new wallet of your own → [/blog/sending-sol-without-revealing-your-main-wallet](/blog/sending-sol-without-revealing-your-main-wallet) - Receiving privately to a fresh wallet → [/blog/receiving-sol-privately-to-a-fresh-wallet](/blog/receiving-sol-privately-to-a-fresh-wallet) - Paying contractors or running payroll → [/blog/private-payroll-on-solana](/blog/private-payroll-on-solana) - Treasury and vendor payments → [/blog/dao-treasury-privacy-on-solana](/blog/dao-treasury-privacy-on-solana) - Accepting donations → [/blog/accepting-crypto-donations-privately](/blog/accepting-crypto-donations-privately) - OTC settlement → [/blog/private-otc-trades-on-solana](/blog/private-otc-trades-on-solana) When you're ready, start at [/swap](/swap). If you'd rather learn hands-on first, the [/tutorial](/tutorial) walks the full flow with live demos. ## FAQ **Q. What's the single most important item on this list?** **A.** Use a fresh recipient address, and never fund it from a wallet linked to you. Most real-world deanonymizations are recipient-side history, not broken cryptography. **Q. Is the 10-minute default delay enough?** **A.** For casual use, maybe. For anything you genuinely care about, choose hours or days and let your deposit mature with pool activity around it. The marginal cost is patience; the marginal gain is a bigger crowd. **Q. Do I have to hide that I deposited?** **A.** No — the deposit is visibly from your wallet, and that's fine. The protection is that nobody can connect your deposit to the withdraw. Using a shielded pool is not itself the secret. **Q. Can I reuse one fresh wallet for several withdraws?** **A.** Better not to. Each use adds history that links future activity together. One destination, one purpose. -------------------------------------------------------------------------------- title: "Swapping SOL to USDC privately: cross-asset withdrawals explained" description: "You can deposit one asset and have the recipient receive another — SOL in, USDC out — with the on-chain link between the two still broken. How private cross-asset withdrawals work and when to use them." last_updated: "2026-05-29" source: "https://solmask.xyz/en/blog/swapping-sol-to-usdc-privately" -------------------------------------------------------------------------------- **Privacy and asset conversion are usually treated as two separate steps — and stitching them together is exactly where most people leak.** The naive flow is: move funds privately, then swap them on a DEX. But the swap is a public transaction from the destination wallet, and if you swap the whole withdrawn amount in one go right after receiving it, you've handed an observer a bright correlation signal. The cleaner approach is to do the conversion *inside* the private withdrawal, so the recipient simply receives the asset they wanted and there's no separate swap to correlate. This post covers cross-asset withdrawals: deposit SOL, recipient gets USDC, link still broken. ## Why "withdraw then swap" leaks Suppose you withdraw 50 SOL privately to a fresh wallet, then immediately swap all 50 SOL to USDC on a DEX. On-chain there's now a fresh wallet that received exactly 50 SOL and, seconds later, swapped exactly 50 SOL. The amounts match, the timing is tight, and the swap is fully public. An observer watching pool withdraws of ~50 SOL and DEX swaps of ~50 SOL can line them up. You didn't break privacy at the pool — you re-introduced a correlatable event right after it. Two things make this worse: the swap exposes a precise amount (defeating the "blend into round amounts" hygiene from [/blog/anonymity-sets-on-solana](/blog/anonymity-sets-on-solana)), and the fresh wallet now has a transaction history, so it's no longer fresh for any future use. ## How a cross-asset withdrawal works instead SolMask can route a swap *as part of the withdraw transaction itself*. You deposit SOL into the SOL pool. When you withdraw, the proof releases your SOL inside the same transaction that swaps it through Jupiter and delivers USDC to the recipient. From the chain's perspective there is one private withdraw that results in the recipient holding USDC — not a withdraw followed by a separate, linkable swap. The privacy property is unchanged by the conversion. The zero-knowledge proof still only asserts "I own one of the deposits in this pool"; it reveals nothing about which deposit, and the swap leg rides along inside the same atomic transaction. The deposit (SOL from your wallet) and the outcome (USDC to a fresh wallet) remain two unrelated-looking events. [/learn/what-is-a-shielded-pool](/learn/what-is-a-shielded-pool) covers the base mechanism; the swap is an add-on to the withdraw, not a separate hop. ## When cross-asset withdrawals are the right tool - **The recipient wants a stablecoin.** Paying a contractor or vendor who invoices in USDC, but you hold SOL. Deposit SOL, deliver USDC, no public conversion in between. This pairs naturally with [/blog/dao-treasury-privacy-on-solana](/blog/dao-treasury-privacy-on-solana) and [/blog/private-payroll-on-solana](/blog/private-payroll-on-solana). - **You want the destination asset to differ from the source for its own sake.** Changing the asset across the private boundary removes the "same amount of the same token went in and came out" pattern entirely. - **Settlement in a specific currency.** OTC and treasury flows that have to settle in USDC rather than SOL — see [/blog/private-otc-trades-on-solana](/blog/private-otc-trades-on-solana). ## What to keep in mind **Slippage and routing are public on the output, not the input.** The swap executes at market through Jupiter, so the recipient receives whatever the route returns at execution time. The conversion rate is a normal market rate; what's hidden is the connection to your deposit, not the existence of a swap leg. Choose amounts and timing sensibly so the delivered figure still resembles ordinary activity. **Amount hygiene still applies.** A cross-asset withdrawal changes the asset but doesn't excuse an outlier amount. If you deposit a very distinctive quantity of SOL, the conversion doesn't erase that distinctiveness. Match the crowd on the deposit side and keep delivered amounts unremarkable. **The delay still matters.** Converting on exit doesn't replace the privacy delay — let your deposit mature in the pool before withdrawing, exactly as with a same-asset withdraw. [/blog/the-privacy-delay-explained](/blog/the-privacy-delay-explained) explains why. **The recipient address still has to be fresh.** Delivering USDC to a wallet with prior history linked to you reconnects both ends regardless of the asset switch. [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address) is unchanged advice here. ## The short version A cross-asset withdrawal folds the conversion into the private step so there's no separate, correlatable swap afterward. Deposit SOL, recipient receives USDC, the link stays broken. Everything else — delay, amount hygiene, fresh recipient — is the same discipline as a same-asset withdraw, summarized in [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist). Try it from [/swap](/swap). ## FAQ **Q. Does the swap reveal which deposit was mine?** **A.** No. The proof asserts ownership of one unspecified deposit in the pool; the swap leg executes inside the same transaction without referencing your specific note. The conversion doesn't weaken the unlinkability. **Q. Can I deposit USDC and have the recipient get SOL?** **A.** Cross-asset withdrawals work across supported assets, so the direction can be reversed depending on pool support. The principle is identical: convert inside the withdraw, not in a separate transaction after it. **Q. Is it cheaper to withdraw same-asset and swap myself?** **A.** It might look marginally cheaper in raw fees, but doing the swap yourself afterward creates a public, correlatable transaction from the fresh wallet — which is the privacy cost you were trying to avoid. The in-withdraw swap exists precisely to avoid that. **Q. What rate do I get on the conversion?** **A.** A normal market rate through Jupiter's routing at execution time, including ordinary slippage. Privacy hides the link, not the fact that a market swap occurred. **Q. Does converting on exit replace the privacy delay?** **A.** No. Use a real delay and let the deposit mature in the pool before withdrawing — the conversion is independent of the timing protections. -------------------------------------------------------------------------------- title: "What the blockchain reveals about you" description: "Solana isn't anonymous — it's pseudonymous, which is weaker than it sounds. A plain walkthrough of exactly what anyone can read from your wallet, how one leaked link unravels the rest, and what actually closes the gap." last_updated: "2026-05-27" source: "https://solmask.xyz/en/blog/what-the-blockchain-reveals-about-you" -------------------------------------------------------------------------------- **"Anonymous" and "pseudonymous" sound similar and mean very different things — and Solana is the second one.** Your wallet isn't labeled with your name, so it feels private. But every transaction it has ever made is public, permanent, and linkable, and it takes exactly one connection between your wallet and your identity for all of it to become *yours* retroactively. That one connection happens constantly: an exchange withdrawal, an NFT mint tied to your Twitter, a payment to someone who knows you, a wallet address pasted in a public profile. After that, the explorer is reading your financial life out loud. This is the orientation piece. If you already know the chain is transparent and just want to do something about it, skip to [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist). ## What anyone can read right now Open any block explorer, paste a wallet address, and without permission or special access you can see: - **The full balance.** Every token, exact amount, right now. - **Every transaction, forever.** Each transfer in and out — counterparty address, amount, timestamp — back to the wallet's first action. Nothing expires. - **The whole graph.** Who this wallet pays, who pays it, how often, and — by following those addresses — who *they* transact with. Relationships are first-class data. This is the default for every wallet, including yours. It isn't a breach or a misconfiguration; it's how a public ledger works. The information is the feature. ## Pseudonymous means "anonymous until one slip" The comforting story is "it's just a random string of characters, not my name." The problem is that random strings get attached to names with almost no effort: - **You bought crypto on a KYC exchange and withdrew to your wallet.** The exchange knows the address is yours; that withdrawal is on-chain; the link is now discoverable by anyone who can correlate it. - **You minted an NFT, claimed an airdrop, or signed up for something** with a wallet you also use publicly. - **You posted an address** — a donation link, a tip jar, an ENS-style handle, a profile field. - **You paid someone who knows you.** They now know one of your addresses, and from it, everything that address has ever done. Any single one of these de-pseudonymizes the wallet. And because the ledger is permanent and fully linkable, the identification is *retroactive*: the moment one address is tied to you, its entire history — and everything connected to it through the graph — becomes attributable. There's no "going forward only." It's all of it, all the way back. ## How one link spreads The reason a single slip is so costly is that wallets don't stay isolated. You move funds between your own wallets, you consolidate, you pay gas from one to fund another. Each of those transfers is a public edge connecting the addresses. So when one wallet gets tied to your identity, the graph hands an observer the rest: your other wallets, your balances across them, your counterparties, your patterns. Chain-analysis firms do exactly this at scale, professionally. You don't have to be a target — you have to be on the graph, and everyone is. This is why "I'll just use a fresh wallet" isn't, by itself, a privacy strategy. The instant you fund that fresh wallet from an existing one, you've drawn an edge connecting them, and the fresh wallet inherits the old one's exposure. [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address) covers what "fresh" actually has to mean. ## What does *not* fix it A few common ideas that feel like privacy but aren't: - **Hopping through intermediate wallets.** Every hop is public, so the trail is fully reconstructable. Three wallets in a chain is three public edges, not a broken link. - **Hiding only the amount.** Schemes that encrypt the transfer amount still leave the sender, recipient, and timing public — the *relationship* survives, which is usually the sensitive part. [/blog/encrypted-amounts-are-not-private](/blog/encrypted-amounts-are-not-private) goes deeper. - **A brand-new wallet you funded from your old one.** As above — the funding transfer is the link. ## What actually closes the gap To break the connection you need a step where your funds become genuinely indistinguishable from many other people's — a shielded pool. You deposit behind a cryptographic commitment, and later withdraw using a zero-knowledge proof that says "I own one of the deposits in this pool" without revealing which ([/learn/how-zk-proofs-work](/learn/how-zk-proofs-work)). The deposit and the withdraw are two unrelated-looking transactions with no public edge between them, so following the graph hits a dead end. [/learn/what-is-a-shielded-pool](/learn/what-is-a-shielded-pool) is the full primer; [/blog/anonymity-sets-on-solana](/blog/anonymity-sets-on-solana) explains why the *crowd* you blend into is what protects you. That's the structural fix. The rest is operational discipline — a real privacy delay, fresh recipient addresses, sensible amounts — and the honest list of what even this can't do is at [/learn/what-solmask-cannot-protect-you-from](/learn/what-solmask-cannot-protect-you-from). When you're ready to act on it, the practical next steps are [/blog/sending-sol-without-revealing-your-main-wallet](/blog/sending-sol-without-revealing-your-main-wallet) and the [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist). Or learn hands-on at the [/tutorial](/tutorial). ## FAQ **Q. Isn't my wallet anonymous since it's not labeled with my name?** **A.** It's pseudonymous, not anonymous. The label is missing, but a single link between the address and your identity — an exchange withdrawal, a posted address, a payment to someone who knows you — attaches your name to the entire history retroactively. **Q. Can people really see my balance and every transaction?** **A.** Yes, anyone, right now, with a free block explorer and your address. Balance, full transaction history, and the graph of who you transact with are all public by default. **Q. If I never use an exchange, am I anonymous?** **A.** Harder to identify, but not safe by default. Posting an address, paying someone who knows you, or linking wallets through funding transfers all create the same identifying link. Avoiding exchanges removes one path, not all of them. **Q. Doesn't using a new wallet make me private?** **A.** Only if it has no link back to you — and funding it from an existing wallet creates exactly that link. A genuinely fresh wallet plus a step that breaks the funding link (a shielded pool) is what's needed. See [/learn/choosing-a-recipient-address](/learn/choosing-a-recipient-address). **Q. What's the single thing that actually helps?** **A.** Breaking the on-chain edge between where your funds came from and where they went, using a shielded pool — then not re-creating that edge through sloppy operational habits. Start with the [/blog/solana-wallet-privacy-checklist](/blog/solana-wallet-privacy-checklist).