89091 bc medium quantus adopts an sr25519 calibrated base weight for ml dsa signatures undercharging signed extrinsics by 79 58
Submitted on Aug 17th 2026 at 04:52:09 UTC by @Ailenia for Audit Comp | Quantus
Report ID: #89091
Report Type: Blockchain/DLT
Report severity: Medium
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 more than 50% of the correct amount (and are not intentional as indicated by the program's terms)
Description
Brief/Intro
The Quantus runtime uses compiled-Wasm ML-DSA signature verification but retains Substrate's default ExtrinsicBaseWeight, which was measured for a conventional development runtime. Quantus's own canonical overhead benchmark constructs a signed empty System::remark with ML-DSA-87. Executing that benchmark against the frozen runtime measures 2,534,781 ns of per-extrinsic base overhead while the shipped runtime charges only 108,157 ns. Even crediting every other declared weight term in full, the complete signed remark is undercharged by 79.58%.
Vulnerability Details
I looked at the frozen commit 3b243b870a5442a6e5f443056f67bbba87d472f5:
runtime/src/configs/mod.rs:86-89constructsRuntimeBlockWeightswithBlockWeights::with_sensible_defaults.The derived
frame_system::Configthen adopts that value as its block-weight configuration.with_sensible_defaultsuses the inheritedExtrinsicBaseWeight, whose checked-in calibration is 108,157 ns:
Quantus changed the signed-extrinsic cryptography without replacing this calibration.
The runtime accepts both ML-DSA-65 and ML-DSA-87, and ML-DSA verification executes inside the Wasm runtime rather than through a separately charged host function.
An attacker can use an ordinary ML-DSA-87 account and submit valid signed calls; no privilege or invalid transaction is required.
The repository already contains the correct canonical measurement path:
node/src/benchmarking.rs:22-59definesRemarkBuilder.Uses
dilithium_bob().Constructs an empty
System::remark.Signs the complete frozen
UncheckedExtrinsicasSignature::Dilithium87.
Running quantus-node benchmark overhead with compiled Wasm, 10 warmups, and 100 repeats produced:
The generated replacement is 2,534,781 ns, or 23.436 times the shipped 108,157 ns.
The cheapest signed empty remark otherwise declares 622,787 ns, comprising the old 108,157-ns base plus 514,630 ns of call and transaction-extension weight.
Replacing only the miscalibrated base yields a conservative correct total of 3,049,411 ns.
So, the shipped 622,787-ns total undercharges the complete extrinsic by (3,049,411 - 622,787) / 3,049,411 = 79.58%.
The measurement uses the frozen runtime's real signed extrinsic, transaction extensions, ML-DSA implementation, Poseidon hashing, and compiled-Wasm execution. The attached measurement-only patch merely registers the benchmark host functions that the frozen node's existing benchmark command omitted; it does not change the runtime, signature builder, extrinsic, weight constant, or measured code path.
Impact Details
This matches the very specific Medium impact "Inaccuracies to the weight (similar to gas) accounting system that under or overcharge by more than 50% of the correct amount (and are not intentional)."
A valid ML-DSA-87 signed extrinsic is charged 79.58% less than its conservative (and correct) complete-extrinsic weight.
This allows ordinary users to consume substantially more block-validation work than the runtime's weight accounting records. I unfortunately cannot claim the High 500%-block-delay impact because the 5-MiB block-length limit independently bounds the number of large ML-DSA-87 extrinsics.
Remediation
Generate Quantus-specific block and extrinsic overhead weights with the production signature schemes and compiled-Wasm execution, replace the inherited base constants with those generated values, and include benchmark overhead in every runtime weight-regeneration release gate.
Benchmark both ML-DSA variants and charge the worst supported scheme unless the signature variant has an explicit separate weight.
Update the generated sanity test's legacy one-millisecond ceiling so it does not reject the measured production value.
References
Link to Proof of Concept
https://gist.github.com/niafreu/12227631669dc94f6e99c706b6438a10
Proof of Concept
The attached PoC clones the frozen competition revision, applies a measurement-only host-function registration, builds the existing runtime-benchmark node, runs its existing Dilithium-87 overhead benchmark under compiled Wasm, and fails unless the measured base exceeds the shipped value by more than 50% of the correct value.
Requirements: Git, Rust/Cargo 1.93.0, Clang/libclang, the normal Quantus native build dependencies, and a local checkout at commit 3b243b870a5442a6e5f443056f67bbba87d472f5. A cold build can take approximately 15 minutes because the Wormhole circuit artifacts are generated.
Code
run.sh
benchmark-host-functions.patch
extrinsic_weights.rs
Was this helpful?