Summary
Published Immunefi report #88281.
CoverOrderAllocator calculates the collateral capacity available for matching new cover orders in period P from the vault’s balance at currentPeriodStart(). However, IncidentManager and FirelightVault allow valid incidents from period P - 1 to be reported and paid out during period P from the same live vault assets.
Because no reserve is held for unresolved previous-period exposure when period P capacity is committed and settled, the protocol sells and charges full premiums for period P cover backed by collateral that is subsequently consumed by a valid period P - 1 claim. When a valid period P claim later arrives, the vault has insufficient assets to honor it.
Root cause
In contracts/core/CoverOrderAllocator.sol (v1_audit_ready @ 42f9ea5e43d88b35197b901acc2a8c24b314fa62), _computeAvailableCapacity() reads the vault’s opening checkpoint for the current period without subtracting outstanding prior-period cover liabilities:
uint256 totalAssetsCanonical = Decimals.convert(
$.vault.totalAssetsAt($.vault.currentPeriodStart()),
$.vaultAssetDecimals,
CANONICAL_DECIMALS,
Math.Rounding.Floor
);
uint256 stakedAssetsValueUSDCanonical =
totalAssetsCanonical.mulDiv(assetPriceUSD, 10 ** $.priceFeedDecimals);
uint256 availableCollateralCanonical =
firstLossBufferUSDCanonical + stakedAssetsValueUSDCanonical;
uint256 strictCapacity =
availableCollateralCanonical.mulDiv(config.effectiveLeverage, config.minCAR);
totalAvailableCapacity = strictCapacity.mulDiv(
BPS_DENOMINATOR + config.divergenceToleranceBps,
BPS_DENOMINATOR
);
commitAllocation() locks in that capacity and settleAllocation() issues the new period P cover against it. Meanwhile, FirelightVault.payout() allows claims captured in period P - 1 to execute during period P:
if (!_isPeriodInPayoutWindow(capturePeriod, _currentPeriod)) {
revert InvalidCapturePeriod();
}
uint256 activePayableAmount =
currentActiveAssets + capturePeriodWithdrawals + nextPeriodWithdrawals;
paidAmount = Math.min(amount, Math.min(assetsAtCapturePeriod, activePayableAmount));
Proof of concept
Reproduction sequence at 1x leverage with all protocol roles operating honestly:
- An LP deposits
1,000units intoFirelightVault. - A
1,000-unit cover order is committed and settled for Period 1. - At the start of Period 2,
totalAssetsAt(currentPeriodStart())is still1,000. The allocator commits and settles a new1,000-unit cover order for Period 2, charging the buyer the full premium. - Still within Period 2’s valid reporting window, a genuine Period 1 incident is reported and approved, paying out the
1,000vault units. - A genuine Period 2 incident is then reported and approved. With no first-loss buffer (
FLB = 0),vaultPaidAmount == 0(100% shortfall). With a funded 100-unit first-loss buffer (FLB = 100), the Period 2 buyer receives only100of the promised1,000units (90% shortfall).
Running npx hardhat test test/PoC_ConsecutivePeriodDoubleUse.js --no-compile:
PoC: consecutive-period collateral is used twice
✔ charges and settles period-2 cover before a late period-1 payout drains its backing (FLB=0)
✔ charges and settles period-2 cover before a late period-1 payout drains its backing (FLB=100)
2 passing
Impact
Protocol insolvency: buyers pay full premiums for settled cover whose backing collateral is still encumbered by prior-period exposure and can be drained before their own valid claim is paid.
Recommended fix
Reserve unresolved prior-period cover exposure (while its reporting/payout window remains open) when computing _computeAvailableCapacity() for a new period, or segregate vault capacity by coverage period until the prior period’s claim window closes.