voidcashdocs

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 canThe 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_guardianTake SOL from the vault
Propose a new admin with propose_admin; the new key takes over only when it signs accept_adminRaise a fee above 1%
Pause any of the four paths, and unpause them; only the admin can unpauseChange 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 rentTake 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.

FlagValueWhat stops
PAUSE_DEPOSITS1Shields
PAUSE_TRANSFERS2Private transfers (and note merges)
PAUSE_EXITS4Unshields, and every relayer fee, so every path that takes SOL out of the vault for a user
PAUSE_BUYBACK8The 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 transact that moves SOL out of the vault emits Exited, 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-src to the app itself, the RPC, the indexer and the relayer. It also sets object-src 'none', base-uri 'none', form-action 'none' and frame-ancestors 'none', with referrer-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 canHow the buyback responds
Change a DAMM v2 pool's base fee, up to 10%, and its dynamic feeEvery 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

PartyCanCannot
Upgrade authority (the dev's key)Replace the program, and so every rule, at onceAct 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 poolSpend a note, take SOL from the vault, raise a fee above 1%, or change a pinned pool
GuardianPause deposits, transfers, exits or the buybackUnpause, move SOL, or change anything else
Circuit, verifier and trusted setupEverything, if unsoundNothing extra, if sound
Holder of your recovery keySpend your notes and read them—
RelayerRefuse or delay a request until its deadline; see requests and their timingChange the recipient, the amount or its fee; spend from your notes; hold a transaction past its deadline
IndexerWithhold dataLearn which notes are yours; alter the tree without the app seeing a root mismatch
The app's own codeRead your keys once you unlock the vault—
MeteoraRaise the pool's fee, disable the pool, upgrade its programsTouch the vault or the notes

On this page