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

90133 bc high mining rewards are minted at raw precision but their zk leaves commit only floor amount 0 01 quan permanently freezing every remainder and making ordinary fee yield wholly unc

Submitted on Aug 23rd 2026 at 07:21:23 UTC by @tinnohofficial for Audit Comp | Quantus

  • Report ID: #90133

  • 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

QPoW miners are paid to Poseidon-derived wormhole addresses. That derivation produces no ML-DSA keypair, so a miner has no signing key and the only way to spend a credit is to prove the ZK-tree leaf that commits it.

MiningRewards::on_finalize mints transaction fees and the block reward as two separate, full-precision balance credits and records a leaf for each. The leaf hash commits only:

floor(raw_amount / 10_000_000_000)      // 10^10 planck = 0.01 QUAN

The remainder exists in the miner's keyless account and in no proof the miner can ever construct. Two consequences follow, both automatic and both recurring:

  1. An ordinary block's entire fee yield can be unclaimable. A real ML-DSA-65 transfer_keep_alive pays 10,994,787,000 planck in fee. That floors to one quantum, and at the configured 4 bps wormhole rate a one-quantum leaf permits zero output. The miner earned real, minted, on-chain value with no positive claim against it.

  2. Every non-aligned block reward strands its remainder forever. The integer emission formula is almost never quantum-aligned, so miner_reward mod 10^10 is lost on essentially every block.

Vulnerability Details

The miner cannot sign; the leaf is the only spend path

extract_author_from_digest hashes the miner-supplied preimage with Poseidon and decodes the image as the reward account:

primitives/wormhole/src/lib.rs:113-150. A Poseidon image is not an ML-DSA public key, so no ordinary signed transfer is possible from it.

Fees and block rewards are minted raw, and recorded raw

Fees are routed into the pallet by the production payment adapter (runtime/src/configs/mod.rs:477-480) and accumulated in CollectedFees (pallets/mining-rewards/src/lib.rs:309-318). At finalization they are paid out as two separate credits:

pallets/mining-rewards/src/lib.rs:192-196. Each call mints the full raw amount and records that same raw value as an independent proof:

pallets/mining-rewards/src/lib.rs:220-241. There is no quantum alignment and no minimum-payout check on either call.

The authoritative leaf hash floors the amount

The tree stores the raw u128, but the root hashes a quantized u32:

pallets/zk-tree/src/tree.rs:30-34,134-149. Leaves whose raw amounts differ only below 10^10 planck hash identically.

A one-quantum leaf has zero output capacity

Every leaf proof independently enforces:

qp-zk-circuits/wormhole/circuit/src/zk_merkle_proof.rs:491-499, where input is that leaf's own quantized amount. With VolumeFeeRateBps = 4 and input = 1, a single output quantum would require 1 * 10000 <= 1 * 9996, which is false. Maximum output is zero.

On chain, an exit scales only the proven integer output back to planck:

pallets/wormhole/src/lib.rs:776-806. Nothing reads the account's free balance to derive exit capacity.

No later proof can recover a remainder

Aggregation sums valid outputs; it cannot move an omitted remainder into another leaf or let a leaf exceed its own bound. The omitted raw mod 10^10 is absent from the first proof and from every possible subsequent proof, so choosing a smaller output and "redeeming the rest later" cannot reach it.

Impact Details

The frozen value is protocol-generated miner yield credited to an account that cannot sign, not a user's voluntary dust transfer.

Wholly unclaimable fee yield. Any block whose total fees floor to exactly one quantum produces a leaf with zero output capacity — a 100% loss of that credit. The PoC demonstrates it with a single ordinary signed transfer through the real Executive::apply_extrinsic pipeline.

Permanently stranded block-reward remainders. Running the frozen runtime's own integer emission equations forward for 10,519,200 blocks (four years at the 12 s target):

yields 3,501,001.704268688139 QUAN of miner yield against 52,302.704268688139 QUAN of uncommitted remainder — approximately 1.49% of miner block-reward yield omitted from the only commitments a miner can spend, before counting fee credits. The percentage is a property of the emission formula; the absolute figure depends on the starting supply used to seed the projection.

Recovery of already-stranded balances requires a runtime migration or another privileged mechanism. This matches High - Permanent freezing of unclaimed yield.

Why This Is Not a Known Issue or Duplicate

The runtime guards vesting against this exact hazard, and not mining

The team identified this failure mode and defended the vesting pallet against it:

runtime/src/configs/mod.rs:562-580. The pallet states the reason on the config item itself — "a payout below one quantum would create a zero-value leaf: funds moved to a keyless beneficiary would be irrecoverable" (pallets/vesting/src/lib.rs:147-157) — and enforces it as a hard construction-time assertion:

pallets/vesting/src/lib.rs:295-310. The two-quanta floor is exactly the zero-output condition demonstrated above: one quantum cannot produce a positive output at a nonzero fee rate, so the minimum must contain two.

