90013 sc high permanent freezing of user funds due to state corruption in rescuewithdrawfromblocklisted when resolving compliance false positives
Submitted on Aug 22nd 2026 at 13:57:17 UTC by @hunraj for Audit Comp | Firelight
Report ID: #90013
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
Executive Summary
The FirelightVault contract provides compliance tools to blocklist users and rescue their pending withdrawals. However, the rescueWithdrawFromBlocklisted function introduces a destructive state corruption by permanently setting the isWithdrawClaimed flag to true for the rescued period.
If an innocent user is erroneously blocklisted (a compliance false positive), rescued, and subsequently unblocklisted within the same vault period, any new withdrawal requests they make during that period will be permanently locked. When the period ends, the user cannot claim their funds, and the admin's RESCUER_ROLE is completely bypassed because the rescue function itself reverts on the corrupted flag. This leads to the permanent freezing of user funds.
Vulnerability Description
In FirelightVault.sol, delayed withdrawals are assigned to the currentPeriod() + 1. When a user's account is flagged for compliance reasons, the RESCUER_ROLE can extract their pending withdrawals using rescueWithdrawFromBlocklisted().
The vulnerability exists in how the user's state is mutated during the rescue:
Setting isWithdrawClaimed to true is mathematically redundant (as zeroing withdrawSharesOf ensures any future claimWithdraw calculates 0 assets and reverts with NoWithdrawalAmount). However, setting this flag introduces a fatal side effect if the user is unblocklisted within the same vault period.
If the compliance flag was a false positive, the BLOCKLIST_ROLE calls removeFromBlocklist(), restoring the user's access. The user resumes standard protocol activity and initiates a new withdraw() or redeem().
Because the protocol is still in the same period, the new withdrawal is assigned to the exact same target period bucket. The protocol happily burns the user's shares and locks their assets in pendingWithdrawAssets, incrementing withdrawSharesOf[period][from].
However, when the period advances and the user attempts to retrieve their assets via claimWithdraw(), the transaction reverts:
The user's funds are now permanently frozen in the vault.
Impact Assessment
Severity: High (Permanent freezing of funds).
Likelihood: Medium/High. Firelight's documentation notes that periods will be 30 days long. A 30-day window is more than enough time for a user to be flagged by automated compliance tools (e.g., Chainalysis), investigated, cleared as a false positive, and resume trading.
Bypass of Emergency Recovery: Crucially, if the admin realizes the user's funds are stuck, they cannot use the
RESCUER_ROLEto save them. TherescueWithdrawFromBlocklistedfunction contains the exact sameif (isWithdrawClaimed[periods[i]][from]) revert AlreadyClaimedPeriod(periods[i]);check, meaning the protocol's emergency escape hatch is also completely neutralized by the corrupted state.
Step-by-Step Reproduction
Period 10: Alice requests a withdrawal of 5,000 USDC. It is scheduled for Period 11.
Period 10: Alice's wallet is flagged by automated off-chain compliance monitoring. Admin calls
addToBlocklist(Alice).Period 10: Admin calls
rescueWithdrawFromBlocklisted(Alice, ComplianceWallet, [11]). Alice'sisWithdrawClaimed[11]is set totrue.Period 10: Compliance investigation concludes Alice's flag was a false positive. Admin calls
removeFromBlocklist(Alice).Period 10: Alice resumes normal activity and requests a new withdrawal of 2,000 USDC. It is scheduled for Period 11. Her shares are burned and 2,000 USDC is locked in
pendingWithdrawAssets.Period 11 starts: Alice calls
claimWithdraw(11). The transaction reverts withAlreadyClaimedPeriodbecause of the state corruption in Step 3. Her 2,000 USDC is permanently lost.Failed Rescue: Admin tries to blocklist Alice and rescue the 2,000 USDC.
rescueWithdrawFromBlocklistedalso reverts withAlreadyClaimedPeriod. The funds are dead.
Proof of Concept
Proof of Concept
Place the following code in test/PoC_RescueLock.js and execute via npx hardhat test test/PoC_RescueLock.js.
Security Recommendations
Remove the line that sets isWithdrawClaimed[periods[i]][from] = true; inside rescueWithdrawFromBlocklisted().
This explicit flag mutation is completely unnecessary. claimWithdraw() determines the assets to send by proportionally querying the user's share balance:
Because the rescue function zeros out withdrawSharesOf[periods[i]][from], any unauthorized call to claimWithdraw by the blocklisted user will naturally calculate assets = 0 and revert with NoWithdrawalAmount(period). Removing the isWithdrawClaimed mutation guarantees that if the user is unblocklisted, they begin with a clean slate for future withdrawals.
Was this helpful?