90377 bc low stale transaction payment benchmark omits quantus fee collector storage undercharging the extension by 48 12 in the measured run
Submitted on Aug 24th 2026 at 18:21:54 UTC by @Pig46940 for Audit Comp | Quantus
Report ID: #90377
Report Type: Blockchain/DLT
Report severity: Low
Target: https://github.com/immunefi-team/audit-comp-quantus-chain
Impacts:
Inaccuracies to the weight (similar to gas) accounting system that under or overcharge by less than 50% of the correct amount (and are not intentional as indicated by the program's terms
Description
Brief/Intro
Quantus configures TransactionFeesCollector as the fee handler used by ChargeTransactionPayment, but continues to use a transaction-payment weight generated against the upstream Kitchensink runtime. That checked-in weight does not include the MiningRewards::CollectedFees storage accessed by the Quantus fee collector. A benchmark-enabled Quantus runtime built from the pinned production configuration recorded this additional storage and measured a transaction-payment extension weight of 310,318,000 ref-time, while the runtime declares only 160,979,000. This is a 48.1245% undercharge in the preserved benchmark run.
Vulnerability Details
A normal fee-paying signed extrinsic reaches the Quantus collector through the following path:
ChargeTransactionPayment::post_dispatch_details
→ OnChargeTransaction::correct_and_deposit_fee
→ TransactionFeesCollector::on_nonzero_unbalanced
→ collect_transaction_fees
→ CollectedFees::mutate
→ FeesCollected eventThe production configuration selects the Quantus collector while continuing to use the checked-in transaction-payment WeightInfo:
The checked-in transaction-payment weight says it was generated using kitchensink_runtime.wasm. Its storage ledger contains only:
Under Quantus’s configured RocksDbWeight, the checked-in ref-time is:
I rebuilt the pinned Quantus runtime with the runtime-benchmarks feature and ran the dedicated transaction-payment benchmark using compiled-Wasm execution, 50 steps, and 20 repeats.
The preserved benchmark run generated this storage ledger:
Its generated ref-time formula was:
The additional MiningRewards::CollectedFees read and write account for 125,000,000 ref-time under the configured database schedule. The generated benchmark also measured additional computation beyond those storage terms.
The difference observed in the preserved benchmark run is:
The precise benchmark computation term can vary with the benchmark host. The reproducible structural result is that the Quantus benchmark adds MiningRewards::CollectedFees (r:1 w:1), reads(2), and writes(2), while the checked-in formula contains only System::Account (r:1 w:1), reads(1), and writes(1).
ChargeTransactionPayment::weight() returns this checked-in transaction-payment weight as the extension’s declared weight. During post-dispatch, the same extension invokes the configured OnChargeTransaction, which reaches the omitted Quantus collector storage.
A separate lifecycle test confirms that:
A normal signed
System::remarkcan reach the production fee collector.The extension declares
160,979,000ref-time.MiningRewards::CollectedFeesincreases.The
FeesCollectedandTransactionFeePaidevents are emitted.Post-dispatch returns zero unspent weight.
That lifecycle test directly invokes the extension lifecycle and does not execute the extrinsic through FRAME Executive. It is therefore used only to establish production callback reachability and state mutation. The numerical discrepancy is established by the dedicated compiled-Wasm benchmark and its generated storage ledger.
The appropriate fix is to regenerate pallet_transaction_payment weights using Quantus’s configured fee collector, or to compose the collector’s weight into ChargeTransactionPayment exactly once.
Impact Details
The selected Blockchain/DLT impact is:
Inaccuracies to the weight (similar to gas) accounting system that under or overcharge by less than 50% of the correct amount (and are not intentional as indicated by the program’s terms).
Severity: Low
In the preserved benchmark run, the transaction-payment extension’s declared ref-time is 160,979,000, while the benchmark-generated ref-time is 310,318,000. The resulting discrepancy is 48.124504540504%, which is below the program’s 50% Medium threshold.
Any funded account can reach the affected callback by submitting an ordinary fee-paying signed extrinsic. The omitted work is therefore executed repeatedly in normal production transaction processing.
This report does not claim network shutdown, liveness failure, theft, asset loss, or Medium severity.
Report #88529 concerns ML-DSA verification omitted from ExtrinsicBaseWeight. This report concerns a different accounting component: the weight returned by ChargeTransactionPayment::weight() and the Quantus-specific fee-collector storage executed during its post-dispatch lifecycle.
A reviewer may nevertheless decide to group both reports as stale fixed-cost benchmarking issues or calculate the discrepancy against a larger complete-extrinsic denominator. This report therefore limits its claim to the dedicated transaction-payment extension formula measured by the provided benchmark.
References
Production transaction-payment configuration, lines 451–464:
https://github.com/immunefi-team/audit-comp-quantus-chain/blob/3b243b870a5442a6e5f443056f67bbba87d472f5/runtime/src/configs/mod.rs#L451-L464
Transaction-payment benchmark, lines 45–90:
https://github.com/immunefi-team/audit-comp-quantus-chain/blob/3b243b870a5442a6e5f443056f67bbba87d472f5/pallets/transaction-payment/src/benchmarking.rs#L45-L90
Transaction-payment extension lifecycle and weight consumer, lines 990–1080:
https://github.com/immunefi-team/audit-comp-quantus-chain/blob/3b243b870a5442a6e5f443056f67bbba87d472f5/pallets/transaction-payment/src/lib.rs#L990-L1080
Checked-in Kitchensink-generated weight, lines 35–91:
https://github.com/immunefi-team/audit-comp-quantus-chain/blob/3b243b870a5442a6e5f443056f67bbba87d472f5/pallets/transaction-payment/src/weights.rs#L35-L91
CollectedFees storage, lines 35–38:
https://github.com/immunefi-team/audit-comp-quantus-chain/blob/3b243b870a5442a6e5f443056f67bbba87d472f5/pallets/mining-rewards/src/lib.rs#L35-L38
collect_transaction_fees, lines 195–203:
https://github.com/immunefi-team/audit-comp-quantus-chain/blob/3b243b870a5442a6e5f443056f67bbba87d472f5/pallets/mining-rewards/src/lib.rs#L195-L203
TransactionFeesCollector::on_nonzero_unbalanced, lines 292–301:
https://github.com/immunefi-team/audit-comp-quantus-chain/blob/3b243b870a5442a6e5f443056f67bbba87d472f5/pallets/mining-rewards/src/lib.rs#L292-L301
Configured RocksDB weight schedule, lines 23–29:
https://github.com/immunefi-team/audit-comp-quantus-chain/blob/3b243b870a5442a6e5f443056f67bbba87d472f5/frame/support/src/weights/rocksdb_weights.rs#L23-L29
Link to Proof of Concept
https://gist.github.com/HiroyukiNaito/39deea6b07a58a607148d58b80155835
Proof of Concept
Minimal evidence Gist:
Run the lifecycle test
First confirm the test name:
Run only the researcher test:
The preserved successful run included:
The lifecycle test establishes that the production transaction-payment configuration reaches TransactionFeesCollector and mutates CollectedFees during post-dispatch.
The benchmark establishes that this storage appears in the generated transaction-payment weight but is absent from the checked-in weight consumed by the runtime.
Was this helpful?