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

88237 sc high previous period payouts do not reduce current period cover capacity allowing undercollateralized cover to be sold

Submitted on Aug 12th 2026 at 10:28:41 UTC by @CarlosMB for Audit Comp | Firelight

  • Report ID: #88237

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Protocol insolvency

Description

Summary

CoverOrderAllocator computes the amount of cover that can be allocated during the current period using the vault's asset balance at the start of that period:

uint256 totalAssetsCanonical = Decimals.convert(
    $.vault.totalAssetsAt($.vault.currentPeriodStart()),
    $.vaultAssetDecimals,
    CANONICAL_DECIMALS,
    Math.Rounding.Floor
);

However, FirelightVault explicitly allows incidents captured during the previous period to be paid during the current period.

Such a payout removes real collateral from the vault and checkpoints the reduced balance, but it does not modify the historical totalAssetsAt(currentPeriodStart()) value used by CoverOrderAllocator.

Consequently, the following sequence is valid:

  1. Period P begins with $X of vault collateral.

  2. An unresolved incident from P-1 is paid during P, consuming some or all of that $X.

  3. commitAllocation(P) is called afterwards.

  4. _computeAvailableCapacity() still counts the collateral that existed at the beginning of P, including assets that have already been paid out.

  5. New cover for P can therefore be committed and settled against collateral that no longer exists.

In the extreme case, a previous-period payout can reduce the vault from $100 to $0, while the allocator still allows $100 of new current-period cover to be committed.

The protocol therefore permits outstanding cover liabilities to exceed the collateral actually available to satisfy them.

Vulnerability Details

The issue results from an inconsistency between the accounting horizon used for underwriting and the payout window supported by the vault.

1. Current-period capacity uses a historical period-start snapshot

In CoverOrderAllocator._computeAvailableCapacity():

Relevant location:

contracts/core/CoverOrderAllocator.sol:696-725

The important line is:

The resulting value is used to bound matchingCapacity in _validateMatchingCapacity():

Relevant location:

contracts/core/CoverOrderAllocator.sol:394-403

Once accepted, the historical capacity is stored in the commitment:

Relevant location:

contracts/core/CoverOrderAllocator.sol:350-354

2. Previous-period incidents remain payable during the current period

FirelightVault.payout() intentionally accepts payouts from both the current period and the immediately preceding period:

The payout window is:

Relevant locations:

contracts/core/FirelightVault.sol:900-907

contracts/core/FirelightVault.sol:1130-1132

Therefore, after period P starts, an incident from P-1 can legitimately consume collateral that was present when period P began.

3. The payout reduces live assets but cannot change the historical period-start snapshot

The vault computes the payout using currently payable collateral:

It then reduces current active assets:

and transfers the collateral:

Relevant location:

contracts/core/FirelightVault.sol:909-962

The new checkpoint is created at the payout timestamp, which is after the start of the current period.

It therefore does not alter:

As a result, after the payout it is possible to simultaneously have:

CoverOrderAllocator uses the second value.

4. New cover can consequently settle using collateral that has already been paid out

commitAllocation() validates the submitted allocation against the stale period-start value:

and stores it:

Settlement subsequently uses that stored capacity rather than the vault's remaining collateral:

The order can therefore become MATCHED even though the vault assets supposedly backing the cover have already been consumed by the previous-period incident.

The ordering can also be reversed: if commitAllocation(P) executes before the previous-period payout, the commitment stores the period-start capacity in commit.totalAvailableCapacity. A later P-1 payout can then remove the live collateral before settlement, and settleCoverOrder() still does not re-check the vault's live backing. Therefore the issue is not limited to the narrow payout -> commit -> settle ordering.

Impact

The issue can cause protocol insolvency.

Consider a configuration with no funded FLB and a 1x effective vault-backed capacity for simplicity.

At the beginning of period P:

A valid incident captured during P-1 is subsequently approved during P:

Nevertheless:

