Fee collection circuit
The circuit that lets the treasury identity collect up to twenty protocol fee notes at a time, what its proof shows, and what the claim_fee_20 handler checks and pays.
Protocol fees accumulate as private fee notes owned by the treasury identity, and they leave the pool in batches of up to twenty. One circuit, fee_claim_20, proves a batch: that its notes are the treasury's, distinct, unspent, of one token, and that their amounts add up to the total being paid.
The path of a fee from the transaction that pays it to the treasury's payout account is shown below.
- Value transactionproves the fee is at least what the rates require
- Fee commitmentone per token slot
- Fee-tree leafwrapped with the treasury's identity commitment
- Fee collection proofa batch of 1 to 20 notes
- Treasury payout accountset by governance
Where fee notes come from
Every value transaction stages three fee commitments, one per token slot, in the fee store of the pool index it appends its output notes to, and an insert later appends them to that pool index's fee tree. Each is a Poseidon hash of the token identifier, the fee total for that token and two blindings; see the transaction circuit. The program wraps each into a fee-tree leaf with the transaction's slot, its position in the tree and the treasury owner commitment, which it reads from the fee enclave's on-chain record when the transaction executes and stores with the staged commitments. The transaction's sender never chooses who owns the fee note.
The fee enclave sees the openings of these commitments when it checks the transaction, and escrows them in encrypted form. The treasury needs those openings later, because a fee commitment is hiding: without the amount and the blindings, nobody can prove what it contains.
What the proof shows
The circuit is FeeClaim(20, 10, 8, 1, 4): twenty note positions over a fee commitment tree of 10 levels of 8 children and one level of 4 (2³² leaves), checked against a fee-nullifier tree of 11 levels of 8 children and one level of 8 (2³⁶ leaves). For a batch it proves the following.
The treasury identity. The circuit rebuilds the treasury's identity commitment from its spending key, nullifier key and outgoing viewing key with their blinders and five other slot leaves. Every fee leaf contains that identity commitment, so only the holder of the treasury identity's keys can prove anything about these notes.
Real notes first. Each of the twenty positions carries a flag saying whether it holds a real note. The flags are 0 or 1, every real note comes before every padding position, and the public real count equals the number of real flags. A batch of three notes therefore has three real positions followed by seventeen padding positions, and the handler knows exactly which nullifiers are real.
Each real note is the treasury's. Its fee commitment, rebuilt from the token identifier, amount and two blindings, wrapped into a leaf with the treasury's identity commitment, is in the fee tree at the given root, and its token equals the batch's token. A note of another token cannot be counted in this batch.
Each nullifier is correct. For every position, padding included, the nullifier is the circuit-form hash of the treasury's nullifier key, the note's insertion position and its nullifier blinding, under the fee-note nullifier domain. Each fee note has exactly one nullifier, so it can be collected only once.
No note fills two positions. The twenty nullifiers, padding included, are pairwise distinct. One note placed in two real positions would repeat its nullifier and add its amount to the total twice. The fee staging buffer would also refuse the repeat, but the proof does not rely on it.
Every nullifier is unspent. Each, padding included, falls strictly between a leaf of the fee-nullifier tree and its successor at the given root.
The total is right. The public total equals the sum of the real notes' amounts, each and the total range-checked to 64 bits. The program pays exactly this total, so the proof is what stops a collection from taking more than the notes hold.
The public input folds 25 values: the fee-tree root, the fee-nullifier root, the token identifier, the total, the real count and the twenty nullifiers.
One batch size
Fee collection has one size. Every collection proves twenty positions and is checked by one instruction, claim_fee_20; a collection of fewer notes fills the remaining positions with padding. The circuit has 256,144 non-linear constraints (circom 2.2.3, optimisation level 2).
What the handler checks
claim_fee_20 runs these checks in order.
Check the nullifiers and the submitter
Every nullifier, padding included, must be a canonical field element, and the submitter must sign.
Check the real count
The real count must be at least 1 and at most 20.
Check the destination
The receiving account must be the one governance named as the treasury's payout account for this token: a token account of the token, or a wallet for SOL.
Resolve the roots
The fee-tree root is looked up by sequence number, and so is the fee-nullifier root, from the fee staging buffer (the code calls it the turnstile). Either one refused as no longer recent stops the collection.
Verify the proof
The handler folds the public values, computing the token identifier from the mint account's address, and verifies the proof under the twenty-note fee-collection key compiled into the program.
Stage the real nullifiers
Only the real nullifiers are staged in the fee staging buffer, which refuses one already staged, including a repeat inside the batch.
Pay the treasury
The handler transfers the total from the token's pool token account to the treasury's payout account. For SOL it pays the total in lamports from the pool's lamport account.
The proof does not name a destination. The destination is whatever account governance has set for that token through a timelocked proposal, so whoever makes the proof cannot redirect the payment. Making a proof needs the treasury identity's keys; submitting it needs only a signature.
Other fees need no proof
Two other kinds of fee accrue in public counters rather than notes, and are collected without a proof:
- Escape-hatch fees, charged by the escape-hatch handler, accrue per token and are collected with
claim_escape_hatch_fee. - Relayer tips accrue per relayer and token and are collected with
claim_submitter_tip.
See fee collection.
Reference
| Entry | circuits/zk/fee_claim_20.circom |
|---|---|
| Instantiation | FeeClaim(20, 10, 8, 1, 4) |
| AMOUNT_BITS | 64 |
| DEDUP_HIGH_ARITY | 8 |
| DEDUP_HIGH_LEVELS | 1 |
| DEDUP_LOW_ARITY | 8 |
| DEDUP_LOW_LEVELS | 11 |
| NULLIFIER_PAIR_COUNT | 190 |
| SLOT_COUNT | 25 |
| nullifierPair | 190 |
circuits/zk/circom-manifest.json.| Tag | Instruction | Version | Payload bytes | Accounts |
|---|---|---|---|---|
| 22 | treasury_payout_address_set | v0 | 64 | 6 |
| 23 | claim_fee_20 | v0 | 792 | 9 |
contracts/solana/tools/umbra-manifest/manifest/shielded-pool.json.Related
Last updated
Escape-hatch circuit
The proof that lets one note leave the pool without any attester, the eleven note values it discloses, what it proves and does not, and what the escape_hatch instruction computes itself.
Merge circuit
A circuit that combines up to eight of one owner's notes into one, written for the EVM port of the pool and not used by the Solana program.