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

89315 sc high boundary timestamp withdrawal corrupts the period payout cap allowing a shareholder to evade slashing and permanently underpay an approved claim

#89315 [SC-High] Boundary-timestamp withdrawal corrupts the period payout cap, allowing a shareholder to evade slashing and permanently underpay an approved claim

Submitted on Aug 18th 2026 at 14:50:17 UTC by @Bug73803 for Audit Comp | Firelight

  • Report ID: #89315

  • 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:

    • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Brief / Intro

An ordinary FirelightVault shareholder can request a withdrawal in a block whose timestamp is exactly the beginning of a coverage period. The withdrawal is booked into that new period's still-slashable withdrawal bucket, but its post-withdrawal asset checkpoint overwrites the checkpoint at the period-start timestamp. This removes the withdrawal from totalAssetsAt(capturePeriodStart), which is the maximum amount payout() will pay, while the same withdrawal remains included in activePayableAmount and is expressly intended to absorb incidents captured during that period. A later approved claim is therefore capped below the assets that remain slashable. IncidentManager accepts the partial return and closes the incident, permanently underpaying the claimant while allowing the boundary withdrawer to claim more assets than the intended proportional slash permits.

Vulnerability Details

Threat model

The manipulating party is an ordinary vault shareholder. The attacker needs no Firelight role and calls the public withdraw() or redeem() entry point.

All permissioned actors may behave honestly:

  • the allocator commits and settles a valid allocation;

  • the curator creates and assesses a genuine covered incident;

  • the assessment approver approves a loss within the settled cover amount; and

  • IncidentManager, holding PAYOUT_ROLE, executes the approved payout.

The attack does not depend on an unfunded first-loss buffer, an excessive assessment, duplicate claims, a depegged token, or a compromised trusted role.

1. A transaction at the boundary already belongs to the new period

periodAtTimestamp() assigns a timestamp to a period using integer division:

Let T0 be the exact start timestamp of period P. At block.timestamp == T0, the transaction executes in period P, not P-1.

2. A boundary withdrawal writes its post-withdrawal amount under key T0

withdraw() requests the withdrawal and then checkpoints the post-withdrawal totals:

_logTrace() uses the current second as the checkpoint key:

When the supplied key equals the latest checkpoint key, Checkpoints.push() replaces the stored value:

Consequently, a withdrawal executing at T0 can define the value returned by totalAssetsAt(T0) for the entire period.

3. The withdrawal is removed from the snapshot but remains slashable

Because the transaction executes in period P, _requestWithdraw() books it into P+1:

Pending withdrawals are excluded from active totalAssets():

If the vault had A assets and the attacker requests X, the checkpoint written at T0 is therefore:

However, payout() expressly treats withdrawals requested during the capture period as payable and slashable:

Immediately after the boundary withdrawal:

The same X is excluded from the payout ceiling while remaining in the set that the payout is intended to slash.

4. The corrupted snapshot caps the payout before the slashable set can be charged

payout() uses both values, but the smaller corrupted snapshot binds first:

Let:

Then:

The vulnerability becomes economically effective when:

Ignoring integer rounding, the additional amount preserved by the attacker is:

The required position depends on the size of the legitimate vault-funded loss:

5. IncidentManager permanently closes the underpaid claim

IncidentManager converts the approved residual loss into vault-asset units and calls the vulnerable payout:

FirelightVault.payout() is allowed to return less than requested. approveCurrentAssessment() records the returned partial amount and closes the incident without recording the unpaid remainder:

The claim shortfall is therefore permanent. The excess assets remain allocated to the pending withdrawal and can later be claimed by the attacker once its bucket matures.

Impact Details

The selected impact is Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield.

The attacker obtains an economic benefit rather than merely causing grief. By reducing the amount charged to the pending withdrawal, the attacker later receives assets that should have been consumed by the approved incident payout. The covered claimant receives correspondingly less than the approved vault-funded loss.

The runnable PoC establishes:

The missing 100-asset vault payment remains inside the vault accounting: approximately 90 benefits the attacker's withdrawal and 10 remains with active shareholders. This is a direct redistribution away from the approved claimant caused by the attacker's unprivileged boundary transaction.

This is distinct from the disclosed aggregate-claims issue. The PoC creates one assessment of exactly $250 against a CoverNFT with exactly $250 of settled allocation. The assessment is neither duplicated nor excessive. It is also distinct from the disclosed first-loss-buffer assumptions: the buffer contains 50 assets, has granted the required allowance, and pays its complete tranche successfully before the vulnerable vault leg executes.

Feasibility Limitations

The report does not claim deterministic exploitation in every period. The following conditions are required:

  1. A block must have timestamp exactly equal to the deterministic period boundary T0.

  2. The attacker's withdrawal must execute in that block, and no later asset flow with the same timestamp may replace the checkpoint with a non-beneficial value.

  3. The attacker must hold a sufficiently large vault position relative to the vault-funded incident loss, as described by X > A - L.

  4. A genuine covered incident must occur in the affected period and produce a vault-funded loss larger than the corrupted snapshot after the first-loss-buffer leg.

  5. Sufficient settled cover must exist for the assessed loss.

  6. Later payouts for the same period may reduce or eliminate the preserved amount before the withdrawal matures.

These conditions affect likelihood, but not the demonstrated impact after successful execution. The attack does not require the attacker to compromise or collude with a Firelight role. Missing the advantageous incident does not destroy the withdrawal principal; the attacker completes an ordinary delayed withdrawal, subject to the vault's normal incident exposure.

Proof of Concept

Step-by-step reproduction

  1. Deploy the legacy vault proxy with a six-decimal FAsset and a seven-day period. Upgrade the same proxy to V2 using initializeV2().

  2. Deploy CoverNFT, CoverOrderAllocator, a $1 price adapter, and IncidentManager. Wire the required roles normally. Configure:

  3. Deposit 900 assets for the attacker and 100 for another shareholder. Create a $250 order targeting the next period.

  4. Read the deterministic boundary:

  5. Set the next local block timestamp to boundary and execute the attacker's ordinary withdrawal:

  6. Observe the corrupted historical state and the still-exposed withdrawal:

  7. Commit and settle the valid $250 cover allocation and verify that CoverNFT ID 0 is minted to the buyer.

  8. Create an incident at boundary, confirm it, add one $250 assessment loss against the $250 settled market allocation, and submit it.

  9. Approve the assessment. The healthy first-loss buffer transfers 50 assets, after which IncidentManager requests 200 from the vault. The corrupted snapshot limits the vault payment to 100:

  10. Observe that the attacker's pending withdrawal is reduced from 900 only to 810. Advance until the bucket matures and claim 810. With the intended complete 200-asset vault payout, proportional charging would have left the attacker only 720.

Runnable Proof of Concept

Save the following file as test/boundary_second_withdraw_realistic.poc.js.

Execution

Run from the repository root:

Expected result:

Was this helpful?