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.solSupporting path:
FirelightVault.sol,IncidentManager.sol,CoverNFT.solCommit:
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:
New-period capacity nevertheless uses the immutable P-start checkpoint:
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
Settle 1,000 USD of period-1 cover backed by 1,000 vault-asset units.
Submit a genuine period-1 incident, which remains payable during period 2.
At period 2 start, the checkpoint records 1,000 units.
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.
Approve the period-1 claim. It drains the vault to zero.
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.
Recommended mitigation
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.
Link to Proof of Concept
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?