88688 sc high coverorderallocator validates capacity against a stale period start snapshot letting cover settle after its collateral backing is paid out and causing protocol insolvency
Submitted on Aug 14th 2026 at 14:30:34 UTC by @Jornason for Audit Comp | Firelight
Report ID: #88688
Report Type: Smart Contract
Report severity: High
Target: https://github.com/immunefi-team/audit-comp-firelight/blob/v1_audit_ready/contracts/core/interfaces/ICoverOrderAllocator.sol
Impacts:
Protocol insolvency
Description
Brief/Intro
CoverOrderAllocator validates the capacity behind a cover allocation commitment against vault.totalAssetsAt(vault.currentPeriodStart()) — a period-start checkpoint — while the contract's own interface documentation specifies that the matching capacity must be computed from live vault.totalAssets(). A valid prior-period incident payout that executes during the target period reduces live vault collateral but leaves the period-start checkpoint unchanged. Because settleCoverOrder loads the stored commitment and never revalidates live collateral, cover for the target period remains settleable after the assets backing it have already left the vault. A later valid incident against that newly settled cover is then closed by IncidentManager with an underpaid payout, leaving the protocol with settled cover liabilities that exceed its remaining assets: protocol insolvency.
Vulnerability Details
Affected Asset
Program: Audit Comp | Firelight
Asset:
contracts/core/CoverOrderAllocator.sol(interacting withcontracts/core/FirelightVault.solandcontracts/core/IncidentManager.sol, both in scope)Repo: https://github.com/immunefi-team/audit-comp-firelight
Branch/commit:
v1_audit_ready@42f9ea5e43d88b35197b901acc2a8c24b314fa62
Root Cause
The documented off-chain matching flow in ICoverOrderAllocator.sol instructs the matcher to compute capacity from the vault's live asset balance:
The on-chain validation of commitAllocation / recommitAllocation, however, derives capacity from the period-start checkpoint instead of the live balance (CoverOrderAllocator.sol, _computeAvailableCapacity):
Settlement then trusts the stored commitment:
settleCoverOrderloads$.allocationCommitments[order.period]and checks the Merkle proof, the per-market concentration caps (computed fromcommit.totalAvailableCapacity), andcommit.totalSettledCover > commit.totalDeclaredAllocated. At no point does it compare the remaining cover to be settled against the vault's live collateral.FirelightVault.payout()pays out of current live active assets and checkpoints the lower post-payout value (_traceTotalAssets.push(Time.timestamp(), currentActiveAssets - paidFromActive)), but nothing propagates this reduction to the allocator's current-period commitment.
So the sequence "commit at healthy snapshot → valid payout drains live assets → settle against stale snapshot" settles cover whose collateral backing no longer exists. IncidentManager.approveCurrentAssessment then pays a later valid claim via FirelightVault.payout, which caps paidAmount by live assets, and closes the incident regardless of the shortfall:
Exploit Flow
Triggerability
No role acts maliciously, incorrectly, or outside its documented permissions. Every step is a routine, intended protocol action: the curator creates cover orders on agreed terms, the allocator commits and settles a matching result that passes every on-chain guard, and the assessor/approver approves only valid, correctly sized losses. The vulnerable state transition is produced entirely by the contracts: the documented capacity constraint (live vault.totalAssets(), per the interface specification) is bypassed because the enforcement point uses a stale period-start snapshot and settlement never revalidates. This is a protocol-constraint bypass, not a trusted-party misbehavior report: even a perfectly operated underwriting desk settles underbacked cover whenever an ordinary valid payout lands between commitment and settlement.
Why Existing Checks Do Not Prevent It
MatchingCapacityOverflowat commit time does not prevent this because it comparesmatchingCapacityagainst the period-start checkpoint, which is unaffected by payouts executed after the period starts.The settlement-time checks (
InvalidProof,ProtocolConcentrationOverflow,TotalSettledOverflow) do not prevent this because all of them operate on the stored commitment values, never on livevault.totalAssets().FirelightVault.payout()bounding each payout by live assets does not restore solvency; it is precisely what produces the shortfall — it silently pays less than the assessed covered loss and the incident is closed anyway.The first-loss-buffer waterfall does not cover the shortfall in general; the PoC demonstrates the failure with the buffer leg at zero, so the underpayment is attributable solely to the vault-side stale capacity accounting.
Impact Details
Selected impact:
Impact:
Affected funds/assets: the vault's staked collateral (FXRP on Flare) and the cover buyers' valid claims.
Per occurrence: up to the full settled cover of the target period minus the live collateral remaining after the earlier payout. In the executable PoC, a valid 1000-unit covered loss is paid 100 and closed with 900 unpaid — 90% of the valid claim is lost.
Cumulative exposure: repeatable every period; every period in which a prior-period payout executes before that period's cover finishes settling can mint underbacked cover. The vault this code upgrades held approximately 60.16M FXRP of predeposit capital on Flare mainnet at the time of the previous audit (July 2026).
Attacker cost: none — no attacker action is required; ordinary valid protocol operation triggers the insolvency.
Recovery status: none on-chain. The underpaid incident is terminally
CLOSED; there is no function to reopen it, top it up, or compensate the cover buyer. The protocol's settled-cover liabilities exceed its remaining assets with no normal recovery path.
Why this is not merely theoretical:
The full sequence runs against the in-scope contracts at the audit commit and is demonstrated by the executable PoC attached in the PoC field: three tests covering the attack cell, a solvency control where sufficient remaining collateral pays the same claim in full, and a boundary control showing the same commit is correctly rejected when the payout happens before the period starts.
The trigger condition — a valid prior-period incident paying out during the following period — is the normal, documented operation of the payout window (
FirelightVault.payout: "Payout can execute during the incident period or the following period").All three components are explicitly listed as in-scope assets, and the program's stated priority areas include commitment/settlement correctness, capacity/solvency constraints, and interaction between multiple incidents in the same period.
Distinction from the program's known issues:
Not an FLB depeg report: the PoC uses zero FLB balance and a constant 1 USD price; no depeg is involved.
Not an unescrowed-FLB funding report: the underpayment occurs in the vault-asset leg after a valid vault payout; no FLB funding or allowance failure is used.
Not an aggregate-claims-over-cover report: each incident is assessed for no more than its own settled cover, and each cover is claimed at most once. The failure is stale collateral accounting at settlement, not duplicate or excessive claims.
Not a collateral-price-drawdown report: live assets decrease because of a valid payout, not because of any USD price movement; the price feed is constant in the PoC.
Recommendation
Revalidate settlement against live collateral, or invalidate stale commitments, so that settled cover never exceeds live backing:
References
Official scope: https://immunefi.com/audit-competition/audit-comp-firelight-1/scope/
Affected source (audit commit
42f9ea5e43d88b35197b901acc2a8c24b314fa62):contracts/core/interfaces/ICoverOrderAllocator.sol— documented matcher capacity formula using livevault.totalAssets()contracts/core/CoverOrderAllocator.sol—_computeAvailableCapacityusingtotalAssetsAt(currentPeriodStart());settleCoverOrdertrusting stored commitment valuescontracts/core/FirelightVault.sol—payoutcapped by live assets; post-payout checkpointcontracts/core/IncidentManager.sol—approveCurrentAssessmentclosing incidents after partial payout
PoC package and raw runtime output: attached in the Proof of Concept field
Link to Proof of Concept
https://gist.github.com/Jornason/472933656254b8dc7f91a14cd4d7070e
Proof of Concept
Summary
This is an executable runtime PoC, not static analysis. It runs on the project's own Hardhat setup against the in-scope contracts at the audit commit and demonstrates the full insolvency path: cover for a target period is committed while the period-start snapshot shows 1000 collateral units, a valid prior-period payout then reduces live collateral to 100, the committed cover still settles for 1000, and a later valid 1000-unit covered loss is closed with only 100 paid — 900 units of a valid claim are never paid and the incident is terminally closed.
Two control cells isolate the root cause:
The same sequence with sufficient remaining collateral pays in full (C2).
The same commit is correctly rejected when the payout happens before the target period starts (C3).
Environment
The PoC uses only the repository's own test helpers (test/setup/fixtures.js) and the repository's MockAggregatorV3 with a constant 1 USD answer for deterministic accounting. The bug is price-independent: the underpayment occurs in native vault-asset units with a constant price, so no oracle or depeg assumption is involved. The first-loss-buffer wallet holds a zero balance throughout, so the shortfall is attributable solely to vault-side stale capacity accounting.
Files
Reproduction Steps
Expected Output
C1 (attack / impact cell):
C2 (solvency control):
C3 (boundary control):
Key invariants (C1):
What the PoC Proves
A target-period allocation commitment passes on-chain validation against the period-start checkpoint while live collateral equals the snapshot.
A valid prior-period incident approved during the target period pays 900 out of the vault through the normal
approveCurrentAssessment→FirelightVault.payoutpath, reducing live assets to 100 whiletotalAssetsAt(targetPeriodStart)remains 1000.settleCoverOrderstill settles the committed 1000 cover because no settlement-time check compares the settled amount against live collateral.A later valid incident assessed for exactly the settled cover amount, 1000, is paid only 100 and is terminally closed (
IncidentStatus.CLOSED), leaving 900 of a valid covered loss permanently unpaid.C2 proves the shortfall is caused by collateral depletion, not by the assessment path: with 2000 starting assets the identical sequence pays the later claim in full.
C3 proves the vulnerable window is precisely the post-period-start payout: when the payout happens before the period starts, the checkpoint is low and
commitAllocationreverts withMatchingCapacityOverflowfor the same 1000 capacity.
PoC Source
Raw Runtime Evidence
Raw passing output for each isolated cell run is included with this report's local package (evidence/aecgiv_c1_runtime.raw.txt, evidence/aecgiv_c2_runtime.raw.txt, evidence/aecgiv_c3_runtime.raw.txt), each with EXIT_CODE=0 and exactly one 1 passing. The commands above reproduce this output from a clean checkout.
Was this helpful?