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

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-89 constructs RuntimeBlockWeights with BlockWeights::with_sensible_defaults.

  • The derived frame_system::Config then adopts that value as its block-weight configuration.

  • with_sensible_defaults uses the inherited ExtrinsicBaseWeight, 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-59 defines RemarkBuilder.

  • Uses dilithium_bob().

  • Constructs an empty System::remark.

  • Signs the complete frozen UncheckedExtrinsic as Signature::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

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.

1

Run the proof of concept

2

Review the retained run output

The retained full run used 10 warmups and 100 repeats and produced:

execution.log preserves the decisive output and extrinsic_weights.rs is the benchmark-generated raw weight file.

Code

run.sh

benchmark-host-functions.patch

extrinsic_weights.rs

Was this helpful?