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

88338 sc high stale vault capacity lets firelight sell cover against collateral owed to prior claims

#88338 [SC-High] Stale vault capacity lets Firelight sell cover against collateral owed to prior claims

Submitted on Aug 12th 2026 at 19:38:17 UTC by @dtutila for Audit Comp | Firelight

  • Report ID: #88338

  • 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

Stale vault capacity lets Firelight sell cover against collateral owed to prior claims

Target

  • Main affected contract: CoverOrderAllocator.sol

  • Supporting path: FirelightVault.sol, IncidentManager.sol, CoverNFT.sol

  • Commit: 42f9ea5e43d88b35197b901acc2a8c24b314fa62

Summary

CoverOrderAllocator calculates new-period capacity from the vault's immutable period-start checkpoint. However, a claim from the previous period remains payable during the new period and draws from the same live vault assets.

The previous-period liability is never reserved. Firelight can therefore collect a premium and mint a CoverNFT even though the collateral authorizing that cover is still owed to an earlier claim. The PoC demonstrates a buyer paying for 1,000 USD of cover that later receives a zero payout.

Impact

Critical — Protocol insolvency.

The same collateral simultaneously backs a payable period-P-1 claim and newly settled period-P cover. The earlier payout can leave outstanding cover obligations greater than vault assets. The shortfall can reach the full period-start vault balance.

Root cause

A P-1 claim remains payable during P:

Source

New-period capacity nevertheless uses the immutable P-start checkpoint:

Source

Later payout checkpoints do not rewrite the historical value at currentPeriodStart(). The commitment guard therefore accepts capacity derived from collateral still payable to the earlier claim:

Settlement then transfers the premium and mints the NFT without checking live vault assets or reserving unresolved previous-period exposure (source).

Attack sequence

  1. Settle 1,000 USD of period-1 cover backed by 1,000 vault-asset units.

  2. Submit a genuine period-1 incident, which remains payable during period 2.

  3. At period 2 start, the checkpoint records 1,000 units.

  4. Commit and settle 1,000 USD of period-2 cover. The vault still holds 1,000 live units, so every existing check passes; the buyer pays the premium and receives the CoverNFT.

  5. Approve the period-1 claim. It drains the vault to zero.

  6. A genuine claim against the period-2 cover closes with a zero payout.

Privileged roles

No privileged role must act maliciously or incorrectly. The engine commits against the documented period-start snapshot, while the curator and approver process genuine claims through the intended workflow. The defect is the missing on-chain reservation for overlapping liabilities.

Proof of concept

The PoC uses the real upgradeable FirelightVault, CoverOrderAllocator, CoverNFT, and IncidentManager. Only the ERC-20 and constant $1 oracle are mocked. It runs on Hardhat's isolated local network and does not access a public chain. The Phase 2 contracts are not yet deployed, so no Phase 2 mainnet fork is available.

Attach C04StaleCapacityPayoutAfterSettlement.test.js, then run:

Expected output:

Why this is not a known issue

  • External first-loss buffer: that issue concerns a correctly read live balance in an external, unescrowed wallet that is later withdrawn. This finding concerns the seperate vault leg, whose historical balance is reused after it becomes encumbered by a prior claim. Escrowing the buffer does not fix the stale vault checkpoint.

  • Aggregate claims are not capped: the PoC uses two different CoverNFTs in two periods, with one valid claim per NFT. Claims do not exceed cover sold; cover sold exceeds available collateral.

  • Staked-asset price drawdown: the oracle remains fixed at $1. Vault-asset quantity is removed by Firelight's own payout(), not by market depreciation.

Subtract all still-payable previous-period cover exposure—not only incidents already created—from new-period capacity. Alternatively, block commitment and settlement until the previous period's claim window closes. A live-balance check alone is insufficient because the earlier payout can occur after new cover is settled.

https://gist.github.com/dtutila/bf2e3cbaed1de08445ab0f984795f26b

Proof of Concept

const { expect } = require('chai') const { ethers, upgrades } = require('hardhat') const { time } = require('@nomicfoundation/hardhat-network-helpers') const { StandardMerkleTree } = require('@openzeppelin/merkle-tree') const { deployVault } = require(process.cwd() + '/test/setup/fixtures.js')

const DAY = 24n * 60n * 60n const YEAR = 365n * DAY const BPS = 10_000n const ceilDiv = (x, y) => (x + y - 1n) / y

describe('C-04B: prior-period payout AFTER settlement', function () { it('sells and settles new cover against full live collateral, which a prior-period claim then drains', async function () { const vaultCtx = await deployVault({ initial_deposit_limit: ethers.parseUnits('1000000', 6), period_configuration_duration: Number(DAY), }) const [deployer, admin, curator, engine, configAdmin, approver, rejecter, invalidator, payoutAdmin, priceAdmin, buyer, premiumCollector, payoutReceiver, firstLossBuffer] = await ethers.getSigners() const vault = vaultCtx.firelight_vault const vaultAsset = vaultCtx.token_contract

}) })

Was this helpful?