88704 sc high boundary block redemption rewrites the collateral snapshot after capacity is committed reducing a valid claim by exactly the redeemed amount
Submitted on Aug 14th 2026 at 15:57:27 UTC by @manvi for Audit Comp | Firelight
Report ID: #88704
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
I have analysed the interaction between Firelight's period-start capacity accounting, delayed withdrawals, allocation commitments, and incident payouts and observed that the collateral value used as the semantic start-of-period exposure snapshot is mutable after an allocation commitment has already relied on it.
CoverOrderAllocator validates the current period's matching commitment against:
vault.totalAssetsAt(vault.currentPeriodStart())FirelightVault.payout() later uses the same period-start historical lookup as an outer cap on how much vault collateral can be paid for a valid incident captured in that period.
However, redeem() and withdraw() move assets into delayed-withdrawal accounting and then checkpoint the reduced active-vault balance at block.timestamp. At the exact period boundary, that write changes what future totalAssetsAt(periodStart) reads return.
Therefore, if the honest allocation commitment executes first in a block whose timestamp is the exact start of period P, an unprivileged staker can redeem afterward in the same block:
The redeemed assets do not leave the vault. They enter withdrawAssets[P + 1], which the payout logic explicitly keeps slashable for period-P incidents. Nevertheless, the rewritten period-start value is applied as an outer cap and prevents some or all of those still-present assets from being used.
The final E2E proves this across three attacker ownership levels:
For every tested ownership level:
The 100% case is only the maximal instance. The vulnerability does not depend on the attacker owning the whole vault.
Vulnerability Details
1. Allocation commitment relies on a mutable period-start lookup
CoverOrderAllocator._computeAvailableCapacity() values vault collateral using:
commitAllocation() validates the resulting capacity and stores the accepted commitment, but does not bind that commitment to an immutable vault snapshot.
Settlement later uses the stored commitment and does not recompute the period-start vault value before charging premium and minting the CoverNFT.
2. Redemption moves assets into a slashable bucket and checkpoints the lower active balance
redeem() / withdraw() call _requestWithdraw() and then checkpoint the reduced active-vault balance:
_logTrace() writes at the current timestamp:
At the first instant of period P, _requestWithdraw() assigns the redeemed assets to currentPeriod() + 1, i.e. withdrawAssets[P + 1].
Those assets remain physically in the vault and are still part of the withdrawal accounting that payout() intends to slash.
3. The period-start historical value can change after commitment
Checkpoints.Trace256.push() replaces the latest value when the same timestamp key is reused:
Even where no checkpoint existed exactly at the period boundary before redemption, inserting a checkpoint at that boundary changes the historical lookup from the inherited pre-boundary value to the new post-redemption value.
Thus the same semantic timestamp can be observed differently by commitment and payout:
4. payout() caps by the rewritten historical value before using slashable withdrawals
FirelightVault.payout() includes delayed-withdrawal buckets:
But applies assetsAtCapturePeriod as an outer cap:
Let:
After redemption:
For a full vault-leg claim of T:
The claim is therefore underpaid by exactly the amount moved into the supposedly slashable delayed-withdrawal path.
Ownership sweep — no 100% vault ownership required
The final E2E runs isolated 10%, 50%, and 100% ownership scenarios:
The protocol-side shortfall is always:
and the attacker's own preserved amount is:
Therefore a positive attacker benefit exists below dominant ownership. Dominant ownership is only required for the maximal case where the attacker personally preserves the whole shortfall.
Attack sequence
Prepare the boundary redemption
The attacker owns a non-zero fraction of vault shares.
The attacker grants share allowance to a helper contract; the helper owns zero shares.
Unauthorized helper callers revert.
Wrong-time helper execution reverts without spending shares or allowance.
Immediately before period P, the price feed is refreshed.
Commit capacity and redeem at the period boundary
The honest allocator commitment is submitted.
The attacker's conditional redemption is submitted afterward.
Both transactions are included in a block whose timestamp is exactly start(P).
The commitment executes first and accepts full 110 capacity.
The redemption executes second.
Redeemed assets enter withdrawAssets[P + 1].
The full physical vault balance remains inside the vault.
totalAssetsAt(start(P)) is reduced by exactly the redeemed amount.
Settle cover and process the valid loss
Settlement succeeds against the stored commitment.
Premium is collected and a real CoverNFT is minted.
The valid loss occurs only after settlement.
Incident is created, confirmed, assessed for 110, submitted, and approved.
Funded FLB correctly pays 10.
The vault leg is underpaid by exactly the boundary redemption.
The incident still becomes CLOSED.
Why the known FLB issue does not apply
Every scenario uses:
The shortfall occurs entirely in the vault leg.
Why this is not the retroactive-cover / delayed-premium issue
The buyer is funded and approved before matching.
The policy settles before the loss.
The attack does not depend on post-loss approval.
Why this is not the cross-period liability-reservation finding
These issues violate different invariants and require different fixes.
Cross-period liability reservation:
Boundary checkpoint mutation:
A liability reserve does not fix a current-period snapshot that can be rewritten, and an immutable current-period snapshot does not reserve previous-period liabilities.
Why this is distinct from post-payout stale capacity
Post-payout stale capacity:
Boundary checkpoint mutation:
Why this does not require privileged-role misconduct
The value-moving adversarial action is an ordinary staker redeem().
The attacker needs no privileged role.
The honest allocator simply submits a commitment that passes the on-chain capacity check.
Incident roles later process a valid post-settlement loss and an assessment exactly within settled cover.
No privileged actor needs to fabricate a loss, knowingly misstate capacity, or make an intentionally incorrect business decision.
Feasibility limitation
The exploit requires:
and:
The E2E proves this state transition locally by asserting:
The helper also proves:
The remaining operational question is whether Firelight's production matcher can place a legitimate commitAllocation() transaction in the exact period-start block.
If it always waits for that block to finalize before broadcasting, this exploit path is unreachable.
If it broadcasts/prepositions for first-boundary-block inclusion, or otherwise permits the commitment to land there, the demonstrated attack is reachable.
I am explicitly disclosing this condition for triage.
Impact Details
The selected impact is:
Protocol insolvency
The final E2E demonstrates:
In every scenario:
FLB is funded and pays normally;
buyer is funded and approved before matching;
cover settles before the loss;
assessment is within settled cover;
the vault-leg shortfall equals the boundary redemption;
the shortfall assets remain physically present;
and the incident still closes.
The premium amount is not relied upon as the impact. Premium collection only proves normal policy settlement.
The maximal demonstrated case is:
Recommended remediation
Bind allocation commitment and payout to one immutable per-period collateral snapshot.
For example:
Finalize the snapshot once per period and never allow later state changes to reinterpret it.
Both:
should consume the same immutable value.
It is also reasonable to bind AllocationCommitment directly to the snapshot value used during matching.
Blocking only withdraw() / redeem() at the boundary is weaker because another path that writes _traceTotalAssets at that timestamp could recreate the same failure.
References
Primary affected asset:
contracts/core/FirelightVault.sol
Related in-scope contracts:
contracts/core/CoverOrderAllocator.solcontracts/core/IncidentManager.solcontracts/core/lib/Checkpoints.solcontracts/core/interfaces/IFirelightVault.solcontracts/core/interfaces/ICoverOrderAllocator.sol
Relevant logic:
CoverOrderAllocator._computeAvailableCapacity()— readsvault.totalAssetsAt(vault.currentPeriodStart()).CoverOrderAllocator.commitAllocation()— validates and stores the commitment.CoverOrderAllocator._settleCoverOrder()— settles against the stored commitment without recomputing the mutated period-start value.FirelightVault.redeem()/withdraw()— create delayed withdrawals and checkpoint reduced active assets.FirelightVault._requestWithdraw()— assigns redemptions tocurrentPeriod() + 1.FirelightVault._logTrace()— writes_traceTotalAssetsatTime.timestamp().Checkpoints.Trace256.push()— replaces/redefines a same-key checkpoint.FirelightVault.totalAssetsAt()— historical lookup reused by capacity and payout semantics.FirelightVault.payout()— includes delayed withdrawals but caps payout byassetsAtCapturePeriod.IncidentManager._executePayout()— pays FLB first and asks the vault for the remainder.IncidentManager.approveCurrentAssessment()— completes payout and closes the incident.
Proof of Concept
PoC classification
This is a repo-native E2E integration PoC using the real in-scope:
FirelightVault;CoverOrderAllocator;CoverNFT;IncidentManager.
Only deterministic external dependencies are mocked:
MockERC20;MockAggregatorV3.
The helper owns no vault shares. The attacker remains the owner and grants only ERC20 share allowance.
The PoC uses:
Helper contract
Complete E2E PoC
Command
I ran:
Observed output
What the PoC proves
Helper and boundary execution conditions
The helper owns zero shares.
The attacker remains share owner and grants only allowance.
Unauthorized helper calls revert.
Wrong-time helper calls revert without spending shares or allowance.
Commitment and redemption execute in the exact same boundary block.
Commitment executes first.
Redemption executes second.
Snapshot mutation and settlement
The commitment records full pre-redemption capacity.
Redemption lowers totalAssetsAt(periodStart) by exactly R.
Redeemed assets never leave the vault.
Redeemed assets remain in delayed-withdrawal accounting intended to stay slashable.
Settlement still succeeds against the stored commitment.
Premium is collected.
Payout impact and attacker benefit
Loss occurs only after settlement.
Assessment is valid and within settled cover.
Funded FLB pays normally.
Vault-leg shortfall equals R.
Incident becomes CLOSED despite that shortfall.
The same protocol-side invariant holds at 10%, 50%, and 100% attacker ownership.
Attacker personally preserves floor(R² / T).
Attacker benefit remains positive below dominant ownership.
This demonstrates a mutable current-period collateral exposure snapshot, not a trusted-role business decision, FLB configuration issue, delayed-premium issue, stale-capacity issue, or cross-period liability-reservation issue.
Was this helpful?