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

88329 bc medium missing guardian consent in set high security lets one signer permanently exhaust any guardian s quota and block legitimate enrollments

Submitted on Aug 12th 2026 at 19:09:58 UTC by @ngosytuan for Audit Comp | Quantus

  • Report ID: #88329

  • Report Type: Blockchain/DLT

  • Report severity: Medium

  • 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

Summary

The permissionless ReversibleTransfers::set_high_security dispatchable lets its caller nominate any account as guardian without a signature, acceptance, or other authorization from that guardian. Every nomination immediately consumes one entry in the target guardian's bounded GuardianIndex, whose production capacity is 32 accounts.

A normal account can exploit this in one signed transaction by batching 32 Utility::as_derivative calls. Each derivative supplies a distinct effective signed origin and calls set_high_security with the same target guardian. The derivative accounts need no keys, balances, reserves, holds, existential deposits, or existing System account records.

After the batch succeeds, the target guardian's index contains 32 attacker-controlled pseudonyms. A legitimate funded account that then selects the same guardian is deterministically rejected with TooManyGuardianAccounts. The target guardian cannot repair the poisoned quota: recover_funds only sweeps balances and deliberately preserves both HighSecurityAccounts and GuardianIndex. The PoC confirms that the entry survives even after the pseudonym is funded, recovered, and reaped from System storage.

Affected asset: The Substrate L1 node/runtime Affected commit: 3b243b870a5442a6e5f443056f67bbba87d472f5 Affected pallet: pallets/reversible-transfers Attacker privileges: One ordinary funded account; no privileged role or victim interaction Observed attack fee: 23,260,190,920 planck = 0.023260190920 QNT

This matches the program's listed Medium impact, “Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol),” in the Quantus competition scope.

Root cause

The guardian reverse index is target-keyed and bounded:

Production config sets the hard limit to 32:

However, set_high_security authenticates only the nominating account. It checks that the target is not the caller, but never obtains authorization from the target guardian before consuming its capacity:

The runtime's Utility pallet amplifies the issue. as_derivative deterministically changes the effective signed origin without requiring the pseudonym to have a key or account balance:

Relevant source locations:

Exploit sequence

1

Select a guardian

The attacker selects a live guardian account to deny service to.

2

Construct a batch

The attacker constructs one Utility::batch containing 32 children.

3

Use derivative calls

Child i is Utility::as_derivative(index=i, set_high_security(target_guardian)).

4

Generate distinct origins

Each child receives a distinct deterministic signed pseudonym.

5

Enroll zero-balance pseudonyms

Each pseudonym is absent from System storage and has zero free, reserved, and held balance, but set_high_security accepts it.

6

Fill the guardian index

All 32 pseudonyms are appended to GuardianIndex[target_guardian].

7

Block a legitimate enrollment

A legitimate funded user calls set_high_security with that guardian and receives TooManyGuardianAccounts.

8

Preserve the poisoned entries

Funding, recovering, and reaping an attacker pseudonym does not remove either persistent storage row, so the index remains full.

This exploit does not depend on controlling multiple P2P identities, validators, miners, or independently funded accounts. The demonstrated attack is one authenticated signer and one valid extrinsic using a runtime-supported dispatch wrapper.

Impact

An attacker can permanently prevent any chosen account from accepting further legitimate high-security guardian relationships. The target does not sign or interact with the attacker. The target also has no guardian-side reject, deregister, or cleanup operation.

The denial persists across:

  • zero-balance/nonexistent pseudonym state;

  • later creation of the pseudonym's System account;

  • guardian execution of recover_funds;

  • balance removal and account reaping.

Restoring the target guardian's capacity therefore requires a runtime migration, direct privileged storage repair, or a code change. The report does not claim theft or freezing of existing user funds; the precise impact is low-cost, persistent denial of the guardian feature and is submitted as Medium griefing.

The full signed-runtime PoC measured:

Property
Result

Attacker signers

1

Signed extrinsics

1

Derivative origins

32

Derivative balances/reserves/holds

0 / 0 / 0

Attack call SCALE length

1,379 bytes

Signed ML-DSA-65 extrinsic length

6,681 bytes

Declared total weight

16,471,033,920 ref_time

Actual fee

23,260,190,920 planck

Actual fee in QNT

0.023260190920

Guardian index after attack

32 / 32

Legitimate enrollment

TooManyGuardianAccounts

Index after recovery and reaping

32 / 32

Expected versus actual behavior

Expected: A target guardian should authorize a relationship before it consumes that guardian's bounded capacity, or the guardian should be able to reject an unwanted relationship and release the slot.

Actual: Any signed origin can irreversibly consume a slot belonging to any target guardian. One signer can consume all 32 slots in one transaction using zero-balance derivatives, and the target guardian has no cleanup path.

Public-known-issue differentiation

The published Equilibrium finding EQ-QNT-REVERSIBLE-F-01 and related PRs discuss owner-side permanence: an account that voluntarily enables high-security mode cannot later rotate its chosen guardian or exit the mode. This report does not treat that accepted permanence as the vulnerability.

The distinct root cause here is that an unconsenting target guardian can have its bounded capacity consumed by third parties. Owner-controlled exit would not repair this exploit because the attacker controls all 32 pseudonyms and has no reason to exit. The repair must add guardian-side authorization/rejection or remove the target-keyed hard quota.

I reviewed the relevant public material:

Those materials discuss owner permanence, genesis index population, self-guardian validation, and guardian-index benchmarking. They do not describe missing guardian consent, arbitrary-target quota exhaustion, or the one-signer derivative attack demonstrated here.

1

Submit a nomination request

The protected account submits a nomination request.

2

Accept as guardian

The nominated guardian signs an acceptance.

3

Insert accepted relationships

Only accepted relationships are inserted into GuardianIndex and count toward MaxGuardianAccounts.

A guardian should also be able to reject pending nominations without consuming quota. If revocation is supported, cleanup must atomically update every related storage structure. Requiring only account existence or a small nominator deposit is not sufficient because one signer can derive and fund many pseudonyms; target authorization is the decisive control.

Proof of Concept

What the PoC proves

The following integration test executes the attack through the complete production transaction pipeline:

  • builds a real ML-DSA-65 signed extrinsic;

  • includes the production TxExtension tuple;

  • calls Executive::apply_extrinsic;

  • uses one outer signer and 32 as_derivative origins;

  • verifies every derivative starts and ends with no System account, balance, reserve, or hold;

  • verifies both the guardian and legitimate victim are live funded accounts;

  • fills the production limit of 32 entries;

  • proves the legitimate victim receives TooManyGuardianAccounts;

  • reads the actual TransactionFeePaid event;

  • funds one poisoned pseudonym, calls recover_funds as the guardian, reaps it, and proves the poisoned rows remain.

No mainnet, public testnet, external node, or third-party asset is used.

1

Obtain the exact scoped commit

2

Create the PoC file

Save the complete code below as:

3

Run the PoC

The test was confirmed with Rust 1.93.0:

Expected output

Compiler warnings unrelated to the finding are omitted. The relevant output is:

The decisive observations are:

  1. The target guardian and legitimate victim both exist.

  2. All 32 derivative origins remain nonexistent and unfunded.

  3. One signed transaction fills the guardian index to 32.

  4. The victim receives TooManyGuardianAccounts.

  5. Recovery and account reaping leave the index at 32 and preserve the high-security row.

  6. The complete attack costs 0.023260190920 QNT at the scoped runtime configuration.

Was this helpful?