90014 sc medium exact timestamp withdrawals at period boundaries permanently corrupt total asset snapshots truncating claim payouts and dos ing cover underwriting
Submitted on Aug 22nd 2026 at 13:58:53 UTC by @hunraj for Audit Comp | Firelight
Report ID: #90014
Report Type: Smart Contract
Report severity: Medium
Target: https://github.com/immunefi-team/audit-comp-firelight/blob/v1_audit_ready/contracts/core/FirelightVault.sol
Impacts:
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Contract fails to deliver promised returns, but doesn't lose value
Description
Executive Summary
The Firelight protocol relies on exact-timestamp snapshots of vault assets to determine underwriting capacity and cap incident payouts for a given cover period. However, a fundamental conflict exists between the vault's delayed withdrawal mechanics and the OpenZeppelin Checkpoints library used to record historical state.
If a user executes a withdraw() or redeem() at the exact second a period begins (block.timestamp == vault.currentPeriodStart()), the transaction overwrites the critical period-start checkpoint with a post-withdrawal balance. This permanently and unrecoverably corrupts the period's snapshot, causing IncidentManager to artificially truncate valid claim payouts (resulting in a direct loss of funds for cover buyers and evasion of slashing for stakers) and causing CoverOrderAllocator to severely under-allocate underwriting capacity (causing a Denial of Service on the protocol's business operations).
Vulnerability Description
In FirelightVault.sol, withdrawals are delayed. When a user requests a withdrawal, their shares are burned immediately, and the underlying assets are moved to pendingWithdrawAssets.
Crucially, totalAssets() subtracts these pending assets dynamically:
After updating balances, the vault pushes the new state to its historical trace via _logTrace():
The underlying library (core/lib/Checkpoints.sol) enforces that if multiple checkpoints are pushed with the exact same timestamp key, the last value overwrites the previous ones:
If a withdrawal is executed at exactly block.timestamp == vault.currentPeriodStart(), the checkpoint pushed by the withdrawal shares the exact same timestamp key as the boundary itself. Consequently, the period's initial snapshot is permanently overwritten by the artificially lowered post-withdrawal balance.
Root Cause Analysis & Impact
The corruption of this specific checkpoint has catastrophic downstream effects on the two core functions of the protocol.
1. Payout Truncation & Slashing Evasion (High Impact) In IncidentManager.sol, when an approved loss is paid out, the protocol caps the payout to protect the vault:
Even though the attacker's pending withdrawal correctly remains in capturePeriodWithdrawals (and thus activePayableAmount is correct), the assetsAtCapturePeriod variable is strictly capped by the corrupted snapshot.
Result: Legitimate payouts to cover buyers are artificially truncated. The cover buyers suffer a permanent loss of funds they are owed. Simultaneously, because the total slashed amount is reduced, the attacker's pending withdrawal suffers a smaller pro-rata slash, providing a direct financial incentive to execute the attack.
2. Underwriting Capacity DoS (Medium/High Impact) In CoverOrderAllocator._computeAvailableCapacity():
The underwriting capacity for the month is calculated using the same corrupted snapshot. If the off-chain engine pre-matched orders expecting the actual collateral capacity, commitAllocation() will revert with MatchingCapacityOverflow. The protocol's capacity is artificially suppressed for the entire period.
Preconditions
A staker (malicious or innocent) must initiate a
withdraw()orredeem()in a block whereblock.timestamp == vault.currentPeriodStart().On EVM-compatible chains (including Flare, which has fast 1-to-2 second block times), an attacker can guarantee this condition trustlessly using a smart contract wrapper:
require(block.timestamp == targetTimestamp);. If the block timestamp does not match, the transaction cheaply reverts, allowing the attacker to try again without risk.
Step-by-Step Reproduction
FirelightVaultholds 2,000,000 USDC.Period 2 starts exactly at
T = 1750000000.An attacker deploys a smart contract that calls
vault.withdraw(500,000 USDC)with arequire(block.timestamp == 1750000000)constraint.The transaction is mined exactly at
T.totalAssets()drops to 1,500,000 USDC. TheCheckpointstrace for keyTis overwritten with1,500,000.Later, an incident occurs requiring a 1,800,000 USDC payout.
IncidentManager.payout()caps the payout usingtotalAssetsAt(T), which returns 1,500,000.Cover buyers are shortchanged by 300,000 USDC. The attacker evades their full pro-rata slashing obligation.
Proof of Concept
Proof of Concept
Save the following test file as test/PoC_BoundaryOverwrite.js and execute it via npx hardhat test test/PoC_BoundaryOverwrite.js.
Security Recommendations
Relying on chronological state-change checkpoints to represent exact time-boundary snapshots is inherently unsafe when user actions can occur at the same timestamp.
Recommendation 1: Explicit Period Snapshotting Instead of querying totalAssetsAt(timestamp) via the generalized OpenZeppelin Checkpoints trace, implement an explicit mapping for period boundaries:
Ensure this mapping is updated strictly before any state changes apply if transitioning into a new period, or cached securely during a dedicated boundary-transition function.
Recommendation 2: Snapshot Modification in _computeAvailableCapacity & payout If retaining the existing Checkpoints structure, modify the architecture so that pending withdrawals initiated during the boundary block are mathematically reconstructed or ignored when querying boundary values, ensuring that totalAssetsAt(periodStart) always reflects the pre-action block state.
Was this helpful?