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

88425 sc high rescue marks an open withdrawal bucket claimed and permanently freezes later post unblock principal

Submitted on Aug 13th 2026 at 05:34:52 UTC by @rtw0x1 for Audit Comp | Firelight

  • Report ID: #88425

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/immunefi-team/audit-comp-firelight/blob/v1_audit_ready/contracts/core/FirelightVault.sol

  • Impacts:

    • Permanent freezing of funds

Description

Summary

rescueWithdrawFromBlocklisted() sets isWithdrawClaimed[period][from] = true, but the same account/period withdrawal bucket remains writable after the account is unblocked. A later ordinary withdrawal is added to that already-claimed bucket. Once the period matures, claimWithdraw() reverts with AlreadyClaimedPeriod; after the user is blocklisted again, rescueWithdrawFromBlocklisted() reverts on the same stale marker. This leaves the newly deposited principal permanently inaccessible through the current implementation.

The fresh-funds test below first rescues the user's entire old balance, then unblocks the user, deposits a new 1,000 units, and requests withdrawal into the same still-open destination period. This proves that the frozen principal did not exist when the rescue was performed.

Severity Rationale

  • Suggested severity: Critical

  • Impacted component: FirelightVault delayed-withdrawal and compliance-rescue state machine

  • Preconditions: A user is blocklisted, their existing withdrawal is rescued, they are unblocked while that destination period remains open, and they later deposit and request another withdrawal into the same period.

The later 1,000-unit principal remains recorded in withdrawalsOf and pendingWithdrawAssets, but every current-contract release path rejects it. Time does not clear the state. Recovery requires a proxy upgrade or exceptional state migration, matching the program's “Permanent freezing of funds” impact.

Impact

An ordinary restored user can lose access to all principal placed into a later same-account/same-period withdrawal after the supported compliance sequence. The funds remain reserved and excluded from active vault assets, but neither the user nor the rescue role can release them under the current code. There is no time-based recovery; remediation requires new implementation code or exceptional migration.

Remediation

Make a claimed withdrawal key immutable. When a new withdrawal would target an account/period whose claim marker is already set, route it to a new unclaimed bucket or revert before accepting shares. A more robust design is to track claimed share amounts or a per-bucket generation instead of a permanent boolean, so rescue authority applies only to the exact shares that existed when rescue executed. Add a regression test for rescue, unblock, fresh deposit, later withdrawal, maturity, and successful recovery.

Proof of Concept

Steps To Reproduce

  1. Clone the competition repository and check out the frozen revision:

  2. Save the following first test as test/poc/rescue-open-bucket.poc.js:

Save the following second test as test/poc/rescue-fresh-funds.poc.js:

  1. Run both tests:

  2. Observe that the post-rescue bucket contains a funded later withdrawal while its old claim marker is already true. After maturity, the test asserts that normal claim reverts; it then re-blocklists the user and proves repeat rescue also reverts. The fresh-funds comparator reports 1,000 units still pending with 1,000 raw units in the vault and zero active assets. The matched no-rescue control pays all 3,000 queued units and clears pending accounting.

    See step-04-rescue-marker-freezes-fresh-funds.png for the command and terminal result.

Proof Of Impact

The relevant code path is:

The rescue transfers only the shares present at rescue time, but its boolean marker continues authorizing rejection of principal added later to the same key.

Was this helpful?