Skip to main content
Sigvex

Timestamp Dependency Remediation

How to fix dangerous dependencies on block timestamps in security-critical logic.

Timestamp Dependency Remediation

Overview

Related Detector: Timestamp Dependency

Solana block timestamps are derived from validator votes and can be manipulated within bounds. Programs that use timestamps for exact equality checks, access control, or randomness generation are vulnerable. The fix depends on the specific pattern: replace equality checks with range comparisons, replace timestamp-based randomness with verifiable random functions, and add tolerance windows to time-gated logic.

Before (Vulnerable)

pub fn process_lottery(ctx: Context<Lottery>) -> Result<()> {
    let clock = Clock::get()?;

    // VULNERABLE: exact equality check
    if clock.unix_timestamp == ctx.accounts.lottery.draw_time {
        // VULNERABLE: timestamp as randomness
        let winner = (clock.unix_timestamp as u64) % ctx.accounts.lottery.num_entries;
        distribute_prize(ctx, winner)?;
    }
    Ok(())
}

After (Fixed)

pub fn process_lottery(ctx: Context<Lottery>) -> Result<()> {
    let clock = Clock::get()?;
    let lottery = &ctx.accounts.lottery;

    // FIXED: range-based comparison with tolerance
    let draw_window = 300i64;  // 5-minute window
    require!(
        clock.unix_timestamp >= lottery.draw_time
            && clock.unix_timestamp <= lottery.draw_time + draw_window,
        ErrorCode::OutsideDrawWindow
    );

    // FIXED: use SlotHashes for entropy
    let recent_slothashes = &ctx.accounts.recent_slothashes;
    let data = recent_slothashes.try_borrow_data()?;
    let hash = solana_program::hash::hash(&data[..32]);
    let winner = u64::from_le_bytes(
        hash.to_bytes()[..8].try_into().unwrap()
    ) % lottery.num_entries;

    distribute_prize(ctx, winner)?;
    Ok(())
}

Alternative Mitigations

1. Use slot numbers instead of timestamps

Slot numbers are monotonically increasing and less susceptible to manipulation:

pub fn check_access(clock: &Clock, required_slot: u64) -> Result<()> {
    // Slots advance predictably at ~400ms intervals
    require!(clock.slot >= required_slot, ErrorCode::TooEarly);
    Ok(())
}

2. Commit-reveal scheme for randomness

For applications requiring verifiable randomness, use a two-phase commit-reveal:

// Phase 1: Commit hash of secret
pub fn commit(ctx: Context<Commit>, hash: [u8; 32]) -> Result<()> {
    ctx.accounts.game.commitment = hash;
    ctx.accounts.game.commit_slot = Clock::get()?.slot;
    Ok(())
}

// Phase 2: Reveal secret after N slots
pub fn reveal(ctx: Context<Reveal>, secret: Vec<u8>) -> Result<()> {
    let clock = Clock::get()?;
    let game = &ctx.accounts.game;

    // Ensure enough slots have passed
    require!(clock.slot >= game.commit_slot + 10, ErrorCode::TooEarly);

    // Verify commitment
    let hash = solana_program::hash::hash(&secret);
    require!(hash.to_bytes() == game.commitment, ErrorCode::InvalidReveal);

    // Use revealed secret as entropy
    let random_value = u64::from_le_bytes(hash.to_bytes()[..8].try_into().unwrap());
    Ok(())
}

3. Switchboard VRF for on-chain randomness

use switchboard_solana::prelude::*;

pub fn consume_randomness(ctx: Context<ConsumeRandomness>) -> Result<()> {
    let vrf = ctx.accounts.vrf.load()?;
    let result_buffer = vrf.get_result()?;
    require!(!result_buffer.iter().all(|&x| x == 0), ErrorCode::VrfNotReady);

    let random_value = u64::from_le_bytes(result_buffer[..8].try_into().unwrap());
    // Use verifiable random value...
    Ok(())
}

Common Mistakes

Mistake 1: Using unix_timestamp for strict equality

// WRONG: may never match due to validator timestamp variance
if clock.unix_timestamp == 1700000000 { ... }

Always use range comparisons: timestamp >= target && timestamp <= target + tolerance.

Mistake 2: XOR-ing timestamp with a “secret” constant

// WRONG: constant XOR does not add entropy
let random = clock.unix_timestamp ^ 0xDEADBEEF;

The constant is public knowledge and the timestamp is predictable. This provides no meaningful randomness.

Mistake 3: Combining multiple predictable values

// WRONG: all values are known to validators before block finalization
let seed = clock.unix_timestamp ^ clock.slot ^ clock.epoch;

Combining multiple predictable values does not create unpredictable output. Use a VRF or commit-reveal scheme.

References