Skip to main content

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_id to 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:

Path A — Private ZK note

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
Path B — Direct EVM transfer

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:

  1. Verifies PendingValidatorFees[caller][asset_id] ≥ amount
  2. Calls T::Relayer::consume_relay_fee(caller, asset_id, amount) — decrements the counter
  3. Inserts commitment into the shielded pool Merkle tree
  4. 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:

  1. Reads registered_evm_address(caller) from pallet-relayer → obtains the registered H160
  2. Derives the H160's mirror AccountId: H160[0..20] ++ [0x00; 12] (EeSuffixAddressMapping)
  3. Verifies PendingRelayerFees[caller][asset_id] ≥ amount
  4. Calls T::Relayer::consume_relay_fee(caller, asset_id, amount)
  5. Transfers amount tokens from pool_account_id to mirror_account
  6. Decrements PoolBalance[asset_id] by amount
  7. 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.

H160 mirror AccountId

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_feesclaim_relay_fees_to_evm
Call index1617
DestinationMerkle tree commitmentH160 EVM account
VisibilityPrivate (ZK UTXO)Public (on-chain transfer)
ZK proof requiredYes (disclosure circuit)No
Caller registered in relayer registryNot requiredRequired
Token sourcepool_account_idpool_account_id
Typical useLong-term private accumulationReceive 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

Pool balance integrity

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.

Path B is public

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.