A current-period allocation can therefore still be committed for:

The resulting state is:

This is not merely an incorrect displayed capacity. The inflated value passes the on-chain allocation constraints and can be used to settle real cover orders.

If a legitimate incident subsequently affects that newly-issued cover, the vault can pay zero because payout() caps payment by currently payable assets:

When:

the result is:

This zero payout is explicitly permitted by the vault.

Furthermore, IncidentManager._executePayout() accepts the returned zero payment:

and approveCurrentAssessment() subsequently closes the incident:

Relevant locations:

contracts/core/IncidentManager.sol:423-438

contracts/core/IncidentManager.sol:672-689

Therefore a buyer can hold valid, settled cover for which the backing collateral had already been consumed before that cover was committed.

This maps directly to the contest's accepted Critical — Protocol insolvency impact.

Why this does not require a malicious privileged actor

commitAllocation() is permissioned, but exploitation does not require the allocator to exceed its documented authority or submit an intentionally invalid allocation.

The allocator can follow the intended workflow and use the maximum capacity returned by the protocol.

The problematic sequence consists exclusively of legitimate operations:

The contract itself reports the already-consumed collateral as available capacity.

The privileged role is therefore not the source of the vulnerability; the incorrect capacity invariant is.

Proof of Concept

Proof of Concept

The following PoC uses the repository's production FirelightVault and production CoverOrderAllocator together in a single end-to-end test. No allocator-vault state is injected through MockCoverOrderAllocatorVault.

The timeline is intentionally aligned with the real vault's period numbering:

  1. The real vault starts in period 0.

  2. $100 is deposited during P0, so it is already present at the start of P1.

  3. A legitimate incident is captured during P1, making the $100 part of that incident's exposure cap.

  4. During P1, a $100 cover order is created for P2.

  5. P2 begins with the same $100, so totalAssetsAt(P2Start) == $100.

  6. The unresolved P1 incident is paid during P2, draining the live vault to $0.

  7. totalAssetsAt(P2Start) remains $100 because the payout checkpoint is written after P2Start.

  8. commitAllocation(P2) therefore accepts $100 of capacity and the P2 order settles as MATCHED while vault.totalAssets() == 0.

Create:

with:

Run:

The important final state is:

The $100 counted by commitAllocation() is the same collateral that the previous-period payout has already transferred out of the vault.

The protocol must prevent collateral that is still exposed to unresolved liabilities from P-1 from simultaneously being counted as backing for new P cover.

Simply subtracting previous-period payouts when commitAllocation() runs is not sufficient, because the unsafe ordering can also be:

A robust fix should therefore enforce the invariant for the entire matching/settlement lifecycle, not only at commit time.

Possible approaches include:

  1. Reserve unresolved previous-period exposure from current-period capacity. Compute the amount of vault collateral that can still be consumed by P-1 incidents and subtract that reserved amount from the collateral available to underwrite P.

  2. Block current-period commitment/settlement while previous-period liabilities remain unresolved. Since FirelightVault already tracks active incidents across the current and previous periods, underwriting could be prevented until the previous-period liability reaches a terminal state.

  3. Invalidate or resize outstanding commitments when a previous-period payout occurs. If concurrent underwriting is required, a payout that reduces live backing must also prevent pre-existing commitments from settling above the newly available capacity.

The core invariant should be:

Any fix must cover both commitAllocation() and recommitAllocation(), and must also ensure that an already-created commitment cannot remain settleable after a later previous-period payout removes its backing.

Additional Notes

This issue is distinct from the disclosed first-loss-buffer custody issue because the PoC requires no FLB balance at all and relies exclusively on vault collateral.

It is also distinct from cumulative/duplicate assessment-loss issues. The two losses can correspond to separate legitimate cover periods and independently valid incidents.

No oracle depeg, malicious token, malicious privileged role, duplicate claim, or abnormal ERC-20 behavior is required.

Was this helpful?