For the complete documentation index, see llms.txt. This page is also available as Markdown.

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, commitAllocation executes first. _computeAvailableCapacity calls totalAssetsAt(currentPeriodStart()). Because no transaction has written a checkpoint at T yet, 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. withdraw records only 20 active assets at timestamp T, while _requestWithdraw moves the other 80 into the delayed-withdrawal bucket

  • Checkpoints.upperLookupRecent returns the latest checkpoint whose key is less than or equal to the requested timestamp. Therefore, later calls to totalAssetsAt(T) return 20, although the allocator previously committed 100 of capacity using the same period-start timestamp

  • _settleCoverOrder relies on the stored commitment and does not recompute capacity. The full 100 units of cover can therefore be settled and represented by a real CoverNFT

  • A valid incident assessment approves a loss of 100 against that settled cover. FirelightVault.payout sees 100 payable assets in total: 20 active assets plus 80 delayed-withdrawal assets. However, it also caps the payment by totalAssetsAt(T), which now returns 20

  • Only 20 is paid. approveCurrentAssessment then 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, but commitAllocation can 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 timestamp T, 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 CoverNFT

  • A 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?