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

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 “without record_transfer,” but defined the problem as indexer drift and changed only event emission. This PoC successfully observes the added MinerVolumeFeePaid event 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-03 concerned 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-05 concerned 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 QUAN quantum and still creates no leaf. The missing-recorder defect therefore exists independently of amount quantization.

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:

1

The users' proof is verified and their input nullifiers are consumed.

2

The author's correct Wormhole account receives the miner share in its raw free balance.

3

MinerVolumeFeePaid reports that yield as successfully paid.

4

No transfer count or Merkle leaf assigns the yield to the author.

5

The Wormhole secret cannot authorize a normal ML-DSA transfer, and the miner cannot produce membership for a nonexistent leaf.

6

Therefore 100% of that credited fee yield remains indefinitely frozen in the keyless account.

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; and

  • a whole-quantum control that exercises the same shared process_exit_bundle settlement routine with an exactly representable 0.01 QUAN miner fee.

1

Clone the scoped asset and check out the frozen commit

2

Create the patch file

Create this one file in the repository root and paste the complete unified diff from the end of this section into it:

3

Apply the patch and run the PoC tests

--test-threads=1 only makes the printed evidence deterministic. It does not change runtime state or either assertion path. The repository's pinned Rust 1.93.0 toolchain is selected automatically.

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 and MinerVolumeFeePaid event;

  • the real user exit inserts one leaf, proving that the ZK tree is active;

  • the author's TransferCount remains zero and no newly inserted leaf belongs to the author;

  • the whole-quantum control credits exactly 10,000,000,000 Planck (0.01 QUAN) and still creates no author leaf; and

  • therefore 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?