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

89055 sc high missing period check in rescuewithdrawfromblocklisted marks a still open period as claimed causing permanent freezing of funds for any withdrawal later requested into that period

Submitted on Aug 16th 2026 at 19:41:33 UTC by @AlexScherbatyuk for Audit Comp | Firelight

  • Report ID: #89055

  • 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

Brief/Intro

FirelightVault batches withdrawals into periods where a request credits withdrawSharesOf[P][account] while currentPeriod() == P - 1, and the resulting assets are claimed once the period has ended. However, a user could be added to the blocklist that opens a new branch where a rescuer can call rescueWithdrawFromBlocklisted to move blocked user funds to a safe address. This mechanism has a flow that can cause a user's funds to be permanently frozen.

The implemented blocklist mechanics assume that a user could be blocklisted by calling addToBlocklist and removed from blocklist via removeFromBlocklist (e.g. user proved the case for blocklisting is false positive), allowing the user to proceed with deposit/withdraw operations.

However, in case rescueWithdrawFromBlocklisted has been called during the blocklisting period and removal from blocklist has happened during the same period, it creates a window when any further withdrawal requests (incease there is more funds to withdraw) would be scheduled for P, which has already been marked as claimed, locking user funds permanently.

This requires no trusted role to misbehave — blocklist → rescue → un-blocklist is exactly the intended handling of a false-positive blocklist, and the final trigger is a normal, permissionless withdrawal request.

According to docs current period is 1 day, but it is going to be up to 30 days period which increases the likelihood.

Vulnerability Details

The withdrawal flow relies on a temporal invariant: pool P only accepts credits while currentPeriod() == P - 1, and isWithdrawClaimed[P][account] can normally only become true once currentPeriod() >= P + 1 (claims are restricted to past periods). A true claim flag can therefore never precede a credit.

rescueWithdrawFromBlocklisted breaks this invariant. It has no period check, so it can set the claim flag for a future period that is still accepting requests:

_requestWithdraw credits the receiver without ever checking the claim flag:

And claimWithdraw hard-reverts on the flag:

Atack Path

1

User creates a withdrawal request

withdrawSharesOf[P][user] > 0.

2

User is blocklisted and withdrawal is rescued

The rescuer calls rescueWithdrawFromBlocklisted(user, to, [P]). Their shares are moved and isWithdrawClaimed[P][user] = true is set for the future period P.

3

User is removed from the blocklist

The false positive is resolved, still during P - 1.

4

User makes a new withdrawal request

Alternatively, any third party names them as receiver, which _requestWithdraw freely allows.

withdrawSharesOf[P][user] += shares succeeds and burns vault shares.

Impact Details

The assets of the new request in step 4 are permanently unrecoverable:

  • claimWithdraw(P) reverts with AlreadyClaimedPeriod

  • Recovery via rescue is also impossible, even if the user is re-blocklisted: the rescue path reverts on the same flag.

  • The assets remain in pendingWithdrawAssets forever and are permanently excluded from totalAssets(). they are not even socialized back to remaining share holders. The user's burned vault shares are a total loss.

This is permanent, unrecoverable loss of user funds with no recovery path for any role, mapping to in-scope impact #Permanent freezing of funds. It is not "temporary freezing" (no state transition ever unlocks the funds) and not mere "griefing" (the damage is permanent loss of principal).

No trusted role needs to misbehave (blocklist → rescue → un-blocklist) is the intended resolution of a false-positive blocklist, and the final trigger is a normal, permissionless withdrawal request in the same period.

Recommendation

Consider removing the isWithdrawClaimed[periods[i]][from] = true line in rescueWithdrawFromBlocklisted:

References

https://docs.firelight.finance/for-stakers/deployments-and-withdrawals

Proof of Concept

Was this helpful?