89163 bc high miner volume fee is reported as paid but records no zk leaf so the credited fee is unrealizable by the miner
Submitted on Aug 17th 2026 at 15:30:34 UTC by @hunter67 for Audit Comp | Quantus
Report ID: #89163
Report Type: Blockchain/DLT
Report severity: High
Target: https://github.com/immunefi-team/audit-comp-quantus-chain
Impacts:
Permanent freezing of funds
Description
Selected impact: Permanent freezing of funds (Blockchain/DLT). Not the (fix requires hardfork) variant: Quantus is Substrate, so a forkless runtime upgrade fixes this, and a migration could record leaves for balances already stranded.
Affected source: github.com/immunefi-team/audit-comp-quantus-chain @ 3b243b870a5442a6e5f443056f67bbba87d472f5, branch audit-comp-ready.
Summary
When a settled wormhole exit bundle successfully credits the block author's share of the volume fee, pallet-wormhole credits it with Unbalanced::increase_balance and emits MinerVolumeFeePaid, so the protocol reports the miner as paid. (Where no author is found, or the credit returns Err, the pallet burns that share instead — this report concerns only the successful credit.)
The block author's address is not an ordinary account, and the miner cannot choose otherwise. The project's own MINING.md states it plainly: "Mining rewards are automatically sent to wormhole addresses derived from your preimage… The system derives your wormhole address using Poseidon hashing", and the miner supplies only that preimage (--rewards-inner-hash). extract_author_from_digest performs exactly that derivation, so whenever a fee is successfully credited the recipient is necessarily a wormhole-derived address.
That address class cannot be spent by signature — DilithiumSignatureScheme::verify requires hash_bytes(pubkey) == signer, so signing as a Poseidon output would require a preimage attack — and the circuit crate names the class UnspendableAccount. Its only non-privileged spend path available to the miner is a ZK exit proved against a leaf, which docs/wormhole-zk.md confirms: "One leaf proof = one user exit from a wormhole address."
The miner therefore cannot avoid this by nominating an ordinary account: the node derives the payout address from the supplied preimage, and the derived address has no signing key.
The fee credit records no leaf. Unlike the exit credit fifty lines earlier in the same function, and unlike pallet_mining_rewards::mint_reward which pays the same address class, the volume-fee branch never calls record_transfer. Because a wormhole exit mints against a leaf rather than debiting the source account, an address's realizable value is the sum of its leaves — so the credited fee raises a balance that the miner has no way to spend or withdraw.
No attacker is involved and no attacker profits. This occurs under ordinary honest operation: a miner produces blocks, an aggregator settles a valid batch.
Root cause
pallets/wormhole/src/lib.rs, process_exit_bundle:
Exit credit (correct):
increase_balance(exit_account, exit_balance)immediately followed byrecord_transfer(asset, mint_account, exit_account, exit_balance).Miner-fee credit (defective):
increase_balance(author, miner_fee), thendeposit_event(MinerVolumeFeePaid { .. }), and nothing else. Norecord_transfer.
pallets/mining-rewards/src/lib.rs, mint_reward, paying the same wormhole-derived miner address, does call record_transfer_proof(None, mint_account, miner, reward) — the codebase's own control case for how this address class must be paid.
Why the credited value cannot be recovered by the miner
Can the author sign a transaction from that address?
No. Signature verification requires an ML-DSA public key whose account hash equals the address; the address is a Poseidon output, so this is a preimage problem.
Can a later recorder or sweep create a leaf for the already-credited balance?
No. record_transfer commits the credit amount passed to it together with (canonical_recipient, transfer_count); it never reads the account's balance. The only balance-wide sweep is the genesis hook at block 1, which cannot run again.
Does the miner's next mining-reward leaf cover it?
No. That leaf commits the reward amount only.
Impact and magnitude
Per settled bundle the lost amount is the miner's share of the volume fee: miner_fee = total_fee − burn, with total_fee = ceil(exit_quanta · VolumeFeeRateBps / (10000 − VolumeFeeRateBps)) quanta and a 50% burn rate, at VolumeFeeRateBps = 4 and a quantum of 10^10 planck. That is roughly 0.02% of wormhole exit volume, credited to the miner but unrealizable by the miner under the current protocol, accumulating across settled exit bundles.
This is the miner's fee income rather than user deposits. The defect is deterministic and accumulates across settled bundles, but the amount per bundle is small; this report does not rely on a large per-event loss claim.
What this report does not claim
Not a theft. Nothing is stolen; no party gains the lost value.
Not "all funds at wormhole addresses are inaccessible". Deposits and mining rewards each receive their own leaf and remain exitable. The defect is confined to the volume-fee credit.
Not a claim that no privileged or future path could ever move the balance — Root retains
force_set_balance. The claim is that the miner cannot realize a fee the protocol reports as paid.Not demonstrated end-to-end: a runtime-level test showing the miner realizing a mining reward
Rwhile the feeFstays stranded was not run. The pallet-level test demonstrates the mechanism; the unrealizability argument is the static chain above.
Known-issue distinction
The program declares as a known issue: "future zk-tree depth and mining reward scaling limitations requiring runtime upgrades". This report is not that issue. That known issue concerns capacity/scaling ceilings needing a future upgrade; this is a missing bookkeeping call on one credit path, present and effective today at current parameters, and independent of tree depth or reward scaling.
Suggested fix
In the miner-fee branch of process_exit_bundle, mirror the exit-credit branch and the mining-rewards path: after a successful increase_balance for the author, call record_transfer for the same amount, so the credit is committed to the zk-tree and becomes exitable by the miner.
Proof of Concept
Self-contained, reproduced from the frozen competition source. No private-repository dependency, no node build and no wasm build are needed.
The patch adds one test to pallets/wormhole/src/tests.rs and changes no production file (1 file changed, 124 insertions(+)). Observed result on the workspace-pinned toolchain 1.93.0 (rust-toolchain, not the host default):
What the test asserts
Assertions 1 and 2 together are the finding: the balance rises while the bookkeeping that makes a wormhole-derived balance spendable does not.
The test deliberately contrasts with the existing test_verify_private_batch_emits_miner_volume_fee_paid, whose assertions stop at free_balance and therefore cannot distinguish "paid" from "credited into an address that can never spend it".
Artifact integrity
H-A11-miner-fee-no-leaf.patch — inlined verbatim below
0ecb65ed8bb7492ae640e3c302c0c049fd5a6fde2cf7650ec1de20390bef1ff5
Full captured run log
48adaf011cdae0a7a598dd45dbd99149bef497e2f194d19d2bf931abf9bf7f3a
The decisive lines of the run log are quoted above. The full log is not reproduced here because it contains local host paths; it can be provided on request. Applying the patch below to the pinned SHA reproduces the result independently of that log.
The patch
Scope of the demonstration
The test is pallet-level and demonstrates the mechanism — credit without leaf. It does not attempt an end-to-end runtime demonstration of a miner realizing a mining reward while the fee stays stranded; that argument rests on the static chain given in the description (no signature path for a Poseidon-derived address, no later recorder or sweep that reads balances, and no balance-covering leaf), not on execution.
Was this helpful?