88300 bc high missing wormhole zk leaf permanently freezes all miner exit fee yield
Submitted on Aug 12th 2026 at 17:11:50 UTC by @Porcupine17126 for Audit Comp | Quantus
Report ID: #88300
Report Type: Blockchain/DLT
Report severity: High
Target: https://github.com/immunefi-team/audit-comp-quantus-chain
Impacts:
Permanent freezing of unclaimed yield
Description
Brief / Introduction
Every successful Wormhole exit allocates part of its volume fee to the current block author. The runtime increases the author's native balance and emits MinerVolumeFeePaid, but it does not call Wormhole::record_transfer. Production block-author reward accounts are keyless Wormhole addresses, so a committed ZK-tree leaf is the only usable claim to the credited value. The miner therefore receives an on-chain balance that can never be spent or withdrawn.
The production parameters charge 4 bps on Wormhole exit volume and allocate 50% to the miner. Approximately 2 bps of every successful Wormhole exit is permanently frozen (with the circuit's one-quantum minimum affecting small exits).
Scoped commit: 3b243b870a5442a6e5f443056f67bbba87d472f5
Vulnerability Details
The block author is a Wormhole address
Production validators must provide --rewards-inner-hash. The node validates it as a canonical Poseidon digest and stores that inner digest in the QPoW pre-runtime log. The runtime derives the author account as:
This is a Wormhole address, not an ordinary ML-DSA signing account:
Settlement credits a balance but omits the leaf
The exit settlement credits the miner with event-free Unbalanced::increase_balance and emits MinerVolumeFeePaid:
The success branch never invokes Self::record_transfer. No other mechanism repairs the omission:
increase_balanceemits noBalances::Mintedor transfer event forWormholeProofRecorderExtensionto scan.verify_private_batchandverify_public_batchare bare unsigned extrinsics, so the signed-transaction extension does not run for settlement.
The adjacent production paths demonstrate the required invariant:
Each successfully minted Wormhole exit explicitly calls
Self::record_transfer: pallets/wormhole/src/lib.rs#L816-L859.Each block mining reward explicitly calls
ProofRecorder::record_transfer_proof: pallets/mining-rewards/src/lib.rs#L220-L240.
record_transfer increments TransferCount, inserts (recipient, transfer_count, asset_id, amount) into the Poseidon Merkle tree, and exposes the leaf index needed for a proof:
Knowing the Wormhole secret cannot compensate for a missing leaf. The secret proves ownership of a leaf already committed to a historical root; it cannot synthesize an omitted leaf or witness.
Impact Details
Any valid private- or public-batch Wormhole exit included by a normal production validator reaches this path. No malicious validator, privilege, malformed proof, Sybil behavior, or race is required.
The post-state is inconsistent:
The miner cannot sign from the derived Wormhole address and cannot construct a Wormhole withdrawal proof without the omitted leaf. The entire miner share is permanently frozen as unclaimed mining yield.
At the scoped runtime parameters:
See runtime/src/configs/mod.rs#L728-L752.
Later correctly recorded mining rewards do not repair the missing credit: those leaves commit only their own amounts and transfer counts, not the stranded earlier fee.
Suggested Remediation
After successful miner increase_balance, explicitly record the native-token credit before emitting MinerVolumeFeePaid:
Update settlement weights for the additional ZK-tree insertion. Retaining event-free increase_balance is appropriate; changing only to mint_into would rely on an event-scanning extension that does not execute for these bare unsigned settlement extrinsics.
Note on commit 60ec991
Commit 60ec991 added MinerVolumeFeePaid because the event-free credit was ambiguous to indexers. That change improves visibility but does not create a ZK-tree leaf. It consequently reports the keyless address as paid while the value remains unclaimable; it does not fix or document this permanent-freeze impact.
Proof of Concept
This PoC extends the repository's existing valid-private-batch test. The original test already loads a real valid proof fixture, sets the matching block hash and QPoW author digest, executes verify_private_batch, and verifies both the nonzero fee balance increase and MinerVolumeFeePaid event.
The added invariant captures Wormhole::transfer_count(author) before settlement and proves it remains unchanged afterward, even though the balance was credited.
What the PoC proves
The test verifies all of the following in one successful valid-proof execution:
expected_miner_fee > 0.The QPoW-derived author's free balance increases by exactly that fee.
MinerVolumeFeePaidreports the exact author and fee.The same author's
Wormhole::TransferCountdoes not advance.
Since record_transfer is the only path that advances the counter and inserts the spendable ZK leaf, the credited fee has no witness in the committed tree and is permanently unclaimable.
Was this helpful?