Fee collection
How the three kinds of fee the pool accrues are collected, who can collect each, and where the tokens go.
The pool accrues three kinds of fee, and each is collected differently. Protocol fees sit in private fee notes owned by the treasury identity and are collected in batches with a zero-knowledge proof. Escape-hatch fees and relayer tips are public counters that the pool program pays out on request. Until a fee is collected, its tokens stay in the pool token account, or for SOL in the pool's lamport account.
| Fee | Accrues as | Collected by | Paid to | Proof |
|---|---|---|---|---|
| Protocol fee | Private fee notes in the fee commitment tree | Anyone submitting a valid batch | The treasury payout account governance set for the token | A fee-collection proof by the treasury identity |
| Escape-hatch fee | A public counter per pool index and token | Anyone | The same treasury payout account | None |
| Relayer tip | A public counter per relayer and token | The relayer, which must sign | Any account of that token the relayer names | None |
SOL is paid in lamports from the pool's lamport account, and the treasury payout address for SOL is a wallet rather than a token account. A wallet that holds no lamports must receive at least the rent-exempt minimum for an empty account, or the Solana runtime refuses the collection. See SOL.
Protocol fee notes
How they accrue
Every value transaction publishes three fee commitments, one per token slot, each committing to a token and the protocol fee paid in it. A slot that carries no token commits to zero, so it looks like any other. The fee inserter appends them to the fee commitment tree of the pool index that receives the transaction's new notes, and only then can they be collected. Each fee note belongs to the treasury identity the fee enclave record named when the transaction executed.
Who can collect them
The treasury owner commitment is an identity commitment like a user's, and whoever knows its opening can collect. The fee enclave derives that identity's keys from its QuorumOS quorum key and attests the commitment every time it boots (see the fee enclave). Collecting also needs each fee note's amount and blindings. The fee enclave seals these under its escrow key for every transaction it signs, and its relay archives the sealed copy to storage that refuses overwrites.
Collecting a batch of fee notes
A collection spends from one to twenty fee notes of one token at once. The treasury's prover builds a proof off chain, then anyone submits it and the pool pays out.
sequenceDiagram participant T as Treasury prover participant M as Chain mirror T->>T: open escrowed fee notes T->>M: fee note paths, nullifier gaps M-->>T: witnesses at recent roots T->>T: fee-collection proof
1. The treasury's prover gathers openings and witnesses
The escrowed fee notes open with the fee enclave's escrow key, the key they were sealed under. From a chain mirror it fetches each note's path in the fee commitment tree and a witness that the note's fee nullifier is not yet in the fee-nullifier tree.
2. The treasury's prover proves the batch
The fee-collection proof shows that the collector knows the opening of the treasury owner commitment, and that:
- each note is in the fee commitment tree at a recent root, owned by that commitment and in the named token;
- each note's fee nullifier is derived from the treasury's nullifier key and is not yet in the fee-nullifier tree;
- the notes' amounts add up to the stated total.
In a batch of fewer than twenty, the remaining positions are padding.
sequenceDiagram participant S as Submitter participant P as Shielded pool participant X as Treasury token account S->>P: fee collection P->>P: check treasury account P->>P: resolve roots, verify proof P->>P: record real nullifiers P->>X: total from pool token account
3. Anyone submits the collection
The transaction needs one signer, who pays the Solana fee. The submitter chooses when the fees move, not where they go.
4. The shielded pool checks the request and the destination
The program checks that the batch holds between one and twenty real notes and that the token matches. It refuses any destination other than the treasury payout account governance set for the token.
5. The shielded pool resolves the roots and verifies the proof
Both roots the proof used must still be recent; a fee commitment tree root stays usable while it is among the tree's 512 most recent. The program then verifies the proof under the fee-collection key compiled into it.
6. The shielded pool records the nullifiers and pays the total
The program records each real fee nullifier, refusing one already recorded, so no fee note is collected twice. It then pays the total from the pool token account to the treasury payout account, or in lamports to the treasury wallet for SOL. It refuses a destination that is the pool's own account, because a transfer to itself would spend the fee notes and move nothing.
Collecting on Robinhood Chain
On Robinhood Chain protocol fees are collected the way they are on Solana: in occasional sessions of a fee-collection enclave that holds the Robinhood fee enclave's quorum key, so the treasury's keys never leave an enclave, and the fee enclave that endorses every transaction holds no collection key at all. A session's job follows the pool's own fee events, a daemon keeps the fee queue drained between sessions, and both pay deferred fees out as the treasury cap allows.
The job follows the pool's fee history
The job and the daemon read the pool's fee events into their own copy of the fee tree, the fee nullifiers the turnstile has admitted and the tokens with deferred fees. They remember the hash of the last processed block, including blocks without fee events. If that block is replaced or the chain becomes shorter, they clear everything they derived and replay from the configured deployment block. Malformed events or a root mismatch fail the current pass and allow a later clean replay.
History through the RPC node's finalized height is read in ranges of at most LOG_CHUNK_BLOCKS, relying on consensus finality. Newer blocks are read individually by exact block hash, with each parent linked to the previous checkpoint. If the provider cannot report finality, all blocks use this slower hash-based path. The provider must support the EIP-234 blockHash log filter; unsupported filters fail the pass instead of falling back to mutable block numbers. Each RPC attempt and each mirror request has a 30-second deadline, including the response body.
A session drains the queue, then collects contiguous ranges
A session first drains the fee queue until it is empty, so every fee note queued before it is in the tree. It then indexes the fee enclave's escrow archive and asks the enclave to plan the next contiguous range of inserted fee-tree leaves, at least a configured minimum of them. The enclave checks that every leaf folds to the tree's root as the treasury's leaf and that its escrow opens, groups the paid notes by token into batches of 20, and records the range in a sealed ledger the session copies to storage that refuses overwrites and deletes. A range that overlaps one planned before, in this session or an earlier one, is refused, so no two totals can be subtracted to expose one transaction's fee. The enclave proves each batch with fee_claim_20 under its hardened prover, against the fee-nullifier root the turnstile mirror serves its gap witnesses at.
The job submits each collection
The job simulates each claimFees with eth_call before it sends it. A collection the pool cannot pay because governance has set no treasury payout address waits for a later session; one whose proof the pool refuses stops the session, because the enclave's proving key is then not the deployed one. The pool spends the notes and pays the payout address governance set. When the treasury outflow cap cannot cover the whole total at once, the pool pays what the cap allows and records the rest as deferred, and sweepDeferredFees pays more of it as the cap refills.
The daemon drains and sweeps between sessions
A fee collector daemon drains the fee queue once 96 entries wait or an operator asks, which keeps users' transactions off the queue's fallback insert, and sends sweepDeferredFees for every token with deferred fees. It holds only a gas key: neither call takes an input that decides where fees go.
The Robinhood pool, SDK and CLI support only the 20-slot proof; smaller collections fill the unused slots with padding. The retired 5-, 10- and 15-slot proof identifiers cannot be loaded again, while the existing 20-slot identifier and verifying key stay the same. Pool interface version 3 marks this restriction.
Escape-hatch fees
The escape hatch charges the default withdrawal rate on the note it spends and adds the fee to a public accrual for that pool index and token. Anyone may create the accrual, and an escape that owes a fee fails until it exists. Anyone may also collect it: the collection pays the whole balance to the treasury payout account for the token and zeroes the counter. As with protocol fees, the submitter decides only when the fees move.
Relayer tips
A relayer's tip is taken from the sender's notes and credited to a public accrual keyed by the relayer's address and the token. The accrual's address is part of the transaction's context digest, which ties a paid transaction to one relayer (see relayers). Only the relayer can collect, because it must sign. The collection pays everything accrued in one token to any account of that token the relayer names, and zeroes the counter.
On Robinhood Chain the transaction names the submitter address its tip goes to and the token it is paid in, which can be any of the transaction's tokens: a sender can pay the tip from a note of a different token than the one it moves, so long as the pool holds notes of that token. The pool charges the withdrawal rate on the tip and credits the rest to that address. Only that address can collect, and the collection pays the address itself, never another account, and draws on the token's outflow cap for relayer tips. Nimbus's Robinhood relayer collects on its own. While none of its transactions is waiting to land, each submitter address reads what it has accrued at a set interval. It collects every token once the accrued amount reaches a set multiple of that token's minimum tip, or, with the multiple set to zero, as soon as anything has accrued. It then unwraps the WETH it collected into the ETH it pays gas with.
Setting the treasury payout account
Only a governance proposal can set the treasury payout account for a token, and only once the proposal's timelock has elapsed; anyone may then execute it. See governance and upgrades.
What collection reveals
A protocol fee collection publishes the token, the total, the number of real notes and their fee nullifiers. It does not reveal which transactions the notes came from, because fee nullifiers are derived with the treasury's own key and match no user nullifier. An escape-hatch or tip collection reveals only what its public counter already showed.
What can go wrong
- The destination is not the treasury payout account governance set for the token, or governance has set none.
- The proof's roots are no longer recent, or a fee note has not yet been inserted into the fee commitment tree.
- A fee note in the batch was already collected, which fails the whole collection.
- An escape-hatch accrual has a zero balance, or an escape that owes a fee runs before its accrual exists.
- A SOL payout to an empty wallet is below the rent-exempt minimum.
- Governance names the pool's own account as the payout account, and every collection in that token fails until it names another. The fees stay collectable.
Related
Last updated