Mining rewards pay the same kind of keyless beneficiary through the chain's primary issuance path and apply neither control — no quantum alignment, no minimum, no assertion.

This establishes the intended producer-side invariant. It does not disclose or accept the MiningRewards violation of it; it is evidence that the violation is unintended.

EQ-QNT-DOC-F-01 is a different mechanism

The closest published finding concerns QIP-5's false claim that a user can partially withdraw one existing leaf and redeem the remainder later. Its example withdraws 60 from a 100-unit leaf and explains that the remaining 40 is unreachable because the first proof consumed the leaf's nullifier. Its recommendation is to correct the documentation.

That is a user-chosen partial spend of a correctly committed leaf, resolved at spend time by nullifier consumption. This report is a protocol-created credit that is under-committed at leaf creation: the value never enters any proof, so no choice of output can recover it, and the one-quantum case has no valid positive output at all. Different trigger, different actor, different fix.

EQ-QNT-WORMHOLE-F-03 is also distinct — it covers decoding the miner preimage as an account instead of deriving its Poseidon address, and that derivation is correct in the scoped commit.

Distinct from the miner-volume-fee report

A separate report covers two process_exit_bundle credit paths in pallets/wormhole that create no leaf at all. Here pallets/mining-rewards does create leaves and does advance TransferCount; the defect is that each leaf commits less than was minted, and small fee leaves have zero output capacity. Adding the missing record_transfer calls to process_exit_bundle does not touch this path, and quantum-aligning mining payouts does not fix that one.

Not a post-audit regression

The payout path and the two-decimal amount representation both predate the March 2026 review, so this is not a claim of new code slipping in after an audit — it is a defect that was reviewable at the time and is absent from the published findings.

Do not mint to a keyless miner an amount that cannot be committed and exited.

1

Combine fees and rewards

Combine tx_fees and miner_reward into a single payout before crediting.

2

Carry pending remainder

Carry the sub-quantum difference in a per-miner pending remainder instead of minting it.

3

Emit only claimable credits

Emit only a quantum-aligned credit whose leaf has positive output capacity; withhold a payout that would produce a one-quantum leaf at a nonzero fee rate.

4

Migrate stranded balances

Migrate already-stranded balances into claimable, quantum-aligned leaves.

Minimum invariants to enforce before mint_into:

References

  • primitives/wormhole/src/lib.rs:97-150 - keyless miner reward-address derivation.

  • runtime/src/configs/mod.rs:138-147 - production MiningRewards wiring.

  • runtime/src/configs/mod.rs:477-480 - transaction fees routed to MiningRewards.

  • pallets/mining-rewards/src/lib.rs:129-199 - reward calculation and separate fee/reward payouts.

  • pallets/mining-rewards/src/lib.rs:220-241 - raw mint followed by proof recording.

  • pallets/mining-rewards/src/lib.rs:309-318 - fee collector.

  • pallets/wormhole/src/lib.rs:1280-1310 - one independent leaf per recorded credit.

  • pallets/zk-tree/src/tree.rs:30-34,134-149 - flooring in the authoritative leaf hash.

  • qp-zk-circuits/wormhole/circuit/src/zk_merkle_proof.rs:486-504 - range and per-leaf fee constraints.

  • pallets/wormhole/src/lib.rs:776-806 - output scaled from quantized proof data only.

  • runtime/src/configs/mod.rs:562-580, pallets/vesting/src/lib.rs:147-157,295-310 - vesting's explicit protection against the same hazard.

Proof of Concept

The PoC is a runtime integration test. It drives the production Runtime, TransactionFeesCollector, MiningRewards, Wormhole, ZkTree, the real transaction extensions and the ML-DSA-65 signature pipeline — no mock proof recorder and no simulated fee. It shows, in order:

  • MiningRewards::on_finalize mints a fee credit and a block-reward credit at raw precision and records a separate leaf for each, and each leaf hashes identically to the same leaf with its remainder removed — the raw remainder is not committed;

  • a one-quantum fee leaf has max_output_quanta == 0, so it can never fund a positive exit;

  • an actual ML-DSA-65 Balances::transfer_keep_alive pushed through Executive::apply_extrinsic pays a 10,994,787,000 planck fee that MiningRewards records as exactly that raw value, producing a leaf with zero output capacity;

  • projecting the frozen runtime's own integer emission equations over four years strands 52,302.704268688139 QUAN of miner block-reward remainders.

Run

The PoC is a standalone integration test and needs no edits to production files. From audit-comp-quantus-chain at frozen commit 3b243b8:

1

Copy the PoC

Copy the PoC to runtime/tests/mining_rewards_quantization.rs.

2

Run the test

Observed Output

Tested against frozen chain commit 3b243b8:

PoC Code

Was this helpful?