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:
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:
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.
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
CoverOrderAllocator::commitAllocation: validates and stores the current-period capacity commitment.CoverOrderAllocator::_computeAvailableCapacity: computes staked capacity from the vault's current period-start total assets.FirelightVault::withdrawlines 771-775 andFirelightVault::redeemlines 733-742: write the reduced total-assets trace after requesting withdrawal.FirelightVault::_requestWithdrawlines 1008-1019: moves withdrawn assets into the next withdrawal period and records the pending withdrawal amount.FirelightVault::_logTrace: stores total-assets checkpoints at the current timestamp.Checkpoints::pushlines 203-212: overwrites the existing value when the checkpoint key is unchanged.FirelightVault::payoutlines 900-927: re-reads the capture-period start assets and uses that value as the vault-leg payout cap.
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,000e18canonical 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
MATCHEDfor1,000e18canonical USD against the stored commitment.The approved incident closes with
vaultPaidAmount == 0and 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?