The pool program
The Anchor program that holds the pool's SOL - its accounts and seeds, every instruction and who may call it, fees, pause flags, events, limits and upgrades.
The pool is one Anchor program (Anchor 1.2.1). It holds the SOL behind every note, verifies a
Groth16 proof for every transaction, and runs the buyback of $VOID. Its declared program ID is
HjQyQZ3xewjsBb3w8JFGJSh2ztauV4f6X4o5CrZTobZm.
Accounts
Every account is a PDA of the program, with a fixed seed: anyone can derive each address from the seeds below and the program ID.
| Account | Seeds | Kind | Holds |
|---|---|---|---|
| Config | ["config"] | Config, 280 bytes | Admin, pending admin, guardian, $VOID mint, pause flags, parameters, 128 reserved bytes |
| Tree | ["tree"] | Raw account owned by the program, 9,400 bytes | The note tree and its root history. See The Merkle tree |
| Vault | ["vault"] | Vault, 8 bytes, owned by the program | The SOL behind every note, as its lamport balance |
| Treasury | ["treasury"] | System account | The treasury's share of the fees |
| Buyback | ["buyback"] | System account | The buyback's share of the fees, until it is spent on $VOID |
| Nullifier | ["nf", nullifier] | Spent, 8 bytes | Nothing: its existence marks the nullifier spent. The seed is the nullifier's 32 big-endian bytes |
| Buyback state | ["bbstate"] | BuybackState, 450 bytes | The pinned pool, the buyback's token accounts, its price observations, the $VOID burned so far. See Buyback and burn |
Notes hold native lamports, so the pool itself has no token accounts. The treasury and the buyback are funded with
their rent-exempt minimum at initialize, so small fee credits to them are accepted.
Config
| Field | Type | Meaning |
|---|---|---|
bump | u8 | |
admin | Pubkey | Sets parameters, unpauses, moves treasury SOL, sets the $VOID mint, pins the buyback pool |
pending_admin | Pubkey | Set by propose_admin, takes effect when it signs accept_admin |
guardian | Pubkey | Can only pause |
void_mint | Pubkey | The mint the buyback buys and burns |
paused | u8 | Pause flags (below) |
params | Params | min_deposit (u64, lamports), shield_fee_bps, exit_fee_bps, buyback_share_bps (u16 each) |
reserved | 128 bytes | Zero, so a later upgrade can add parameters without migrating the account |
The launch parameters are a minimum deposit of 0.01 SOL (10,000,000 lamports), no shield fee, a 0.5% exit fee (50 bps) and a buyback share of 5,000 bps (half of every fee). The values in force are always the config account's: the app reads them on every sync.
Instructions
| Instruction | Who may call | What it does |
|---|---|---|
initialize(admin, guardian, void_mint, params) | The program's upgrade authority, once | Checks the parameters, creates the config, tree and vault, funds the treasury and buyback rent. The program checks the upgrade authority against its own ProgramData, so nobody can front-run it |
transact(proof, root_index, nullifiers, commitments, ext) | Anyone; the payer signs | Every shield, private transfer, merge and unshield (below) |
init_buyback | Admin, once | Pins the $VOID/SOL Meteora DAMM v2 pool and the buyback's token accounts |
observe | Anyone | Records the pinned pool's price, at most once per interval. Pays nothing |
buyback | Anyone | Buys $VOID with one chunk of the buyback's SOL and burns it. Pays the caller nothing |
set_void_mint | Admin, until init_buyback | Sets the $VOID mint: an initialized SPL Token mint only |
set_params(params) | Admin | Replaces the parameters, within the limits below |
set_guardian(guardian) | Admin | Replaces the guardian |
propose_admin(admin) | Admin | Names a pending admin |
accept_admin | The pending admin | Becomes the admin; clears the pending admin |
pause(flags) | Guardian or admin | Sets pause flags. flags must be non-zero and only known flags |
unpause(flags) | Admin | Clears pause flags |
withdraw_treasury(amount) | Admin | Moves treasury SOL to any destination, keeping the account's rent |
At initialization one key is the upgrade authority, the admin and the guardian.
transact
Accounts, in order: payer (signer, writable), config, tree, vault, treasury, buyback, nullifier 0, nullifier 1 (both created here), recipient, relayer, system program.
Data, after the 8-byte discriminator: the proof (256 bytes), root_index (u8), two nullifiers and two
commitments (32 bytes each, big-endian), then ExtData:
ExtData field | Type | Meaning |
|---|---|---|
ext_amount | i64 | Lamports entering the pool (positive) or leaving it (negative), before the exit fee. 0 for a private transfer |
relayer_fee | u64 | Lamports paid to the relayer from the notes |
deadline_slot | u64 | The last slot the transaction may land in |
fee_bps | u16 | The fee rate the user was shown |
ciphertext0, ciphertext1 | 88 bytes each | The output notes, encrypted. See Notes |
Checks, in order:
- The current slot is at most
deadline_slot(Expired). - The absolute
ext_amountand therelayer_feeare at mostMAX_AMOUNT(AmountTooLarge). - By direction: a shield needs deposits open, at least
min_deposit(DepositTooSmall) and no relayer fee (DepositFee); a private transfer needs transfers open; an unshield needs exits open. Any relayer fee also needs exits open (Paused). fee_bpsequals the pool's current rate for the direction (FeeChanged).- The program computes
extDataHashfrom the accounts and the ext data, andpublicAmount = ext_amount - relayer_fee, reads the root atroot_index, and verifies the proof (BadRoot,BadProof). See The circuit. - Anchor creates both nullifier accounts, paid by the payer. A spent nullifier's account already exists, so the transaction fails.
Then it inserts both commitments, emits Inserted, and moves SOL.
Fees
fee = |ext_amount| × fee_bps / 10,000 rounded down
buyback share = fee × buyback_share_bps / 10,000 rounded down
treasury share = fee − buyback share the rounding goes to the treasuryfee_bps is shield_fee_bps for a shield, exit_fee_bps for an unshield, and 0 for a private transfer.
| Action | Into the vault | Out of the vault |
|---|---|---|
| Shield | ext_amount from the payer. The shield fee is on top: the payer also pays the fee to the treasury and the buyback, and the note holds the full amount | Nothing |
| Private transfer or merge | Nothing | relayer_fee to the relayer |
| Unshield | Nothing | The amount less the fee to the recipient, the fee to the treasury and the buyback (it comes out of the exit), relayer_fee to the relayer |
The notes pay |ext_amount| + relayer_fee for an unshield and relayer_fee for a transfer, which is what
publicAmount makes the circuit balance. The payer of every transaction also pays the rent of its two nullifier
accounts; for a relayed transaction the relayer is the payer, and its fee covers that rent.
The rate is part of the proof's ext data and must match the pool's: a fee change after a transaction was built makes the program refuse it, so no transaction is ever charged a rate the user was not shown.
Pause flags
| Flag | Value | Stops |
|---|---|---|
PAUSE_DEPOSITS | 1 | Shields |
PAUSE_TRANSFERS | 2 | Private transfers and merges |
PAUSE_EXITS | 4 | Unshields, and every transaction that pays a relayer fee: every path that takes SOL out of the vault for a user |
PAUSE_BUYBACK | 8 | buyback |
The guardian or the admin sets flags; only the admin clears them. A relayed private transfer needs both transfers and
exits open, since it pays the relayer from the vault. observe is not paused: it moves no SOL.
Events
| Event | Fields | Emitted |
|---|---|---|
Inserted | index (u64, the first of the two leaves), commitments (two 32-byte values) | By every transact |
Exited | amount (u64, all SOL that left the vault in the instruction: exit, fee and relayer fee), vault_after (u64, the vault's balance after it) | By every transact that moves SOL out of the vault |
The indexer reads both, and alerts on Exited amounts above a share of the vault.
Limits
| Limit | Value | Enforced |
|---|---|---|
MAX_AMOUNT | 2^60 lamports, for ext_amount and relayer_fee | By transact (AmountTooLarge). Keeps the circuit's field sums from wrapping |
MAX_FEE_BPS | 100 (1%), for the shield and the exit fee | By initialize and set_params (BadParams). Higher takes a program upgrade |
buyback_share_bps | at most 10,000 | By initialize and set_params (BadParams) |
min_deposit | above 0 | By initialize and set_params (BadParams). Keeps dust notes out of the tree |
deadline_slot | the transaction must land by it | By transact (Expired). The app sets 150 slots ahead; the relayer accepts at most 900 by default |
CIPHERTEXT_LEN | 88 bytes | By the instruction layout: each ciphertext is a fixed 88-byte field |
| Tree | 2^26 leaves, 192 recent roots | By transact (TreeFull, BadRoot) |
There is no cap on how much can leave the vault per transaction or per time window, and no cap on the pool's size.
Every exit emits Exited for monitoring.
Errors
| Error | Meaning |
|---|---|
BadTree | The tree account's magic or size is wrong |
BadRoot | root_index is out of range or names an unwritten entry |
AmountTooLarge | An amount is above MAX_AMOUNT, or the buyback's expected output does not fit |
BadProof | The proof does not verify |
TreeFull | No room for two more leaves |
Poseidon | The Poseidon syscall failed |
Lamports | Not enough lamports in the account being debited |
Expired | Past deadline_slot |
Paused | The flag for this path is set |
DepositTooSmall | A shield below min_deposit |
DepositFee | A shield with a relayer fee |
FeeChanged | fee_bps is not the pool's current rate |
Unauthorized | The signer may not call this instruction |
BadParams | Parameters or pause flags out of range |
BadTokenAccount, InputNotSpent, WrongPool, TooSoon, NotEnoughObservations, StaleObservations, PoolDisabled, PricePumped, ChunkTooSmall, BuybackPinned, BadMint | The buyback's checks. See Buyback and burn |
Upgrades
The program is upgradeable, so a bug can be fixed in place without moving funds to a new program. The upgrade authority and the config's admin are one key, the developer's, with no multisig and no timelock.
That key can change every rule: the fees (within MAX_FEE_BPS with set_params, or beyond it with an upgrade), the
verifying key, and where funds go. With an upgrade it can move the vault's SOL. Users of the pool trust whoever holds
it. The guardian can only pause.
Every upgrade is visible on-chain: the indexer alerts when the program's ProgramData changes, as it does on every pause
and parameter change. The 128 reserved bytes in Config let an upgrade add parameters without an account migration.
Trusted setup
Where the Groth16 keys come from - the public phase 1, one contribution closed with a Solana block hash, the published hashes, and how to check them yourself.
The relayer
Why private transfers and unshields go through a relayer, what it checks, what it can and cannot do, its fee quote, its API, and the buyback crank it runs.