59335 sc low periodattimestamp function returns current period instead of queried period leading to temporary freezing of funds

Submitted on Nov 11th 2025 at 08:59:36 UTC by @legion for Audit Comp | Firelight

  • Report ID: #59335

  • Report Type: Smart Contract

  • Report severity: Low

  • Target: https://github.com/firelight-protocol/firelight-core/blob/main/contracts/FirelightVault.sol

  • Impacts:

    • Temporary freezing of funds

Description

Brief/Intro

The periodAtTimestamp(uint48 timestamp) view function in FirelightVault.sol contains a critical logic error where it always returns the current period number regardless of the timestamp parameter passed to it. This function is designed to allow users and off-chain systems to determine which period corresponds to any given historical or future timestamp. However, due to incorrect use of Time.timestamp() instead of the function parameter in the calculation, all queries return the same value—the currently active period. This breaks withdrawal scheduling automation, historical analytics, and user-facing dashboards, potentially causing users' mature withdrawals to remain temporarily frozen beyond their intended claim window because automated systems cannot correctly determine when periods end.

Vulnerability Details

Root Cause

The vulnerability exists in the periodAtTimestamp function at line 246-250 of FirelightVault.sol:

function periodAtTimestamp(uint48 timestamp) public view returns (uint256) {
    PeriodConfiguration memory periodConfiguration = periodConfigurationAtTimestamp(timestamp);
    // solhint-disable-next-line max-line-length
    return periodConfiguration.startingPeriod + _sinceEpoch(periodConfiguration.epoch) / periodConfiguration.duration;
}

The function correctly retrieves the appropriate PeriodConfiguration for the given timestamp parameter using periodConfigurationAtTimestamp(timestamp). However, it then calculates the elapsed time by calling the private helper function _sinceEpoch:

The _sinceEpoch function always uses Time.timestamp() (the current block timestamp) rather than the timestamp parameter passed to periodAtTimestamp. This means the elapsed time calculation is always based on the current moment, not the queried timestamp.

Concrete Example

Consider a vault deployed with a 1-week (604800 seconds) period duration:

Timeline:

  • Deploy time (T₀): timestamp = 1,000,000

  • Current time (T_now): timestamp = 4,024,000 (5 weeks later, period = 5)

Expected behavior:

  • periodAtTimestamp(1,000,000) → should return 0 (period at deploy)

  • periodAtTimestamp(2,209,600) → should return 2 (period after 2 weeks)

  • periodAtTimestamp(7,048,000) → should return 10 (future period)

Actual behavior:

  • periodAtTimestamp(1,000,000) → returns 5 (current period)

  • periodAtTimestamp(2,209,600) → returns 5 (current period)

  • periodAtTimestamp(7,048,000) → returns 5 (current period)

All three calls return the same value despite querying different timestamps.

Impact Detail

Temporary Freezing of Funds The FirelightVault implements an asynchronous withdrawal system where users call withdraw() or redeem() to queue a withdrawal for the next period (currentPeriod() + 1). Once that period ends, users can call claimWithdraw(period) to receive their funds. The claimWithdraw function requires that period < currentPeriod(), meaning the period must have ended. This can lead to : Broken off-chain automation: Dashboards, bots, and integrations rely on periodAtTimestamp() to calculate:

  • "When will my withdrawal become claimable?"

  • "What period will be active at timestamp X?"

  • Historical period lookups for analytics and auditing

References

  • Vulnerable function: FirelightVault.sol#L246-250 - periodAtTimestamp()

  • Root cause: FirelightVault.sol#L794-796 - _sinceEpoch()

  • Related functions correctly using _sinceEpoch for current time:

    • currentPeriodStart() - Line 274

    • currentPeriodEnd() - Line 282

Proof of Concept

Proof of Concept

Was this helpful?