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
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:
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.
Recommended remediation
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.
Expected output
Compiler warnings unrelated to the finding are omitted. The relevant output is:
The decisive observations are:
The target guardian and legitimate victim both exist.
All 32 derivative origins remain nonexistent and unfunded.
One signed transaction fills the guardian index to 32.
The victim receives TooManyGuardianAccounts.
Recovery and account reaping leave the index at 32 and preserve the high-security row.
The complete attack costs
0.023260190920 QNTat the scoped runtime configuration.
Was this helpful?