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

89284 bc medium permissionless high security enrollment lets attackers irreversibly exhaust an arbitrary guardian s account capacity

Submitted on Aug 18th 2026 at 12:03:52 UTC by @CarlosMB for Audit Comp | Quantus

  • Report ID: #89284

  • 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

ReversibleTransfers::set_high_security() allows any signed account to unilaterally choose any other AccountId as its guardian:

pub fn set_high_security(
    origin: OriginFor<T>,
    delay: BlockNumberOrTimestampOf<T>,
    guardian: T::AccountId,
) -> DispatchResult {
    let who = ensure_signed(origin)?;

    ensure!(guardian != who.clone(), Error::<T>::GuardianCannotBeSelf);

There is no guardian-side signature, acceptance, registration, or consent.

However, the pallet maintains a bounded reverse index for each guardian:

and production config sets:

set_high_security() uses this reverse index as an admission condition:

Therefore a third party can consume another account's bounded guardian capacity without authorization.

An attacker creates 32 ordinary accounts A1..A32 and from each calls:

where G is the victim guardian.

After those calls:

Any subsequent legitimate user attempting:

fails with:

The victim guardian has no callable operation that removes the attacker-controlled accounts from its index.

Root Cause

The root cause is that an unauthenticated third party controls consumption of a bounded resource owned by another identity.

GuardianIndex is documented as a reverse lookup:

but the bounded representation is also used as security-sensitive admission control.

The account being inserted chooses the guardian unilaterally, while the guardian:

  • does not sign;

  • does not accept;

  • does not register the protected account;

  • cannot reject the relationship;

  • cannot remove the relationship later.

This allows arbitrary users to consume all capacity associated with any chosen guardian.

External Reachability

The attack is fully permissionless.

The only origin check in set_high_security() is:

The attacker does not need:

  • the guardian's private key;

  • guardian approval;

  • governance;

  • Root;

  • Technical Collective privileges;

  • an existing relationship with the guardian.

The attacker only needs MaxGuardianAccounts ordinary signed accounts and enough balance to submit the transactions.

There is no guardian-registration deposit or guardian-side authorization tied to the slot being consumed.

Attack Path

Assume G is a guardian address that legitimate users intend to use.

In production:

The attacker controls 32 Sybil accounts:

Each attacker account executes:

Every call succeeds because:

The resulting state is:

Now Alice attempts to enable high-security mode with the same guardian:

The call reaches:

and fails with:

Because the GuardianIndex mutation fails before:

Alice does not enter high-security mode at all.

The attacker has therefore denied G to all future users.

Why the DoS persists

There is no normal lifecycle operation that removes protected accounts from GuardianIndex.

The important cleanup/recovery path is recover_funds().

It:

1

Verifies the caller is the configured guardian

2

Cancels/reconciles pending transfers

3

Sweeps the protected account's free balance to the guardian

But it never removes:

or:

So even if the victim guardian uses its strongest available recovery operation against one of the attacker's accounts and completely drains it, the slot remains occupied.

Account balance reaping does not clean pallet-specific storage either.

Therefore the attacker can sacrifice minimally funded Sybil accounts and leave the victim guardian's bounded capacity exhausted indefinitely under normal protocol operation.

A privileged runtime/storage migration could repair the state, but there is no recovery path available to the victim guardian or legitimate users.

Existing protocol test already proves the saturation primitive

The repository already contains:

This already establishes that arbitrary signed accounts can fill the bounded guardian index and that the next user is rejected.

The PoC below extends that existing regression test to demonstrate the security impact: the accounts filling the vector are attacker-controlled, the rejected account is an unrelated legitimate user, and even recover_funds() does not release a consumed slot.

Severity / Impact

Medium — BC-M08: Griefing

An unprivileged attacker can use ordinary signed accounts to permanently consume all MaxGuardianAccounts slots of an arbitrary guardian address.

Once the guardian's bounded reverse index is full, every later legitimate user attempting to select that guardian is deterministically rejected with TooManyGuardianAccounts.

The targeted guardian cannot reject the malicious relationships, remove them, or reclaim the consumed slots through any normal protocol operation. Even recover_funds() empties the malicious protected account without removing its HighSecurityAccounts entry or its slot from GuardianIndex.

In production MaxGuardianAccounts = 32, so 32 attacker-controlled accounts are sufficient to deny that guardian to every future user.

Proof of Concept

File

Add the following test to:

The existing mock config uses:

while production uses:

The vulnerability is generic over T::MaxGuardianAccounts; the smaller mock value only makes the regression test shorter.

PoC code

Run

From the chain repository root:

Expected result

The test should pass.

The important state transition is:

This demonstrates both the permissionless capacity exhaustion and the lack of a normal recovery path.

Impact

A malicious user can intentionally deny an arbitrary guardian address to all future high-security users.

This is particularly relevant for guardians that are intended to be reused, such as:

  • organizational recovery accounts;

  • custodial/recovery services;

  • publicly recommended guardians;

  • corporate security addresses;

  • any guardian address shared among multiple users.

The attack does not need to compromise G.

The victim guardian also cannot fix the problem by recovering or draining the malicious protected accounts: those accounts remain permanently present in GuardianIndex.

The protocol therefore lets unrelated users irreversibly consume a bounded resource belonging to another security identity.

This is a targeted denial of service / griefing attack and maps to:

BC-M08 — Griefing

The report does not claim loss or freezing of user funds.

Why this is not merely the intentional permanence of High Security

The protocol intentionally documents that a user who enables high-security mode cannot later disable it.

That is not the issue reported here.

This report does not require changing the permanence of the protected account's own security configuration.

The vulnerability is that the permanent relationship consumes bounded capacity associated with a different account:

G neither requests nor authorizes that capacity consumption.

With enough attacker-controlled accounts, third parties can therefore weaponize the intended permanence against an unrelated guardian and deny it to future legitimate users.

Why choosing another guardian does not remove the impact

A legitimate user may technically select a different address, but this does not undo the targeted denial of service against G.

The security primitive explicitly allows reusable guardian accounts and maintains a reverse index of accounts protected by each guardian.

If G is a specific recovery service, organizational account, custodian, or pre-established security identity, an attacker can make that advertised guardian unusable for all new enrollments.

Requiring every targeted guardian to rotate to a fresh address after an attacker fills its slots is itself the attacker-imposed disruption.

More importantly, the victim has no protocol operation that restores the consumed resource.

Do not allow one party to irreversibly consume another account's bounded guardian capacity without authorization.

Possible fixes include:

Option 1 — Guardian acceptance

Use a two-step enrollment:

Only accepted relationships count toward MaxGuardianAccounts.

Option 2 — Remove the guardian-wide admission bound

If GuardianIndex exists only as a reverse lookup, do not make its BoundedVec capacity determine whether a user may enter high-security mode.

For example, use a keyed mapping:

and paginate/index it off-chain as needed.

Option 3 — Guardian-controlled removal/rejection

If a bounded index is required, give the guardian a safe way to reject unsolicited relationships and release the slot without weakening the protected account's own high-security guarantees.

The key invariant should be:

An unrelated account must not be able to permanently consume a bounded resource associated with another account without that account's authorization.

Was this helpful?