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

88684 sc high checkpoint overwrites lead to loss of legitimate payouts

Submitted on Aug 14th 2026 at 14:13:28 UTC by @IllIllI for Audit Comp | Firelight

  • Report ID: #88684

  • 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:

    • Theft of unclaimed yield

Description

Brief/Intro

Checkpoints.Trace256.push overwrites a checkpoint when a new write uses the same key as the latest checkpoint. All user-controlled asset flows in FirelightVault write _traceTotalAssets keyed by block.timestamp, so a deposit, mint, redeem, or withdraw mined in the exact block whose timestamp equals an aligned period boundary rewrites the totalAssetsAt(capturePeriodStart) snapshot. That snapshot is the exposure cap for payout and the collateral value used by CoverOrderAllocator._computeAvailableCapacity. A permissionless boundary-block redeem can permanently suppress a legitimate, consortium-approved claim payout to zero; a boundary-block deposit can inflate the snapshot and allow over-exposure.

Vulnerability Details

The root cause is the same-key overwrite behavior in Checkpoints.Trace256.push (contracts/core/lib/Checkpoints.sol:198-210):

(bool exists, uint48 lastKey,) = self._trace.latestCheckpoint();
uint256 len = self._values.length;
uint256 lastValue = latest(self);
if (exists && key == lastKey) {
    self._values[len - 1] = value;       // overwrite on same key
} else {
    self._trace.push(key, uint208(len));
    self._values.push(value);
}

All asset-mutating user paths write the _traceTotalAssets checkpoint trace:

  • deposit / mint call _depositFunds, then _logTrace(..., totalAssets after deposit, true).

  • redeem / withdraw call _logTrace(owner, ..., totalAssets - assets, true), recording a deflated total-assets value.

  • payout also writes a checkpoint, but it is restricted to PAYOUT_ROLE.

  • checkpointTotalAssets is restricted to CHECKPOINT_ROLE.

Because every transaction in a block shares the same block.timestamp, totalAssetsAt(capturePeriodStart) returns the final total-assets state of whatever block has timestamp exactly equal to capturePeriodStart. If a user operation is mined in that exact boundary block, it overwrites the period-start checkpoint.

This corrupted snapshot is then used in two critical places:

  • FirelightVault.payout uses it as the per-incident exposure cap: paidAmount = min(amount, min(assetsAtCapturePeriod, activePayableAmount))

  • CoverOrderAllocator._computeAvailableCapacity uses vault.totalAssetsAt(vault.currentPeriodStart()) as the period-start collateral value.

1

A vault shareholder holds shares

2

Redeem at the period boundary

In the block whose timestamp equals the aligned period-start P, the shareholder calls redeem(≈A) for approximately the full total assets.

3

Overwrite the checkpoint

_logTrace overwrites the key-P checkpoint with A - X, so totalAssetsAt(P) reports near zero.

4

Assets remain pending withdrawal

The withdrawn assets are still physically in the vault as a pending withdrawal and are included in activePayableAmount.

5

Approve a genuine incident

A genuine incident in that period is assessed and approved by the consortium.

6

Payout is suppressed

payout executes with assetsAtCapturePeriod ≈ 0, so paidAmount = 0.

7

Suppression is permanent

The suppression is permanent for that incident: later blocks append greater keys, no mechanism rewrites key P, and the payout window closes after the incident period and next period.

The attacker can repeat this at every boundary. The redeemer avoids the period’s claim slashing while the protocol still counted their stake as backing cover; cover buyers are left with unpaid claims.

The reverse direction is also possible: a boundary-block deposit or mint inflates totalAssetsAt(P), which inflates _computeAvailableCapacity and allows additional cover to be sold beyond the true period-start collateral.

Impact Details

A permissionless shareholder can permanently suppress legitimate, consortium-approved claim payouts to zero for an entire period. The cover buyer’s claim is not paid even though the assessment and approval were genuine. The underlying assets remain in the vault as pending withdrawals or are withdrawn by the redeemer, so the claim obligation is defaulted while the redeemer escapes the period’s slashing.

In the inflation direction, boundary-block deposits can inflate the period-start collateral value used by CoverOrderAllocator, potentially allowing cover capacity to be sold without real backing assets at the period boundary. This can create over-exposure and further unpaid claims.

This is a direct manipulation of the protocol’s core settlement function. It harms cover buyers and the protocol, requires no trusted-role involvement, and is repeatable at every period boundary.

High - Theft of unclaimed yield, where the yield being stolen is the cover buyer's claim's value

References

  • contracts/core/lib/Checkpoints.sol — Trace256.push

  • contracts/core/FirelightVault.sol — _logTrace, totalAssetsAt, payout, redeem, withdraw, deposit, mint

  • contracts/core/CoverOrderAllocator.sol — _computeAvailableCapacity

Proof of Concept

The header of the PoC below contains placement and run instructions.

Was this helpful?