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

90496 bc low one 1 000 quan balance advertises 1 838 500 quan of unreserved tips to monopolize each targeted node s future queue

Submitted on Aug 25th 2026 at 09:57:09 UTC by @tinnohofficial for Audit Comp | Quantus

  • Report ID: #90496

  • Report Type: Blockchain/DLT

  • Report severity: Low

  • Target: https://github.com/immunefi-team/audit-comp-quantus-chain

  • Impacts:

    • Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

Description

Brief/Intro

Quantus ranks nonce-gap transactions by advertised tip after checking only that the signer could pay one transaction. The tip is neither withdrawn nor reserved while the transaction remains in the future queue.

One ordinary account holding 1,000 QUAN can therefore sign all 3,677 transactions needed to fill a default future queue, with nonces 1..=3677 and a 500-QUAN tip on each. This creates 1,838,500 QUAN of aggregate advertised tips per node from the same unchanged 1,000-QUAN balance. No tip or fee is paid while nonce 0 is withheld.

Quantus's priority-first capacity policy then deterministically evicts every newer lower-priority future transaction, returns ImmediatelyDropped, and bans its hash for 30 minutes. The default pool does not revalidate future entries, and the immortal attack set effectively never expires.

The same approximately 19.7 MB signed set can be reused against every reachable node without new signatures or capital. The official SDK configures three RPC URLs, exposes no tip control, hardcodes zero tip in every software and hardware transaction-construction path, and retries the exact same signed bytes. The measured official-wallet victim transfer has priority 736, while a 0.001-QUAN attacker tip receives 1,000,000,001 times that priority. Official-wallet users cannot attach a competing tip without leaving the official client and constructing custom extrinsics. Any unprivileged user with one funded account can therefore establish persistent censorship of affected official-wallet future transactions on each targeted RPC or processing node.

Vulnerability Details

Default future capacity is 3,677 entries

client/cli/src/params/transaction_pool_params.rs:45-55 sets the default full-pool count to 36,772 and size to 262,144 KiB. client/transaction-pool/src/builder.rs:71-81 assigns one tenth to future transactions: 3,677 entries and 26,843,545 bytes. The PoC's 3,677 signed transactions consume at most 19,657,242 bytes, so the count limit binds first.

Validation assigns tip priority without consuming or reserving the tip

pallets/transaction-payment/src/lib.rs:836-929,1003-1045 checks whether the signer can withdraw fee + tip and calculates priority from tip + 1 during validation:

Withdrawal occurs only later in prepare. A nonce-gap transaction does not reach that stage, so all 3,677 validations reuse the same balance. The runtime PoC signs every nonce with genuine ML-DSA and the production extension tuple; the account remains at nonce zero with its complete 1,000-QUAN balance.

The advertised tip is an attacker-selected barrier, not a fixed 500-QUAN cost

The PoC uses 500 QUAN to demonstrate a high censorship barrier while leaving fee margin. The 5,346-byte transaction receives:

The attacker chooses this barrier. The PoC directly measures a 0.001-QUAN tip at priority 736000000736, exactly 1,000,000,001 times the measured zero-tip victim priority 736. substrate_service.dart:238,265,282,316 hardcodes tip: 0 in the software signing payload, signed extrinsic, fee-estimation extrinsic, and hardware signing payload, while mobile-app/lib contains no tip setting or override. Priority can vary with transaction length, weight, and class, but the official app gives users no way to attach the competing tip required by this eviction policy. Matching Mallory's chosen barrier therefore requires hand-building extrinsics outside the official client. Mallory pays no advertised tip while preserving the nonce gap; a victim's competing tip becomes payable if their transaction becomes ready and executes.

The Quantus future policy protects the highest advertised priorities

client/transaction-pool/src/graph/base_pool.rs:530-568 selects the lowest-priority future entry whenever the limit is exceeded. Age only breaks equal-priority ties, with the older entry evicted:

