90159 sc high redundant state update in rescuewithdrawfromblocklisted permanently freezes funds for unblocklisted users withdrawing again
Submitted on Aug 23rd 2026 at 11:43:18 UTC by @shekdo for Audit Comp | Firelight
Report ID: #90159
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
In FirelightVault, rescueWithdrawFromBlocklisted redundantly sets isWithdrawClaimed to true when moving pending withdrawals from a blocklisted user. Because withdrawal requests are always scheduled for the next period, this rescue operation flags a future period as claimed. If the user is subsequently unblocklisted and makes a new withdrawal targeting that same period, their shares are burned and the assets enter pendingWithdrawAssets, but both claimWithdraw and further administrative rescues will revert. This permanently freezes the user's new withdrawal with no protocol recovery path.
Vulnerability Details
When the RESCUER_ROLE calls rescueWithdrawFromBlocklisted to move pending withdrawals, it zeroes out withdrawSharesOf[periods[i]][from] and sets isWithdrawClaimed[periods[i]][from] = true:
// contracts/core/FirelightVault.sol:860-873
withdrawSharesOf[periods[i]][to] += _withdrawOf;
withdrawSharesOf[periods[i]][from] = 0;
isWithdrawClaimed[periods[i]][from] = true;The function places no constraints on periods[i] other than the user having a non-zero withdrawal balance for it:
Because all withdrawal requests are automatically scheduled for currentPeriod() + 1 (with no ability for the caller to choose the period), the rescued period is often a future period:
If the user is unblocklisted and makes another withdrawal before the rescued period starts, the new request targets the exact same future period. The user's shares are burned immediately within _requestWithdraw:
The withdrawn assets are added to pendingWithdrawAssets, meaning they remain counted in the vault's pending-withdrawal accounting forever:
When the user attempts to claim the withdrawal with claimWithdraw(period), the transaction reverts with AlreadyClaimedPeriod because the prior rescue set the flag. This occurs before the function reads the caller's share balance:
The RESCUER_ROLE cannot recover the funds either, as rescueWithdrawFromBlocklisted also reverts if isWithdrawClaimed is true:
The isWithdrawClaimed flag is a storage mapping that cannot be cleared by any function in the in-scope sources. An upgrade deleting the offending line prevents new cases but releases no already-trapped funds. The write itself is redundant, as the zeroing of withdrawSharesOf already ensures a double-claim reverts with NoWithdrawalAmount:
Impact Details
rescueWithdrawFromBlocklisted stamps isWithdrawClaimed on a period that has not started, which is the exact period every new withdrawal request targets. A user who is cleared and withdraws again inside that period has their shares burned and their assets moved into pendingWithdrawAssets, where claimWithdraw and any further rescue both revert forever. The stamp is storage and nothing in the contracts ever clears it, so even shipping a fix does not release funds already trapped. The amount is bounded only by the user's own share balance, not by the rescued amount. While the bug requires administrative compliance actions to trigger, it bypasses the intended protocol constraint that unblocklisting a user restores their ability to withdraw normally, fulfilling the program's privileged-actor carve-out.
References
https://github.com/immunefi-team/audit-comp-firelight/blob/v1_audit_ready/contracts/core/FirelightVault.sol
Proof of Concept
Proof of Concept
The test deploys the FirelightVault implementation behind an ERC1967Proxy using the live vault's 1-day period. It replays the scenario: a user withdraws to a future period, is blocklisted, their withdrawal is rescued, they are unblocklisted, and they redeem their remaining position into the same period. After the period elapses, the user's attempt to claimWithdraw reverts with AlreadyClaimedPeriod, and a second rescue attempt reverts with the same error, proving the funds are permanently stuck in pendingWithdrawAssets.
Was this helpful?