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:
Exit credits to users (correct pattern)
Miner volume-fee share (missing leaf)
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:
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_accountvariable is already available in scope at both locations, having been initialized at line 742.
References
Pallet::process_exit_bundle: full settlement logicPallet::record_transfer: ZK-tree leaf insertionqp_wormhole::extract_author_from_digest: miner address derivationqp_wormhole::derive_wormhole_address: Poseidon2 hash address derivationMiningRewards::mint_reward: correct pattern withrecord_transfer_proofRuntime fee parameters:
VolumeFeeRateBps,VolumeFeesBurnRate,VolumeFeesAggregatorRatePinned commit: 3b243b8
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_balancebut norecord_transferis called — no ZK-tree leaf, noNativeTransferredevent, noTransferCountincrement.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.
Proof source
Impact assertions
The test proves permanent freezing of funds in two parts:
Part A — The defect (miner credited without leaf):
Miner balance increases by the volume-fee share, confirming
increase_balanceexecuted.MinerVolumeFeePaidevent emitted, confirming the fee credit path ran.No
NativeTransferredevent for the miner fee — provesrecord_transferwas never called.TransferCountfor the miner address unchanged — independently confirms no ZK-tree leaf insertion.
Part B — Impact (funds permanently frozen):
No ZK-tree leaf matches the miner's canonical address for the fee amount — confirmed by walking every leaf inserted during dispatch.
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:
3b243b870a5442a6e5f443056f67bbba87d472f5Uses the pallet's own pre-computed ZK proof fixture (
test-data/private_batch.hex)Exercises the full
verify_private_batchextrinsic dispatch pathNo target code modifications needed
Was this helpful?