client/transaction-pool/src/graph/validated_pool.rs:296-329,396-423 then converts eviction of the incoming transaction into ImmediatelyDropped and bans every limit-evicted hash. The default ban is 1,800 seconds (client/transaction-pool/src/builder.rs:76-81).

One reusable set can target the configured RPC fleet

Future transactions are not automatically propagated because client/service/src/lib.rs:475-492 exports only pool.ready(). However, RPC submission and direct peer import are normal external ingress; the transaction protocol forwards received bytes to submit_one(..., TransactionSource::External, ...) at lines 518-548.

The attacker therefore submits the same immortal, genesis-bound 19.7 MB set to each target. No new signatures or account balance are needed per node. The official SDK ships three RPC URLs (app_constants.dart:14-19) and independently performs nonce lookup and submission through its redundant endpoint service. Failover can submit a nonce derived from one node's pending pool to another node where its predecessor is absent, creating exactly the affected future transaction.

The SDK then amplifies the failure: substrate_service.dart:179-201 signs once and retries the exact same bytes. The first capacity eviction bans that hash; retries encounter the same 30-minute ban, while a freshly signed zero-tip replacement is evicted again.

Admitted entries are not revalidated and effectively never expire

Default single-state maintenance revalidates only ready() hashes (single_state_txpool.rs:807-809). Stale cleanup inspects future entries but only removes expired transactions (validated_pool.rs:721-757); the runtime PoC reports longevity 18446744073709551613. Later affordability changes therefore do not remove the set.

Preserving the pin requires withholding nonce 0. Mallory can end it with a zero-tip current-nonce transfer_all: the PoC proves that only this ordinary exit transaction pays a fee and the promoted high-tip transaction is rejected with Payment, without charging an attack-sized tip.

Attack Scenario

1

Fund and preserve nonce zero

Mallory funds one ordinary account with 1,000 QUAN and keeps its on-chain nonce at zero.

2

Sign the future set

Mallory signs one immortal set of 3,677 transfers with nonces 1..=3677 and a 500-QUAN tip each.

3

Submit to target nodes

Mallory submits the same set to each targeted RPC or processing node. Every node accepts all entries against the same unchanged balance.

4

Fill the future queue

Each target's complete future queue is now protected by priority 367500000000000735, representing 1,838,500 QUAN of aggregate advertised tips that were never reserved or paid.

5

Submit a lower-priority victim transaction

Alice submits a zero-tip transaction whose predecessor is absent on that node, including through ordinary endpoint failover or out-of-order sequential submission.

6

Evict and ban the victim transaction

The pool evicts Alice's runtime-valid transaction, returns ImmediatelyDropped, and bans its hash for 30 minutes. The official SDK retries the same banned bytes.

7

Maintain persistent denial

Every later lower-priority future transaction is rejected without further attacker traffic. The same attack set remains reusable after a pool restart.

Quantus-specific implementation

The vulnerable priority-first future policy is a Quantus modification, not an unmodified upstream report. Commit 8f4e0d5fbcddc8299ad13ccc10840044e3e702f2 changed future eviction from oldest-first in parent e362a8776e883b598d8966695d8b02ca1ed178f9 to lowest-priority-first. The change was intended to stop low-priority flooding, but combining it with reusable, unreserved tip priority lets one funded signer invert the defense and protect an entire nonce-gap set.

This report concerns the interaction between that Quantus eviction change, runtime validation that assigns priority before tip withdrawal, and the absence of per-signer future-queue limits. The PoCs exercise that interaction in the in-scope Quantus runtime and Quantus-modified transaction pool at the frozen revision.

Impact Details

One unprivileged account can occupy all 3,677 future-transaction slots on each targeted node and deny every lower-priority transaction in that class, including sequential withdrawals and batched payouts whose predecessor is pending elsewhere. Admission and eviction are automatic; no validator, node operator, governance actor, or victim cooperation is required. Victims receive terminal drops and repeated retry failures despite submitting runtime-valid transactions.

