90066 sc high function rescuewithdrawfromblocklisted permanently freezes a rescued account s future withdrawals
Submitted on Aug 22nd 2026 at 20:18:18 UTC by @berring for Audit Comp | Firelight
Report ID: #90066
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
Function rescueWithdrawFromBlocklisted permanently marks a rescued account's withdrawal slot as claimed for a period that has not yet occurred. If the account is later removed from the blocklist and submits another withdrawal during the same period, the new request is booked into that already-poisoned slot. Once the claim window opens, claimWithdraw always reverts with AlreadyClaimedPeriod, permanently freezing the user's assets. The frozen amount remains included in pendingWithdrawAssets, causing totalAssets() to remain understated for all vault participants.
Vulnerability Details
Root cause
When a pending withdrawal is rescued, the function both transfers the withdrawal shares to the rescue destination and permanently marks the source account's (period, account) slot as claimed:
contracts/core/FirelightVault.sol:861-871
The function does not restrict periods[i] to completed periods. This is important because normal withdrawal requests are always assigned to the next, still-future period:
contracts/core/FirelightVault.sol:1008-1017
Therefore, rescuing a normal pending withdrawal sets isWithdrawClaimed[futurePeriod][from] = true before that period occurs. While the current period is still active, the same account can be removed from the blocklist and submit another legitimate withdrawal. That new request is assigned to the same future period, but the previously written claim flag remains set.
Why the new withdrawal can never be claimed
claimWithdraw checks isWithdrawClaimed before reading or converting the account's withdrawal-share balance:
contracts/core/FirelightVault.sol:788-806
As a result, even when withdrawSharesOf[period][sender] contains a valid, non-zero entitlement from the later withdrawal, execution reverts at the claim-flag check and never reaches the balance conversion.
The condition is permanent:
isWithdrawClaimedhas only two writers in the contract: the normal claim path and the affected rescue path.Neither code path ever resets the flag to
false.There is no administrative recovery function for the poisoned slot.
Re-running the rescue cannot recover the new withdrawal because the function first checks the source account's already-latched flag and reverts with
AlreadyClaimedPeriod, regardless of the supplied destination.
Step-by-step exploitation scenario
No privileged role needs to exceed its documented permissions:
Alice deposits assets and requests a withdrawal during period
P. The request is booked into periodP+1, sowithdrawSharesOf[P+1][alice] > 0.BLOCKLIST_ROLEadds Alice to the blocklist.RESCUER_ROLElegitimately callsrescueWithdrawFromBlocklisted(alice, treasury, [P+1]).The function moves Alice's pending withdrawal to the treasury, zeroes her balance for that request, and sets
isWithdrawClaimed[P+1][alice] = true.BLOCKLIST_ROLEremoves Alice from the blocklist using the vault's intendedremoveFromBlocklistfunction.Before period
Pends, Alice submits another withdrawal. It is assigned to the same periodP+1, resulting in a new positivewithdrawSharesOf[P+1][alice]balance.After period
P+1passes, Alice callsclaimWithdraw(P+1).The call always reverts with
AlreadyClaimedPeriod(P+1), despite Alice having a positive withdrawal entitlement.Re-running the rescue also reverts on the same flag, leaving no path through which Alice, the rescuer, or another account can release the assets.
The rescue destination can still claim the originally rescued withdrawal. This confirms that the problem is isolated to the poisoned (period, account) slot rather than a general failure of the vault.
Why the vulnerable write is unnecessary
The preceding assignment already sets withdrawSharesOf[period][from] = 0, which is sufficient to prevent the rescued withdrawal from being claimed again:
A later
claimWithdrawcall sees a zero balance and reverts withNoWithdrawalAmount.Repeated periods in the same rescue call, such as
[5, 5], are rejected because the second iteration reads the balance zeroed by the first iteration.The normal withdrawal path retains its independent double-claim protection.
The appropriate fix is to remove the redundant claim-flag write:
With this line removed, all 40 relevant existing project tests continue to pass, including the rescue tests and the ordinary double-claim protection test.
Impact Details
The selected in-scope impact is permanent freezing of funds.
After the affected sequence occurs, the victim has a positive withdrawal entitlement that cannot be claimed through any available code path. The condition does not expire, and neither the victim nor an administrator can clear the poisoned claim flag. The loss is therefore permanent rather than a temporary denial of service.
The accounting consequences extend beyond the affected user. The frozen assets remain in pendingWithdrawAssets, while totalAssets() is calculated as:
Consequently:
the victim permanently loses access to the assets represented by the poisoned withdrawal slot;
pendingWithdrawAssetsremains permanently overstated;totalAssets()remains understated for every remaining LP; andtotalAssetsAt(), which is used byCoverOrderAllocator._computeAvailableCapacity, is also depressed, reducing the protocol's calculated sellable-cover capacity.
The runnable PoC demonstrates 3,000 fXRP permanently unclaimable after the treasury successfully claims the originally rescued amount. The 3,000 fXRP remains counted in pendingWithdrawAssets.
This issue does not require an attacker to obtain a privileged role. The blocklisting, rescue, and removal from the blocklist are all legitimate protocol operations performed within the roles' documented authority. The permanent loss occurs when the unblocked user later uses the ordinary withdrawal flow.
References
Relevant code
Vulnerable write:
contracts/core/FirelightVault.sol:861-871Withdrawal-period assignment:
contracts/core/FirelightVault.sol:1008-1017Fatal claim-flag check:
contracts/core/FirelightVault.sol:788-806Vault accounting affected by the frozen pending amount:
contracts/core/FirelightVault.sol:428In-scope commit:
42f9ea5(v1_audit_ready)
Fix verification
After removing the redundant isWithdrawClaimed[periods[i]][from] = true assignment, run:
All 40 existing tests pass, including the rescue suite and the ordinary double-claim guard.
Proof of Concept
Proof of Concept
Create the following file:
Run it from the repository root with:
Soucre code:
Was this helpful?