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:
Period
Pbegins with$Xof vault collateral.An unresolved incident from
P-1is paid duringP, consuming some or all of that$X.commitAllocation(P)is called afterwards._computeAvailableCapacity()still counts the collateral that existed at the beginning ofP, including assets that have already been paid out.New cover for
Pcan 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:
The real vault starts in period
0.$100is deposited duringP0, so it is already present at the start ofP1.A legitimate incident is captured during
P1, making the$100part of that incident's exposure cap.During
P1, a$100cover order is created forP2.P2begins with the same$100, sototalAssetsAt(P2Start) == $100.The unresolved
P1incident is paid duringP2, draining the live vault to$0.totalAssetsAt(P2Start)remains$100because the payout checkpoint is written afterP2Start.commitAllocation(P2)therefore accepts$100of capacity and theP2order settles asMATCHEDwhilevault.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.
Recommended Mitigation
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:
Reserve unresolved previous-period exposure from current-period capacity. Compute the amount of vault collateral that can still be consumed by
P-1incidents and subtract that reserved amount from the collateral available to underwriteP.Block current-period commitment/settlement while previous-period liabilities remain unresolved. Since
FirelightVaultalready tracks active incidents across the current and previous periods, underwriting could be prevented until the previous-period liability reaches a terminal state.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?