Clock Staleness
Detects Clock sysvar usage in financial logic without staleness bounds, exploitable after network halts.
Clock Staleness
Overview
The clock staleness detector identifies Solana programs that read the Clock sysvar and use it in conditional logic controlling financial operations (lamport transfers) without implementing staleness bounds. After network halts, the on-chain clock can resume from a stale timestamp, allowing arbitrageurs to exploit time-dependent financial logic. For remediation steps, see the Clock Staleness Remediation.
Why This Is an Issue
Solana’s Clock sysvar provides unix_timestamp, slot, and epoch values. During normal operation, these values advance predictably. However, after a network halt (which has occurred multiple times in Solana’s history), the clock resumes from a stale state. Programs that use timestamps for financial decisions are vulnerable:
- Vesting schedules: Tokens that should have vested during the halt become immediately claimable at the stale timestamp, allowing early withdrawal before the clock catches up.
- Expiration deadlines: Options, loans, or auctions that should have expired during the halt remain active with stale timestamps, enabling exploitation.
- TWAP calculations: Time-weighted average prices computed with stale timestamps produce incorrect values, enabling oracle manipulation.
- Staking rewards: Time-delta calculations using stale timestamps undercount elapsed time, affecting reward distributions.
An attacker who monitors for network restarts can submit transactions in the first slots after restart, exploiting the gap between the stale on-chain clock and real wall-clock time.
How to Resolve
// Before: Vulnerable -- no staleness check
pub fn claim_vested(accounts: &[AccountInfo]) -> ProgramResult {
let clock = Clock::get()?;
let vesting = load_vesting_state(accounts)?;
if clock.unix_timestamp >= vesting.end_time {
transfer_vested_tokens(accounts)?;
}
Ok(())
}
// After: Fixed -- staleness bound added
pub fn claim_vested(accounts: &[AccountInfo]) -> ProgramResult {
let clock = Clock::get()?;
let state = load_program_state(accounts)?;
// Reject if clock is stale (> 5 minutes since last known update)
let max_stale_seconds = 300;
if clock.unix_timestamp < state.last_known_timestamp + max_stale_seconds
&& clock.slot < state.last_known_slot + 600
{
return Err(ProgramError::Custom(1)); // Clock too stale
}
let vesting = load_vesting_state(accounts)?;
if clock.unix_timestamp >= vesting.end_time {
transfer_vested_tokens(accounts)?;
}
state.last_known_timestamp = clock.unix_timestamp;
state.last_known_slot = clock.slot;
Ok(())
}
Examples
Vulnerable Code
pub fn process_expiry(accounts: &[AccountInfo]) -> ProgramResult {
let clock = Clock::get()?;
let loan = load_loan(accounts)?;
// Uses clock directly in financial decision
if clock.unix_timestamp > loan.expiry_timestamp {
liquidate_collateral(accounts)?; // Financial operation
}
Ok(())
}
Fixed Code
pub fn process_expiry(accounts: &[AccountInfo]) -> ProgramResult {
let clock = Clock::get()?;
let global_state = load_global_state(accounts)?;
// Staleness check: slot number is more reliable than timestamp
let slots_since_update = clock.slot.saturating_sub(global_state.last_update_slot);
if slots_since_update > MAX_STALE_SLOTS {
return Err(ProgramError::Custom(ErrorCode::ClockStale as u32));
}
let loan = load_loan(accounts)?;
if clock.unix_timestamp > loan.expiry_timestamp {
liquidate_collateral(accounts)?;
}
// Update last known values
save_global_state(accounts, clock.slot, clock.unix_timestamp)?;
Ok(())
}
Example JSON Finding
{
"detector": "clock-staleness",
"severity": "medium",
"confidence": 0.60,
"title": "Clock Sysvar Used in Financial Logic Without Staleness Check",
"description": "This function reads the Clock sysvar and uses it in conditional logic that controls financial operations.",
"cwe_ids": [367]
}
Detection Methodology
- Clock variable collection: Identifies variables assigned from Clock sysvar reads (syscalls containing “clock” or “get_clock” in the name).
- Conditional logic scan: Checks whether clock-derived variables appear in branch conditions.
- Financial operation detection: Searches for lamport transfer statements (
TransferLamports) in the function. - Three-condition trigger: Only flags functions where all three conditions are met — clock read, conditional use, and financial operation — reducing false positives.
Limitations
False positives: Programs that implement staleness checks in helper functions called from the flagged function may still be reported. Programs using slot numbers instead of timestamps (which are more reliable) may receive findings with lower relevance. False negatives: Financial operations performed via CPI rather than direct lamport transfers are not detected. Clock values passed through multiple variable assignments may lose tracking.
Related Detectors
- Clock Account Spoofing — detects unvalidated clock account sources
- Timestamp Dependency — detects dangerous timestamp reliance in access control