#59928 [SC-Low] incorrect period calculation periodattimestamp function

Submitted on Nov 17th 2025 at 02:26:03 UTC by @Le_Rems for Audit Comp | Firelight

  • Report ID: #59928

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Contract fails to deliver promised returns, but doesn't lose value

Description

Brief/Intro

The periodAtTimestamp() function in FirelightVault is designed to return the period number corresponding to any given timestamp. However, the implementation incorrectly uses the current block timestamp instead of the provided parameter when calculating the period number. This causes the function to return incorrect results for any timestamp that differs from the current time.

Vulnerability Details

The FirelightVault contract implements a period-based system where time is divided into discrete periods, each defined by a PeriodConfiguration containing an epoch timestamp, duration, and starting period number.

The periodAtTimestamp() function is a public view function intended to determine which period number corresponds to a specific timestamp. This function should enable querying periods for any point in time—past, present, or future—which is essential for historical queries, period validation, and integration with external systems.

The function implementation is as follows:

periodAtTimestamp()

The function first correctly retrieves the appropriate PeriodConfiguration for the given timestamp by calling periodConfigurationAtTimestamp(timestamp). However, when calculating the actual period number, it calls _sinceEpoch(periodConfiguration.epoch), which is defined as:

_sinceEpoch()

The root cause is that _sinceEpoch() always uses Time.timestamp() (the current block timestamp) rather than accepting and using the timestamp parameter provided to periodAtTimestamp(). This means:

  1. When querying the current period, the function works correctly because timestamp equals Time.timestamp()

  2. When querying a past timestamp, the function retrieves the correct configuration but calculates the period number using the current time, returning a period that would be correct now but incorrect for the queried time

  3. When querying a future timestamp, the calculation is similarly incorrect

For example, consider a scenario where:

  • The vault has been operational for 100 periods, each lasting 30 days

  • A period configuration has an epoch at timestamp T0 and duration of 30 days

  • At timestamp T0 + 15 days, the period number should be startingPeriod + 0 (still in the first period)

  • At timestamp T0 + 45 days (15 days into the future from the current time T0 + 30 days), if someone queries periodAtTimestamp(T0 + 15), the function would:

    1. Correctly identify the period configuration for T0 + 15 ✓

    2. But calculate using (T0 + 30) - epoch instead of (T0 + 15) - epoch, returning startingPeriod + 1 instead of startingPeriod + 0

The issue affects the contract's API contract: the function signature and documentation promise to return the period for "the timestamp given," but the implementation violates this promise for any timestamp other than the current time.

Impact Details

The impact manifests in several ways:

1. The function doesn't behave as expected

  • Any external system or smart contract that queries periodAtTimestamp() with historical timestamps will receive incorrect period numbers

2. Future Functionality Risks

  • If the contract is upgraded or extended to validate withdrawals, deposits, or claims based on historical periods (e.g., validating that a withdrawal was requested in a specific past period), the bug would cause incorrect validations

Proof of Concept

// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28;

import {Test, console} from "forge-std/Test.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {FirelightVault} from "../contracts/FirelightVault.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract FirelightVaultTest is Test { // Contracts FirelightVault public firelightVault; FirelightVault public firelightVaultImplementation; ERC1967Proxy public proxy; IERC20 public tokenContract; address public assetManager; // Mock asset manager for FAsset

}

Was this helpful?