Fee Lifecycle
Relay fees do not leave the shielded pool immediately. After each relayed operation, the fee amount stays physically inside pool_account_id while an on-chain accounting counter (PendingRelayerFees) tracks how much each relayer is owed. The relayer later claims the tokens through one of two paths.
Overview: tokens never leave the pool at fee time
When a user calls unshield(amount=900, fee=100):
- 900 tokens are transferred from
pool_account_idto the recipient's public account - 100 tokens remain inside
pool_account_id— they are not moved at all PendingRelayerFees[validator][asset_id]is incremented by 100
The fee is an accounting entry, not a token transfer. This keeps pool accounting consistent and avoids two separate token movements per operation.
Where fees accumulate: PendingRelayerFees
PendingRelayerFees is a StorageDoubleMap<AccountId, u32, u128> in pallet-relayer. Keys are (validator_account_id, asset_id) and the value is the accumulated fee in planck.
It is incremented by accumulate_relay_fee (called from pallet-shielded-pool after each successful relayed operation) and decremented by consume_relay_fee (called from either claim path).
Multiple operations accumulate in the counter until the relayer decides to claim.
The two claim paths
Once the relayer wants to collect their fees, they choose one of two extrinsics in pallet-shielded-pool:
claim_shielded_fees(commitment, amount, asset_id, memo)
- Requires a disclosure ZK proof
- Inserts a new commitment into the Merkle tree
- Funds are fully private: spendable as any other note
- Call index 16
claim_relay_fees_to_evm(asset_id, amount)
- No ZK proof required
- Tokens go directly to the relayer's H160 EVM account
- Visible on-chain as a public transfer
- Receive fees directly to a public EVM account
- Call index 17
Path A in detail: claim_shielded_fees
The validator computes a commitment off-chain:
commitment = Poseidon(amount, asset_id, validator_public_key, blinding)
Then calls the extrinsic. The pallet:
- Verifies
PendingValidatorFees[caller][asset_id] ≥ amount - Calls
T::Relayer::consume_relay_fee(caller, asset_id, amount)— decrements the counter - Inserts
commitmentinto the shielded pool Merkle tree - Records the encrypted memo for wallet scanning
The validator now holds a private note worth amount. They can spend it via private_transfer or unshield at any future time without any on-chain link to their validator identity.
Path B in detail: claim_relay_fees_to_evm
Path B allows a validator to receive fees directly to a public EVM account without a ZK proof. This is useful when the validator prefers a transparent on-chain transfer rather than a private note.
The pallet steps:
- Reads
registered_evm_address(caller)frompallet-relayer→ obtains the registered H160 - Derives the H160's mirror AccountId:
H160[0..20] ++ [0x00; 12](EeSuffixAddressMapping) - Verifies
PendingRelayerFees[caller][asset_id] ≥ amount - Calls
T::Relayer::consume_relay_fee(caller, asset_id, amount) - Transfers
amounttokens frompool_account_idtomirror_account - Decrements
PoolBalance[asset_id]byamount - Emits
RelayFeesClaimedToEvm { validator, evm_address, asset_id, amount }
Because Orbinum uses EeSuffixAddressMapping, any ORB held by the mirror AccountId is immediately visible as the H160's EVM balance. No bridge or wrapping step is needed.
The EeSuffixAddressMapping maps an H160 address to a 32-byte AccountId by padding with 12 zero bytes:
AccountId = H160_bytes[0..20] ++ [0x00; 12]
Any Substrate balance on this AccountId appears as the H160's EVM balance. There is no wrapping or conversion.
Comparison: Path A vs Path B
claim_shielded_fees | claim_relay_fees_to_evm | |
|---|---|---|
| Call index | 16 | 17 |
| Destination | Merkle tree commitment | H160 EVM account |
| Visibility | Private (ZK UTXO) | Public (on-chain transfer) |
| ZK proof required | Yes (disclosure circuit) | No |
| Caller registered in relayer registry | Not required | Required |
| Token source | pool_account_id | pool_account_id |
| Typical use | Long-term private accumulation | Receive fees as a public EVM balance |
Both paths pull tokens from pool_account_id and both decrement PoolBalance[asset_id]. The only difference is where the tokens land.
Partial claims
Both paths accept any amount up to the full pending balance. The counter is not reset — remaining fees stay in PendingRelayerFees for future claims.
A relayer may, for example:
- Claim 50% as a private note for long-term savings
- Claim the other 50% to EVM to cover imminent gas costs
Security considerations
PoolBalance[asset_id] tracks the total tokens in the pool that are owed to note holders. Both claim paths decrement it when tokens leave the pool, keeping the accounting consistent. A validator cannot claim more than their registered pending fees, so note holders are never under-collateralized.
claim_relay_fees_to_evm emits a public on-chain event with the validator identity, EVM address, asset, and amount. If anonymity of fee accumulation is a concern, use Path A instead.