88912 bc high missing record transfer in wormhole miner fee settlement permanently freezes unclaimed yield
Submitted on Aug 15th 2026 at 15:44:45 UTC by @IronsideSec for Audit Comp | Quantus
Report ID: #88912
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/Intro
Every ordinary Wormhole exit batch can credit 50% of its volume fee to the block author's QPoW-derived Wormhole account, but process_exit_bundle uses event-free Unbalanced::increase_balance and never records the transfer in the ZK tree. The author's raw balance and MinerVolumeFeePaid event therefore increase while no leaf gives the keyless author ownership of the reward, permanently freezing 100% of that fee yield unless Root governance performs an exceptional recovery migration.
Vulnerability Details
Affected in-scope asset: The Substrate L1 node — runtime, FRAME pallets, QPoW consensus, quantus-node
Frozen commit: 3b243b870a5442a6e5f443056f67bbba87d472f5
Root cause
The block author is intentionally a Wormhole account rather than an ordinary ML-DSA account. derive_wormhole_address derives the address as the second Poseidon hash of the 32-byte inner hash placed in the QPoW pre-runtime digest:
extract_author_from_digest extracts that inner hash and returns the derived Wormhole account. The miner knows the Wormhole secret needed for a membership proof, but does not know an ML-DSA key whose public-key hash equals this independently derived account. Verify::verify accepts an ordinary signature only when the supplied ML-DSA public key hashes to the signed AccountId; finding such a key for the Wormhole-derived account would require an infeasible preimage search.
Consequently, a balance credit to this author is spendable only if the runtime also creates the ZK-tree leaf against which the miner can prove membership.
The same settlement function shows the correct invariant for user exits. After crediting each exit account with increase_balance, process_exit_bundle explicitly calls record_transfer:
record_transfer assigns the recipient's next TransferCount, inserts the leaf into the ZK tree, and emits its leaf index. The transfer count, recipient, asset ID, and amount are part of the note that the proof consumes.
The miner-fee branch omits that second half. Both verify_private_batch and verify_public_batch reach process_exit_bundle. The runtime configures a four-basis-point volume fee and a 50/50 burn/miner split. After calculating the split, process_exit_bundle performs only the raw credit and success event:
There is no Self::record_transfer, no TransferProofRecorder::record_transfer_proof, no TransferCount increment, and no leaf insertion. In this frozen fork, Unbalanced::increase_balance bypasses the ordinary balance mutation completion hook, so the runtime's event-scanning proof recorder also has no matched transfer event from which it could repair the omission.
Later base mining rewards do not absorb the missing fee. Mining Rewards records a separate leaf for the exact reward argument it pays; it does not commit the author's aggregate free balance. Each note has its own transfer count and nullifier, so a later note cannot retroactively authorize the earlier unrecorded credit.
Why the finding remains independent of published issues
PR #619: PR #619 publicly observed
increase_balance“withoutrecord_transfer,” but defined the problem as indexer drift and changed only event emission. This PoC successfully observes the addedMinerVolumeFeePaidevent and the raw balance, then demonstrates the distinct security failure left by that event-only fix: the keyless recipient still has no membership leaf and cannot claim the indexed balance. An indexer event is not a ZK ownership commitment.Eiger F-03: Eiger
EQ-QNT-WORMHOLE-F-03concerned incorrect author derivation. The PoC positively asserts that the correctly double-Poseidon-derived author receives the exact fee; the failure occurs afterward.Eiger F-05: Eiger
EQ-QNT-WORMHOLE-F-05concerned unsupported signed transfer variants. This is internal, event-free settlement outside those signed-call variants.Published reward-quantization issue: The real fixture happens to pay a half-quantum miner share, but the second PoC gives the miner one complete
0.01 QUANquantum and still creates no leaf. The missing-recorder defect therefore exists independently of amount quantization.
Recommended remediation
Treat the miner fee as an atomic keyless-account credit-and-proof operation. Accumulate sub-quantum entitlements per miner, and whenever a whole quantum is available, credit exactly that amount and call record_transfer in the same transactional operation. If leaf recording fails, roll back the balance credit and do not emit MinerVolumeFeePaid. Historical missing-leaf balances require an explicit migration because later notes cannot absorb them.
Impact Details
Selected in-scope impact: High — Permanent freezing of unclaimed yield.
For an exit expressed in the circuit's 0.01 QUAN units, the frozen fee rule is:
For large batches the miner's entitlement approaches 2.0008 bps of exit volume. For each ordinary nonzero batch with a canonical QPoW author and a successful miner credit:
The committed real-proof fixture credits 5,000,000,000 Planck (0.005 QUAN) while leaving the author's TransferCount at zero; the only new leaf belongs to the user exit. The aligned control credits exactly 10,000,000,000 Planck (0.01 QUAN) and still leaves the author without a leaf. This proves the loss is caused by missing ownership recording rather than the circuit's precision floor.
No malformed proof, race, compromised key, privileged role, or 51% attack is required. Any normal valid Wormhole exit can trigger the faulty path, and the loss accumulates across both private and public batches. There is no miner-controlled recovery path. Root governance could exceptionally force-transfer or migrate historical balances, but that does not make the yield claimable through the protocol and does not change the selected High impact from “Permanent freezing of unclaimed yield.”
References
Proof of Concept
Reproduction — one patch file against the scoped repository
This PoC runs entirely as local unit tests against the exact in-scope frozen commit. It does not contact a public network, require private keys, replace any production code, or require an attachment. The single patch file adds only two regression tests to the repository's existing pallets/wormhole/src/tests.rs test module:
a real-proof test that executes the committed Plonky2 proof in
pallets/wormhole/test-data/private_batch.hex; anda whole-quantum control that exercises the same shared
process_exit_bundlesettlement routine with an exactly representable0.01 QUANminer fee.
Expected result
The test output and assertions establish that:
the repository's authentic private-batch proof verifies and settles normally;
the correct QPoW-derived author receives the exact
5,000,000,000-Planck raw balance andMinerVolumeFeePaidevent;the real user exit inserts one leaf, proving that the ZK tree is active;
the author's
TransferCountremains zero and no newly inserted leaf belongs to the author;the whole-quantum control credits exactly
10,000,000,000Planck (0.01 QUAN) and still creates no author leaf; andtherefore the missing leaf is caused by the omitted recorder call, not by sub-quantum rounding.
The first test uses the real verifier and committed proof fixture. The second deliberately calls only the exact shared post-verification settlement function so that amount quantization is isolated as a control; it neither mocks nor replaces the vulnerable miner-credit code.
Complete single-file PoC — poc-miner-volume-fee-missing-leaf.patch
Copy this file exactly as shown:
Was this helpful?