88410 sc high rescue terminalizes a still open withdrawal bucket and permanently freezes post unblock principal
Submitted on Aug 13th 2026 at 03:16:37 UTC by @Kopi for Audit Comp | Firelight
Report ID: #88410
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 marks a blocked user's future withdrawal bucket as claimed even though that bucket can still receive new withdrawals. If the account is legitimately unblocked before the current period ends, a later withdraw or redeem succeeds, burns the user's shares, and appends fresh principal to the already-claimed bucket. Once mature, both normal claim and another rescue permanently revert, leaving the fresh principal trapped in the vault with no supported recovery path.
Vulnerability Details
Root cause
Firelight's delayed withdrawal flow burns shares immediately and records withdrawal shares in withdrawSharesOf[period][receiver], where the target period is currentPeriod() + 1.
rescueWithdrawFromBlocklisted supports future periods. It transfers the source bucket to a rescue receiver, clears the source amount, and sets isWithdrawClaimed[period][from] = true. This terminal marker is safe only if the source bucket can never receive another withdrawal.
The source bucket can be reused. If the account is unblocked before the current period changes, its next withdrawal targets the same future period. _requestWithdraw does not check isWithdrawClaimed[period][receiver] before it:
adds new withdrawal shares to the bucket;
increases
withdrawAssets,withdrawShares, andpendingWithdrawAssets; andburns the owner's vault shares.
At maturity, claimWithdraw checks isWithdrawClaimed first and reverts AlreadyClaimedPeriod. Re-blocking the account and attempting another rescue reaches the same source-side flag and also reverts. No in-scope function clears the flag or moves the new pending entitlement.
Reachable sequence
In period
P, an account has a withdrawal entitlement in periodP + 1.The account is blocklisted through the intended compliance role.
An authorized rescuer moves the entitlement to an allowed receiver. The vault sets
isWithdrawClaimed[P + 1][account] = true.Still in period
P, the blocklist role legitimately unblocks the account.The restored user owns or acquires shares and calls self-
withdrawor self-redeem.The request succeeds and burns the shares into the same
P + 1bucket despite its terminal flag.At maturity, the rescue receiver claims the original entitlement. The restored user's claim reverts, and a repeat rescue also reverts.
Neither privileged actor exceeds its named authority. The unsafe result is produced by the interaction of valid rescue, unblock, and ordinary withdrawal transitions.
Recommended remediation
The narrow prospective fix is to stop setting isWithdrawClaimed[period][from] during rescue after the source entitlement is fully moved and zeroed. A repeat claim/rescue then fails on zero amount, while a later request remains claimable. As defense in depth, _requestWithdraw should reject an already-claimed target bucket before changing accounting or burning shares. Existing rescue-created flags require targeted migration or user compensation; the mapping is not enumerable, so candidate keys must be reconstructed from complete WithdrawRescuedFromBlocklisted logs rather than cleared indiscriminately.
Impact Details
The exact in-scope impact is Permanent freezing of funds. The PoC freezes 1,000 six-decimal vault-asset units:
The user's second withdrawal succeeds.
The user's vault shares are irreversibly burned.
The fresh entitlement remains recorded in
withdrawSharesOfandpendingWithdrawAssets.The same amount remains in the vault's raw token balance.
claimWithdrawpermanently rejects the entitlement.rescueWithdrawFromBlocklistedalso permanently rejects it.
The trapped amount is excluded from active ERC-4626 totalAssets, so it is neither claimable principal nor reusable active capital. Recovery requires a contract upgrade, storage migration, or exceptional compensation. The issue does not depend on attacker profit; it is a permanent loss of access to the restored user's principal.
The amount is not dust-bounded. Any shares the restored account owns or acquires before the period changes can be burned into the poisoned bucket. Likelihood is low because rescue and unblock must occur in the same source period, but each operation is supported and Firelight permits one-day minimum periods.
References
The prior audit's M-02 finding has a similar final symptom but a different producer: period-duration reconfiguration could route a request into an old claimed period. That mechanism was repaired with monotonic period configuration. This finding requires no reconfiguration; the new rescue function itself terminalizes the still-open P + 1 source bucket. The later destination-claimed rescue fix checks the recipient, not reuse of the rescued source bucket.
Link to Proof of Concept
https://gist.github.com/sebastian-osec/920898b55fbb4c216effe93696ca6d5e
Proof of Concept
Proof of Concept
The PoC runs only on a local Hardhat network and uses the repository's real proxy FirelightVault, role checks, withdrawal accounting, and six-decimal fixture asset.
Steps
Clone
https://github.com/immunefi-team/audit-comp-firelight.gitand check out revision42f9ea5e43d88b35197b901acc2a8c24b314fa62.Run
npm ci.Create
test/security/.Attach/copy
poc.test.jsintotest/security/poc.test.js.Run:
The test has SHA-256 d577317947652fc6c0871213a914f456f55d065076cc134c2b92ab58e120b05a and passed at the pinned revision with Node v24.16.0, npm 11.13.0, and Hardhat 2.22.19:
The exploit case observes:
Was this helpful?