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

89014 sc high exact boundary checkpoint overwrite underpays approved cover claims

Submitted on Aug 16th 2026 at 14:07:51 UTC by @pks271 for Audit Comp | Firelight

  • Report ID: #89014

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/immunefi-team/audit-comp-firelight/blob/v1_audit_ready/contracts/core/lib/Checkpoints.sol

  • Impacts:

    • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Bug Description

The allocator validates committed cover capacity from the vault snapshot at a period boundary. After that validation, a public withdrawal can occur later in the same block and overwrite the vault checkpoint stored under that exact timestamp. A later payout re-reads the same timestamp as an immutable period-start exposure cap, so it uses the post-withdrawal value rather than the value used to validate and settle cover.

The source trace is:

contracts/core/CoverOrderAllocator.sol#commitAllocation -> contracts/core/FirelightVault.sol#totalAssetsAt -> public withdraw in the same block -> contracts/core/FirelightVault.sol#_logTrace -> contracts/core/lib/Checkpoints.sol#push -> contracts/core/IncidentManager.sol#approveCurrentAssessment -> contracts/core/FirelightVault.sol#payout -> totalAssetsAt again

The critical code paths are:

// contracts/core/lib/Checkpoints.sol#push
if (exists && key == lastKey) {
    self._values[len - 1] = value;
} else {
    self._trace.push(key, uint208(len));
    self._values.push(value);
}

The allocator capacity calculation consumes the boundary snapshot:

The payout later consumes the same lookup as its exposure cap:

The public withdrawal writes a replacement checkpoint at the same timestamp:

As a result, cover can be validated and settled against 100 units, while a later approved loss for that settled cover is capped using 10 units.

Impact

A public LP withdrawal in the same exact-boundary block as allocator validation can reduce the historical period-start asset snapshot. The Vault then pays only 10 of an approved 100-unit insurance loss even though the settled cover, approved assessment, and payable liability all support 100. The IncidentManager completes the incident with the 10-unit payout, leaving 90 units unpaid in the reproduced state. The report does not demonstrate a later payout path for the same closed incident.

The affected payout recipient does not need to be the withdrawing LP. The withdrawing LP has a direct economic incentive: it requests withdrawal immediately after cover capacity is committed, reducing the historical exposure cap before a subsequent covered incident is paid.

This is distinct from the disclosed “aggregate claims are not capped on-chain” issue. The PoC uses one legitimate claim whose amount is bounded by one settled cover; it does not submit duplicate, excessive, or multiple claims.

Attack Vector and Preconditions

  • A 100-unit cover order exists for the period.

  • The allocator's normal boundary commitment executes in a block whose timestamp is exactly the mathematical period start.

  • A public withdrawal from an LP is independently signed and included after that commit in the same block.

  • The allocation is settled and a legitimate 100-unit incident assessment is approved. This is a single claim already bounded by one settled cover; it does not rely on the separately disclosed aggregate multi-claim cap limitation.

  • The proof uses the real Vault, CoverOrderAllocator, CoverNFT, and IncidentManager. The allocator commit and incident setup are normal role operations used to create the insured state; the state rewrite itself is made by the public LP withdrawal.

Recommendation

Make the period-start asset snapshot immutable and unambiguous. For example:

  1. Write a once-only period-start checkpoint.

  2. Derive the boundary value from the last checkpoint before the boundary.

  3. Persist the exact vault snapshot in the allocation commitment and have payout consume that committed snapshot instead of re-querying the mutable same-key trace.

Proof of Concept

The test disables automine, sends the allocator commit and LP withdrawal as separately signed transactions from different signers, mines them in one exact-boundary block, verifies both receipts succeed, and then settles a full 100-unit cover and approves a 100-unit loss.

From the repository root, run the current-behavior regression:

Observed result:

To show the economically correct payout and produce the failing assertion, run:

Observed failure:

Was this helpful?