One 1,000-QUAN balance backs 1,838,500 QUAN of advertised tips per node, and the same balance and signatures work across targets. Mallory sends the bounded set once per node, pays no admission fee or tip, and needs no continuing traffic for practical node lifetime. Capital is tied only while preserving the pin; the tested unwind pays one ordinary transaction fee rather than an advertised 500-QUAN attack tip.

Within the targeted class the denial is complete and persistent: every lower-priority future transaction is evicted, the official client exposes no tip control to cross the attacker-selected barrier, and the attack entries are not revalidated and effectively never expire. Ready transactions are unaffected, each node must be reached directly, a custom client can displace entries by matching the attacker's tip, and an operator can clear the pool, though the still-valid set can refill it immediately. Those boundaries support the selected Medium - Griefing impact while leaving the complete per-node denial intact.

1

Cap nonce-gap entries per signer

Cap nonce-gap entries per signer before applying the global limit.

2

Account for cumulative exposure

Rank a signer's future set by cumulative affordable/reserved exposure, not independently reusable tips.

3

Age and expire future priority

Age future priority and impose a pool lifetime independent of immortal runtime longevity.

4

Avoid banning capacity-evicted valid transactions

Do not ban valid transactions solely because capacity enforcement selected them for eviction.

References

  • client/cli/src/params/transaction_pool_params.rs:45-55

  • client/transaction-pool/src/builder.rs:56-84

  • client/transaction-pool/src/graph/base_pool.rs:530-568

  • client/transaction-pool/src/graph/base_pool.rs:344-400,625-672

  • client/transaction-pool/src/graph/pool.rs:428-496

  • client/transaction-pool/src/graph/validated_pool.rs:296-329,396-423

  • client/transaction-pool/src/graph/validated_pool.rs:721-757

  • client/transaction-pool/src/single_state_txpool/single_state_txpool.rs:807-809

  • client/service/src/lib.rs:475-492,518-548

  • pallets/frame-system/src/extensions/check_nonce.rs:70-99

  • pallets/balances/src/lib.rs:716-752

  • pallets/transaction-payment/src/lib.rs:836-929,1003-1045

  • quantus_sdk/lib/src/constants/app_constants.dart:14-19

  • quantus_sdk/lib/src/services/network/redundant_endpoint.dart:71-95

  • quantus_sdk/lib/src/services/substrate_service.dart:117-124,179-201,213-283,307-354

Proof of Concept

Two complementary tests demonstrate the exploit and its exact boundary without sending any network traffic:

  • production-runtime tests sign and validate all 3,677 high-tip nonce-gap transactions against the same unchanged account state, measure the 0.001-QUAN priority, and prove a zero-tip transfer_all unwind does not charge the attack tip;

  • a real Quantus transaction-pool test proves lower-priority eviction and banning, then proves that a later equal-priority victim displaces the older attacker.

Setup

From audit-comp-quantus-chain at frozen revision 3b243b870a5442a6e5f443056f67bbba87d472f5:

1

Create the runtime PoC

Create runtime/tests/transactions/future_priority_pin.rs with the runtime PoC code below.

2

Register the runtime module

Add the following line to runtime/tests/transactions/mod.rs:

3

Add the pool test

Add the pool helper and test below inside the existing watcher_reclaim_tests module in client/transaction-pool/src/graph/validated_pool.rs.

Run

Production runtime validity, balance reuse, sizing, priority, and unwind economics:

Actual Quantus eviction, ban, and equal-priority control:

Observed Output

Runtime test:

Pool test:

The one-entry pool limit is a safe deterministic abstraction of the production 3,677-entry limit. The runtime test independently proves that the production count can be filled by one account.

PoC Code: Production Runtime Test

Place this code in runtime/tests/transactions/future_priority_pin.rs:

PoC Code: Quantus Pool Eviction and Ban Test

Add this helper and test inside mod watcher_reclaim_tests in client/transaction-pool/src/graph/validated_pool.rs:

Was this helpful?