Contract fails to deliver promised returns, but doesn't lose value
Description
Brief
The FirelightVault::periodAtTimestamp() function incorrectly uses the current block timestamp instead of the provided timestamp parameter when calculating the period number. This causes the function to always return the current period regardless of which historical timestamp is queried, breaking any functionality that relies on historical period-to-timestamp mappings.
Vulnerability Details
The FirelightVault::periodAtTimestamp() function is designed to return the period number corresponding to any given timestamp. The function correctly identifies which period configuration applies to the given timestamp via periodConfigurationAtTimestamp(), but then fails to use the timestamp parameter in the actual period calculation.
The issue stems from the _sinceEpoch() helper function:
The helper function always calculates the time difference from epoch to Time.timestamp() (the current block timestamp), completely ignoring the timestamp parameter passed to periodAtTimestamp(). This means that instead of calculating how many period durations have elapsed from the epoch to the given timestamp, it calculates how many have elapsed to the current time.
For example, with a daily period duration:
Contract deployed at timestamp T₀ (period 0)
Query periodAtTimestamp(T₀) at timestamp T₃ (3 days later, period 3)
Expected result: 0
Actual result: 3
The calculation becomes:
The periodConfigurationAtTimestamp() function correctly finds the period configuration for the given timestamp by iterating through configurations and comparing against the timestamp parameter. However, this correct behavior is undermined when _sinceEpoch() then uses the current time instead.
Impact Details
Historical period queries always return the current period number with the period configuration at the timestamp instead of the correct historical period.
const { loadFixture, time } = require('@nomicfoundation/hardhat-network-helpers')
const { deployVault } = require('./setup/fixtures.js')
const { expect } = require('chai')
const { ethers } = require('hardhat')
describe('PoC: periodAtTimestamp Bug', function () {
const DECIMALS = 6
const INITIAL_DEPOSIT_LIMIT = ethers.parseUnits('20000', DECIMALS)
const PERIOD_CONFIGURATION_DURATION = 86400 // 1 day
let token_contract, firelight_vault, users, utils
let deploymentTime
before(async () => {
({ token_contract, firelight_vault, users, utils } = await loadFixture(
deployVault.bind(null, {
decimals: DECIMALS,
initial_deposit_limit: INITIAL_DEPOSIT_LIMIT,
period_configuration_duration: PERIOD_CONFIGURATION_DURATION
})
))
deploymentTime = await time.latest()
})
it('demonstrates periodAtTimestamp always returns current period instead of period at given timestamp', async () => {
const initialTime = await time.latest()
const period0Timestamp = initialTime
// Advance time to period 1 (1 day later)
await time.increase(PERIOD_CONFIGURATION_DURATION)
const period1Time = await time.latest()
const period1Timestamp = period1Time
// Advance time to period 2 (2 days from start)
await time.increase(PERIOD_CONFIGURATION_DURATION)
const period2Time = await time.latest()
// Advance time to period 3 (3 days from start)
await time.increase(PERIOD_CONFIGURATION_DURATION)
const period3 = await firelight_vault.currentPeriod()
// BUG: All these calls should return different periods, but they all return the CURRENT period
const periodAtTime0 = await firelight_vault.periodAtTimestamp(period0Timestamp)
const periodAtTime1 = await firelight_vault.periodAtTimestamp(period1Timestamp)
const periodAtTime2 = await firelight_vault.periodAtTimestamp(period2Time)
// Assertions to prove the bug
expect(periodAtTime0).to.equal(period3, 'BUG CONFIRMED: periodAtTimestamp(period0Time) returns current period')
expect(periodAtTime1).to.equal(period3, 'BUG CONFIRMED: periodAtTimestamp(period1Time) returns current period')
expect(periodAtTime2).to.equal(period3, 'BUG CONFIRMED: periodAtTimestamp(period2Time) returns current period')
// These assertions show what SHOULD happen (but currently fail)
try {
expect(periodAtTime0).to.equal(0)
} catch (e) {
// Expected to fail due to bug
}
try {
expect(periodAtTime1).to.equal(1)
} catch (e) {
// Expected to fail due to bug
}
try {
expect(periodAtTime2).to.equal(2)
} catch (e) {
// Expected to fail due to bug
}
})
it('demonstrates impact: historical period queries are broken', async () => {
const currentTime = await time.latest()
const currentPeriod = await firelight_vault.currentPeriod()
// Try to query a time 2 days in the past (should be period currentPeriod-2)
const pastTime = currentTime - (2 * PERIOD_CONFIGURATION_DURATION)
const periodAtPast = await firelight_vault.periodAtTimestamp(pastTime)
expect(periodAtPast).to.equal(currentPeriod, 'BUG CONFIRMED: Returns current period for historical timestamp')
})
})