88772 sc high stale claimed flag after compliance rescue permanently freezes a later withdrawal credited to the same period
Submitted on Aug 15th 2026 at 00:37:58 UTC by @Maroutis for Audit Comp | Firelight
Report ID: #88772
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
rescueWithdrawFromBlocklisted sets isWithdrawClaimed[period][account] = true even though that period can still receive new withdrawal requests. If compliance later unblocks the account, a normal withdrawal burns more shares and credits a new withdrawal to the same period. claimWithdraw and a second rescue both revert with AlreadyClaimedPeriod, leaving the underlying in the vault but excluded from totalAssets().
Vulnerability Details
The rescue moves the existing withdrawal shares and then marks the source (period, account) pair as claimed:
uint256 _withdrawOf = withdrawSharesOf[periods[i]][from];
if (isWithdrawClaimed[periods[i]][from]) revert AlreadyClaimedPeriod(periods[i]);
withdrawSharesOf[periods[i]][to] += _withdrawOf;
withdrawSharesOf[periods[i]][from] = 0;
isWithdrawClaimed[periods[i]][from] = true;Withdrawal requests made during the current period always target currentPeriod()+1. _requestWithdraw does not check the terminal flag:
The fresh request succeeds and irreversibly burns shares. At maturity, claimWithdraw checks the stale flag before calculating or transferring the new entitlement:
The rescue path contains the same check. There is no admin or user function that clears the flag or moves an entitlement behind a true flag.
This requires no malicious privileged actor. Blocklist, rescue, and later unblock are normal compliance lifecycle actions. Once unblocked, the user is an ordinary caller and the vault publicly accepts the fatal withdrawal.
Impact Details
The later withdrawal amount is permanently frozen. In the PoC, 600 FXRP is left in the vault after the user's 600 live shares are burned. pendingWithdrawAssets remains 600, totalAssets() excludes it, and withdrawalsOf(P,user) still reports 600, yet neither claimant nor rescuer can transfer it. The amount is bounded only by the user's remaining share balance, so this is the in-scope Critical impact “Permanent freezing of funds.”
References
Proof of Concept
From the repository root:
The test performs these steps:
Full runnable test code
The exact standalone test executed for this submission is reproduced in full below.
Actual execution result
Was this helpful?