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

89122 sc high same block period start withdrawal can lower the payout exposure cap after cover capacity is committed

Submitted on Aug 17th 2026 at 10:49:29 UTC by @abarbatei for Audit Comp | Firelight

  • Report ID: #89122

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/immunefi-team/audit-comp-firelight/blob/v1_audit_ready/contracts/core/FirelightVault.sol

  • Impacts:

    • Protocol insolvency

Description

Brief/Intro

At the start of a cover period, committed cover capacity is computed from the vault's period-start active assets. A staker can withdraw or redeem after that capacity commit in the same block when the block timestamp equals the period start, causing the vault to overwrite the same period-start total-assets checkpoint with a lower value. Later incident payouts for the same period re-read that lower checkpoint as the payout exposure cap, so cover can be settled against assets that are no longer charged for the approved vault-leg payout.

Vulnerability Details

Normal behavior is that the same period-start vault exposure should govern both cover capacity and the later vault payout cap. Capacity can be committed once for the current period, settlement then uses the stored commitment, and an approved payout is capped by the assets exposed at the incident capture period start.

The root cause is that the vault does not freeze the total-assets checkpoint once another contract has consumed it as a period-start economic snapshot. The capacity helper reads the vault's current period-start total assets while validating the matching capacity:

CoverOrderAllocator::_computeAvailableCapacity lines 710-716

The capacity value is then stored in the period commitment and is not tied to a frozen vault snapshot:

CoverOrderAllocator::commitAllocation lines 348-356

A later withdrawal request in the same block burns shares, assigns the assets to a future withdrawal period, and records them as pending withdrawal assets:

FirelightVault::_requestWithdraw lines 1008-1019

Both FirelightVault::redeem lines 733-742 and FirelightVault::withdraw lines 771-775 then call the same trace logger with the reduced total active assets. The trace key is the current block timestamp:

FirelightVault::_logTrace

The checkpoint library overwrites the last stored value when the new key equals the last key, and historical reads return the latest checkpoint at or before the queried key:

Checkpoints::push lines 203-212

Checkpoints::upperLookupRecent

This makes the attack path possible:

1

A period begins at timestamp T.

2

Capacity is committed

The allocator commits capacity for that period before the attacker in the same block. The committed capacity includes the attacker's active vault assets because the period-start read still includes them.

3

The attacker requests a withdrawal or redemption

The attacker calls FirelightVault::withdraw lines 771-775 or FirelightVault::redeem lines 733-742 later in the same block. The vault writes a lower total-assets trace value at key T.

4

Cover orders settle against the stored commitment

Settlement reads the stored committed capacity rather than recomputing the vault period-start checkpoint:

CoverOrderAllocator::_settleCoverOrder lines 500-510

5

An approved incident re-reads the rewritten checkpoint

If a legitimate incident for that period is later approved, the payout path re-reads the capture-period start asset checkpoint and uses it as the cap, even though the pending withdrawal pools are still counted as payable assets:

FirelightVault::payout lines 900-927

The important detail is that the withdrawal pool is present in the payable-assets calculation. The underpayment comes from the rewritten period-start cap. If period-start active assets were A and the attacker withdrew W after the capacity commit in the same timestamp, future vault-leg payout capacity for that period can be reduced from A to A - W. In the extreme case where W == A, the vault-leg payout can be capped to zero while cover was settled against those same vault assets and the attacker's delayed withdrawal remains claimable after the payout window.

Required preconditions are narrow but attacker-reachable. The attacker is an untrusted staker with vault shares and can submit a withdrawal or redemption transaction. A block timestamp must equal the vault period start, and the allocator's honest capacity commit must execute earlier in that same block. The allocator, settlement, and incident roles do not need to act maliciously; they only need to perform the documented matching, settlement, and legitimate payout lifecycle.

Impact Details

The impact is protocol insolvency. Settled cover can be accepted using a period-start capacity snapshot that includes the attacker's assets, but a later approved vault payout for that same cover period can be capped by a rewritten lower snapshot that excludes those assets. The protocol can therefore owe an approved cover payout that the vault path refuses to pay, while the attacker can later claim the delayed withdrawal that backed the committed cover capacity.

The maximum shortfall is bounded by the amount of same-timestamp active assets withdrawn after the capacity commit and by the approved vault-leg payout amount. This is not merely a temporary withdrawal delay or a trusted-role configuration issue: the mismatch is created by an untrusted staker action that changes a historical exposure checkpoint after another in-scope contract already relied on it.

Mitigation

Use one immutable per-period exposure snapshot for both capacity and payout. For example, store periodStartTotalAssets[period] when a period is first used for capacity, reject later changes to that value, and have both capacity validation and vault payout read that stored per-period value.

Alternatively, prevent the total-assets trace from overwriting an existing checkpoint at a period boundary once that boundary can be consumed as an economic snapshot. A same-timestamp withdrawal should not be able to change the value returned for a period-start timestamp that capacity validation may already have read.

References

Proof of Concept

Setup assumptions

Place the proof file at test/poc/boundary-withdrawal-rewrites-period-start-exposure/ProofBoundaryWithdrawalRewritesPeriodStartExposure.t.sol in the target repository. It deploys the in-scope FirelightVault, CoverOrderAllocator, CoverNFT, and IncidentManager implementations through ERC1967 proxies, using the target contracts and target OpenZeppelin imports. No target source file needs to be edited.

The proof uses the pinned commit 42f9ea5e43d88b35197b901acc2a8c24b314fa62. The test constructs a one-day vault period, creates a next-period cover order, commits allocation capacity at the exact period-start timestamp, has the staker withdraw in the same timestamp, settles the order against the stored commitment, approves an incident assessment, and then asserts that the vault leg pays zero while the staker can later claim the delayed withdrawal.

Execution command

Execution result

Forge compiled 66 files with Solidity 0.8.28, ran the single proof test, and reported 1 passed; 0 failed; 0 skipped.

Proof source

Final impact assertions

The passing proof's final assertions check the accepted impact directly:

  • The allocator stores 1,000e18 canonical USD of committed capacity from the original period-start checkpoint.

  • The same-timestamp staker withdrawal rewrites totalAssetsAt(periodStart) to zero.

  • The cover order still settles as MATCHED for 1,000e18 canonical USD against the stored commitment.

  • The approved incident closes with vaultPaidAmount == 0 and the payout receiver gets zero vault assets.

  • The staker's withdrawal for the covered period remains payable and is claimable after the payout window, returning the assets that backed the settled cover capacity.

Was this helpful?