88473 sc high mutable period start checkpoint causes protocol insolvency by underpaying approved cover claims
Submitted on Aug 13th 2026 at 11:04:53 UTC by @Aestheticccccc for Audit Comp | Firelight
Report ID: #88473
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:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
CoverOrderAllocator and FirelightVault both use vault.totalAssetsAt(vault.currentPeriodStart()) as the collateral exposure for a period, however this value can change during the first block of that period when an allocation is committed first and a staker withdraws later in the same period-boundary block, the commitment is validated using the assets that existed before the withdrawal, thewithdrawal then records a lower checkpoint at the exact period-start timestamp, cover can still be settled using the larger capacity stored in the commitment, while a later incident payout reads the new, lower checkpoint and pays less than the approved claim values 100, 80, and 20 used below are an example scenario reproduced by the poc as well the vault starts with 100 assets, the commitment records capacity for 100, and a withdrawal of 80 leaves a period-start checkpoint of 20. Note - The issue is not limited to these values and scales with the amount withdrawn these values are just used for an example
Vulnerability Details
root cause is that the protocol treats an inclusive timestamp checkpoint as an immutable period start snapshot, even though a transaction can still write a checkpoint with that timestamp after another transaction has already read it at the exact period boundary T, periodAtTimestamp and currentPeriodStart consider the new period active and return T as its start. The following sequence is then possible
Before
T, the vault contains 100 active assets. Cover orders have already been created for the next period.In the first block of the new period,
commitAllocationexecutes first._computeAvailableCapacitycallstotalAssetsAt(currentPeriodStart()). Because no transaction has written a checkpoint atTyet, the lookup returns the latest earlier value, 100. The allocator accepts and stores a matching capacity of 100.A staker's public
withdraw(80, ...)transaction executes later in the same block.withdrawrecords only 20 active assets at timestampT, while_requestWithdrawmoves the other 80 into the delayed-withdrawal bucketCheckpoints.upperLookupRecentreturns the latest checkpoint whose key is less than or equal to the requested timestamp. Therefore, later calls tototalAssetsAt(T)return 20, although the allocator previously committed 100 of capacity using the same period-start timestamp_settleCoverOrderrelies on the stored commitment and does not recompute capacity. The full 100 units of cover can therefore be settled and represented by a realCoverNFTA valid incident assessment approves a loss of 100 against that settled cover.
FirelightVault.payoutsees 100 payable assets in total: 20 active assets plus 80 delayed-withdrawal assets. However, it also caps the payment bytotalAssetsAt(T), which now returns 20Only 20 is paid.
approveCurrentAssessmentthen closes the incident, so the unpaid 80 is not recovered through a later payout this can occur naturally because cover orders target the next period, butcommitAllocationcan only be called once that period is current. an allocator bot is therefore expected to commit near the beginning of a period, a staker may already have a withdrawal pending at the boundary or may submit one for inclusion after the commitment,the allocator, curator, and assessment approver perform normal operations with valid data the only benefiting actor is an ordinary staker calling the public withdrawal function thus the required condition is that both transactions execute in the first new-period block at timestampT, with the commitment before the withdrawal and flare documents an approximately 1.8-second block time and priority-gas-auction ordering, so this order is possible
Impact Details
a valid cover claim can become undercollateralized after commitment and then be permanently underpaid. In the reproduced example
100 assets are present when capacity is committed
100 units of cover are settled and represented by a real
CoverNFTA 100-unit loss is validated against that settled cover and approved
The vault holds 20 active assets plus 80 delayed-withdrawal assets, all still slashable by the payout logic
Only 20 is paid, the incident is closed, and the staker retains 64 pending withdrawal assets plus 16 active assets claim is underpaid by 80% in this example, and the shortfall scales with the amount withdrawn in the boundary block the protocol has sold and settled 100 units of cover but can satisfy only 20 units of the approved claim. this leaves its cover obligations greater than the amount the payout logic will release
References
Add any relevant links to documentation or code
Proof of Concept
Proof of Concept
to run this poc - create a file named period_boundary_claim_underpayment.js
Paste only the JavaScript code below in that file
run it using
npx hardhat test test/period_boundary_claim_underpayment.js
Was this helpful?