Trust model
Who holds which key in Voidcash, what each key and each service can do, and what stops them.
Only your keys can spend your notes or read them. Beyond your keys, Voidcash depends on mathematics, on the pool program and whoever can change it, on the services that carry your transactions and data, on Meteora for the buyback, and on your browser. This page lists each party, what it can do, and what it cannot.
The pool program
The pool program (HjQyQZ3xewjsBb3w8JFGJSh2ztauV4f6X4o5CrZTobZm) is upgradeable. One key, the dev's, is both
the program's upgrade authority and the admin in its Config account. At initialization the same key is also set
as the guardian. There is no multisig and no timelock.
The upgrade authority can replace the program's code at once. New code can change every rule on this page: the fee caps, the verifying key, the pause rules and where the vault's SOL goes. Users trust whoever holds this key. The reason for the choice: a bug can be fixed in place, with no migration of notes.
Without an upgrade, the program holds to these rules:
- Nobody can spend a note without a proof. Every SOL movement out of the vault for a user goes through
transact, which verifies a Groth16 proof against a root from the program's own history. The admin has no instruction that touches the vault. - Fees are capped. The admin can set the shield fee and the exit fee up to the program's fee cap, 1% (100 basis points) each. A higher fee takes an upgrade.
- A fee change cannot apply to a transaction already built. The rate the app showed is part of the proof's
ext-data hash and must equal the pool's current rate, or the program refuses it (
FeeChanged).
The admin
| The admin can | The admin cannot, without an upgrade |
|---|---|
Set the parameters with set_params: the minimum deposit (above zero), the shield fee and the exit fee (each up to 1%), and the buyback's share of every fee (up to 100%) | Spend or move a note; every spend needs a proof |
Set the guardian with set_guardian | Take SOL from the vault |
Propose a new admin with propose_admin; the new key takes over only when it signs accept_admin | Raise a fee above 1% |
| Pause any of the four paths, and unpause them; only the admin can unpause | Change a pinned buyback pool or the VOID mint once a pool is pinned |
Withdraw the treasury's SOL to any account with withdraw_treasury, keeping its rent | Take SOL from the buyback account; its SOL leaves only into a buy |
Set the VOID mint with set_void_mint (an initialized SPL Token mint), until a pool is pinned | |
Pin the VOID/SOL pool the buyback uses with init_buyback (owned by Meteora's cp-amm, 1,112 bytes, VOID against wSOL) |
The treasury holds the treasury's share of the fees, and nothing else. The vault's SOL, which backs the notes, is a
separate account that only transact debits.
The guardian
The guardian can only pause. pause sets one or more of four flags, and the guardian or the admin may call it. Only
the admin can clear a flag with unpause.
| Flag | Value | What stops |
|---|---|---|
PAUSE_DEPOSITS | 1 | Shields |
PAUSE_TRANSFERS | 2 | Private transfers (and note merges) |
PAUSE_EXITS | 4 | Unshields, and every relayer fee, so every path that takes SOL out of the vault for a user |
PAUSE_BUYBACK | 8 | The buyback |
A pause moves no SOL and changes no note. While exits are paused, notes stay in the pool and nobody can take SOL out, their owners included.
Exits
Exits are not rate-limited. Nothing in the program caps how much SOL leaves the vault in one transaction or in a window of time, and the pool's size has no cap. In their place:
- Every
transactthat moves SOL out of the vault emitsExited, with the amount and the vault's balance after it. - The indexer alerts on any exit above 2% of the vault, on exits above 5% of it within 9,000 slots (about an hour), on any change of the pause flags or the parameters, and on any program upgrade.
- Each SOL path, deposits, transfers, exits and the buyback, has its own pause flag.
The circuit and the trusted setup
Every spend is a Groth16 proof for the Transaction(26, 2, 2) circuit. The program checks it against seven public
inputs it builds itself: a root from its own history, the public amount, the ext-data hash it computes on-chain, two
nullifiers and two output commitments. Soundness rests on the circuit being correct, on groth16-solana and the
Solana syscalls it uses, and on the trusted setup.
Whoever knows a Groth16 setup's secret can forge proofs and take the vault's SOL. The keys come from one contribution on the public Hermez phase 1, closed with a Solana block hash, with a transcript anyone can check. See Trusted setup. The upgrade authority can also replace the verifying key with an upgrade.
The relayer
Private transfers and unshields reach the chain through a relayer, which pays the transaction fee so the user's wallet never appears. The relayer is paid from the notes, in SOL.
- It cannot change the recipient, the amount, its own fee, the fee rate, the deadline or the ciphertexts: all of them, and the relayer's own account, are in the ext-data hash the proof binds. A changed field fails the proof.
- It cannot hold a transaction: each one carries a deadline slot, after which the program refuses it. The app sets it 150 slots ahead, about a minute.
- It can refuse or delay a request until its deadline. The relayer is run by the Voidcash team. A refused transaction moves nothing, and its notes stay spendable, so it can be proven again later with a fresh deadline.
- It sees each request: the proof, the nullifiers, the commitments and the ext data, which is what the chain shows once the transaction lands, plus the connection it came from and its timing. Its HTTP server keeps no request logs: no IP addresses and no bodies. It logs the signature of each transaction it sends.
The relayer never signs a transaction the user built. It rebuilds each one from the request's fields, adds only a compute budget, and simulates it before sending, so the request cannot add an instruction that spends the relayer's SOL.
The indexer
The indexer is a convenience. It serves every leaf with its ciphertext and every nullifier, the same to everyone, so it never learns which notes are whose. It cannot forge chain data:
- The tree is checked against the chain. Before proving, the app rebuilds the tree from the indexer's leaves and looks for its root in the root history the program keeps in its tree account. A dropped or invented leaf gives a root the program does not hold, and the app proves nothing against it.
- Notes are checked against their commitments. The app accepts a decrypted note only if it recomputes the commitment stored in the tree.
- The indexer checks itself. When it has every leaf the program has, its root must be in the program's history; if it is not, it alerts and answers 503 instead of serving leaves.
An indexer that hides a nullifier makes a spent note look spendable. The spend then fails: the note's nullifier
account already exists, so the relayer's simulation refuses it. An indexer that drops a ciphertext hides a note from
its owner until the data is read from another source. In neither case can it move funds. Everything it serves can be
rebuilt from the chain: commitments are in the program's Inserted events, and ciphertexts and nullifiers in the
instruction data.
Your browser
The app runs in your browser, and your keys stay there.
- The vault is a random 32-byte root generated in the browser. The spending key and the viewing key derive from it. The root is sealed with your passphrase (PBKDF2-SHA256, 600,000 rounds, then AES-256-GCM) and stored in this browser's local storage. The recovery key, shown once, is the root itself. Nothing comes from your wallet, so a wallet signature obtained by phishing cannot spend.
- Proofs are built on your device, in a Web Worker. The worker loads the circuit's files and refuses them unless their SHA-256 matches the hash pinned in the build.
- The Content-Security-Policy the app sends with every page runs only this origin's scripts that carry the
request's nonce, and limits
connect-srcto the app itself, the RPC, the indexer and the relayer. It also setsobject-src 'none',base-uri 'none',form-action 'none'andframe-ancestors 'none', withreferrer-policy: no-referrer. Injected code cannot send your keys to an arbitrary server.
The policy does not protect you from the app's own code. A malicious build served on the app's domain, or a fake site, could read your keys once you unlock the vault there.
The recovery key
Anyone who holds your recovery key can spend your notes. Nobody can restore it for you.
Meteora
The buyback buys VOID on a Meteora DAMM v2 (cp-amm) pool, pinned by the admin. Meteora controls that pool's program:
| Meteora can | How the buyback responds |
|---|---|
| Change a DAMM v2 pool's base fee, up to 10%, and its dynamic fee | Every buy's minimum output is what the reference price gives, less the program's 2% loss limit, and a higher fee eats into that margin. In tests against Meteora's mainnet program, a 1% pool fee still buys and a 3% fee buys nothing |
Disable the pool (set_pool_status) | The program reads the pool's status and refuses to buy while it is disabled (PoolDisabled) |
| Upgrade the DBC and DAMM v2 programs (one upgrade authority for both) | The program checks, after every swap, that the whole chunk was spent and that the price is still within the band, and burns what it received. Each swap puts in one chunk of the buyback's own SOL, 1 SOL or less, at most once per buy interval |
The vault, the notes and the treasury never interact with Meteora. Shields, private transfers and unshields work whatever happens to the pool. See The buyback.
Summary
| Party | Can | Cannot |
|---|---|---|
| Upgrade authority (the dev's key) | Replace the program, and so every rule, at once | Act without a public upgrade, which the indexer alerts on |
| Admin (the same key) | Set fees up to 1%, the minimum deposit and the buyback share; set the guardian; hand over the admin role; pause and unpause; withdraw the treasury; set the VOID mint and pin the buyback pool | Spend a note, take SOL from the vault, raise a fee above 1%, or change a pinned pool |
| Guardian | Pause deposits, transfers, exits or the buyback | Unpause, move SOL, or change anything else |
| Circuit, verifier and trusted setup | Everything, if unsound | Nothing extra, if sound |
| Holder of your recovery key | Spend your notes and read them | — |
| Relayer | Refuse or delay a request until its deadline; see requests and their timing | Change the recipient, the amount or its fee; spend from your notes; hold a transaction past its deadline |
| Indexer | Withhold data | Learn which notes are yours; alter the tree without the app seeing a root mismatch |
| The app's own code | Read your keys once you unlock the vault | — |
| Meteora | Raise the pool's fee, disable the pool, upgrade its programs | Touch the vault or the notes |