For the complete documentation index, see llms.txt. This page is also available as Markdown.

89689 bc high miner volume fee and aggregator rebate credits lack zk tree leaves permanently freezing funds

Submitted on Aug 20th 2026 at 15:40:27 UTC by @abarbatei for Audit Comp | Quantus

  • Report ID: #89689

  • Report Type: Blockchain/DLT

  • Report severity: High

  • Target: https://github.com/immunefi-team/audit-comp-quantus-chain

  • Impacts:

    • Permanent freezing of funds

Description

Brief/Intro

When a wormhole exit bundle is settled on-chain, the volume fee owed to the block-producing miner and the rebate owed to the aggregator are credited via Unbalanced::increase_balance, but no ZK-tree leaf is created for either credit. Because miner addresses are always wormhole-derived (keyless Poseidon2 hashes with no corresponding private key), and aggregator addresses may also be wormhole-derived, the credited funds can never be spent: the ZK exit circuit requires a Merkle proof rooted in a leaf that does not exist, and no private key exists to sign a conventional extrinsic. This results in permanent freezing of protocol fees that accumulates with every wormhole exit settlement.

Vulnerability Details

The wormhole pallet's Pallet::process_exit_bundle function handles three categories of on-chain credit during exit settlement:

  1. Exit credits to users (correct pattern)

  2. Miner volume-fee share (missing leaf)

  3. Aggregator rebate (missing leaf)

Correct pattern: exit credits

User exit credits correctly follow a two-step process: increase_balance to credit the account, then Pallet::record_transfer to insert a leaf into the ZK Merkle tree.

lib.rs::process_exit_bundle lines 828-859

Bug: miner volume-fee credit omits record_transfer

The miner's share of the volume fee is credited via increase_balance but no Pallet::record_transfer call follows. Only a MinerVolumeFeePaid event is emitted.

lib.rs::process_exit_bundle lines 955-973

Bug: aggregator rebate credit omits record_transfer

The aggregator rebate suffers the same omission.

lib.rs::process_exit_bundle lines 919-933

Why there is no alternative spend path for miner addresses

The miner (block author) address is always derived via qp_wormhole::extract_author_from_digest, which calls derive_wormhole_address(preimage).

lib.rs::extract_author_from_digest lines 122-151

lib.rs::derive_wormhole_address lines 109-111

The address is a Poseidon2 hash output. No private key corresponds to this address. This means:

  • Signed extrinsics: Impossible. No private key exists to produce a valid signature.

  • ZK exit proof: Impossible. The ZK circuit requires a Merkle proof against a leaf in the ZK tree, but no leaf was ever created for these credits.

  • Proxy or recovery pallets: Require initial setup from the target account, which itself requires a signature.

Contrast with mining block rewards (correct pattern)

The pallet-mining-rewards correctly records a transfer proof after minting block rewards to the same wormhole-derived miner address:

lib.rs::mint_reward lines 230-237

For reference, Pallet::record_transfer creates the ZK-tree leaf that enables future withdrawal via ZK exit proofs.

Fee parameters

The volume fee parameters are configured in the runtime:

mod.rs lines 728-738

Impact Details

Permanent freezing of miner volume-fee revenue. Every wormhole exit settlement permanently freezes the miner's share of the volume fee. The frozen amount per settlement equals 0.02% of exit volume (4 bps fee rate, 50% burn rate, remaining 50% to miner).

This is not a one-time event. It occurs on every single wormhole exit bundle settlement during normal protocol operation, with no attacker action required. The frozen amount accumulates indefinitely over the lifetime of the chain.

For public-batch exits, the aggregator rebate (50% of the burn portion, i.e., 0.01% of exit volume) is also frozen if the aggregator uses a wormhole-derived address.

The impact is classified as permanent freezing of funds:

  • Funds are credited to the miner's on-chain balance (visible via state queries).

  • No mechanism exists to move the funds: no private key (Poseidon2-derived address), no ZK leaf (omitted record_transfer), no governance override in scope.

  • The loss compounds with every exit settlement, growing without bound.

  • While individual amounts are small (protocol fee fractions), the cumulative effect degrades miner incentive alignment over the chain's lifetime.

Mitigation

Add Pallet::record_transfer calls after each increase_balance for the miner volume fee and the aggregator rebate, matching the pattern used for exit credits.

For the miner volume-fee credit (after line 964):

For the aggregator rebate credit (after line 923):

The mint_account variable is already available in scope at both locations, having been initialized at line 742.

References

Proof of Concept

Overview

This is an integration test that exercises the full verify_private_batch extrinsic dispatch with a real pre-computed ZK proof fixture (test-data/private_batch.hex). The proof undergoes real plonky2 deserialization, block-state validation, nullifier checks, exit minting, and fee distribution — the complete on-chain settlement path.

The test demonstrates both the defect and the impact:

  • Part A (defect): After full dispatch, the miner is credited via increase_balance but no record_transfer is called — no ZK-tree leaf, no NativeTransferred event, no TransferCount increment.

  • Part B (impact): The ZK tree is walked to confirm no leaf exists for the miner's volume-fee credit. The miner address is a Poseidon2 hash with no private key, so without a ZK leaf there is no spend path — funds are permanently frozen.

1

Setup

Save the test file as pallets/wormhole/src/proof_vol_fee_no_leaf.rs in the audit-comp-quantus-chain repository at commit 3b243b8.

Then temporarily add the module declaration to pallets/wormhole/src/lib.rs:

2

Execution command

Proof source

Impact assertions

The test proves permanent freezing of funds in two parts:

  • Part A — The defect (miner credited without leaf):

    1. Miner balance increases by the volume-fee share, confirming increase_balance executed.

    2. MinerVolumeFeePaid event emitted, confirming the fee credit path ran.

    3. No NativeTransferred event for the miner fee — proves record_transfer was never called.

    4. TransferCount for the miner address unchanged — independently confirms no ZK-tree leaf insertion.

  • Part B — Impact (funds permanently frozen):

    1. No ZK-tree leaf matches the miner's canonical address for the fee amount — confirmed by walking every leaf inserted during dispatch.

    2. The miner address is a Poseidon2 hash with no private key. Without a ZK leaf, there is no spend path: no signed extrinsic (no key), no ZK exit proof (no leaf), no proxy/recovery (requires initial setup from the account).

Reproducibility

  • Pinned commit: 3b243b870a5442a6e5f443056f67bbba87d472f5

  • Uses the pallet's own pre-computed ZK proof fixture (test-data/private_batch.hex)

  • Exercises the full verify_private_batch extrinsic dispatch path

  • No target code modifications needed

Was this helpful?