Skip to main content
Sigvex

Timestamp Manipulation Exploit Generator

Sigvex exploit generator that validates block.timestamp dependency vulnerabilities by executing contracts at shifted timestamps (±900 seconds) to detect miner-manipulable randomness and time-lock bypasses.

Timestamp Manipulation Exploit Generator

Overview

The timestamp manipulation exploit generator validates findings from the timestamp, timestamp_dependency, and weak-randomness detectors by executing the target contract at four different timestamp values and checking whether outcomes change in ways a miner could exploit. The generator specifically tests the ±900-second manipulation window that Ethereum consensus rules permit for block.timestamp.

Miners (and, post-Merge, validators) can set block.timestamp to any value within approximately ±900 seconds (15 minutes) of the real current time. This window is sufficient to control the outcome of block.timestamp % N randomness, unlock time-locked vesting positions early, and influence auction timing.

Note: Exploit generation in Sigvex is for vulnerability validation purposes only.

Attack Scenario

Timestamp as randomness (critical):

  1. A lottery selects a winner with winnerIndex = block.timestamp % numPlayers.
  2. The attacker (as miner or in coordination with one) calculates which timestamp maps to their player index: desiredTs = currentTime + offset where desiredTs % numPlayers == myIndex.
  3. The miner sets block.timestamp = desiredTs (within the ±900s window) when mining the draw block.
  4. The attacker wins with certainty.

Time-lock bypass:

  1. Vesting unlocks at unlockTime = 1000000000.
  2. Current real time is 999999200 — 800 seconds before unlock.
  3. A miner adds 800 seconds to block.timestamp, setting it to 1000000000.
  4. require(block.timestamp >= unlockTime) passes. The attacker claims vesting 800 seconds early.
  5. Manipulations up to 900 seconds are undetectable under consensus rules.

Auction timing:

  1. An auction ends at a specific block.timestamp.
  2. A miner can include last-minute bids or exclude competitor bids by adjusting the timestamp to shift the effective auction window.

Exploit Mechanics

The generator runs four scenarios against the target bytecode. The base timestamp is set to 1704067200 (2024-01-01 00:00:00 UTC). Storage slot 0 holds the last action timestamp, slot 1 the user balance, slot 2 a lock-until timestamp, and slot 3 a random outcome seed.

Scenario Timestamp Storage slot 2 (lock-until) Purpose
1 — Normal 1704067200 base - 100 (unlocked) Baseline
2 — Future +900s 1704068100 base - 100 Miner forward manipulation
3 — Past -900s 1704066300 base - 100 Miner backward manipulation
4 — Timelock bypass 1704069000 (+1800s) base + 1000 (locked) Early unlock attempt

The fallback selector 0x4e71d92d (claim) is used when no specific selector is found.

Verdict:

  • Scenario 1 and Scenario 2 both succeed AND storage slot 3 changes → timestamp randomness confirmed (confidence 0.95).
  • Scenario 1 reverts AND Scenario 4 succeeds → timelock bypass confirmed (confidence 0.90).
  • Scenario 1 succeeds but Scenario 2 or 3 diverges in behavior → execution-window dependency warning (confidence 0.75).
  • Gas variance >5000 units between scenarios → possible timestamp-dependent code paths (confidence 0.60).

Remediation

For randomness, replace block.timestamp with the off-chain aggregator VRF. For time-locks, use block numbers (which cannot be manipulated) or add buffers well exceeding 900 seconds:

// VULNERABLE: Lottery outcome determined by timestamp
uint256 winnerIndex = block.timestamp % numPlayers;

// SECURE: the off-chain aggregator VRF provides unpredictable randomness
uint256 requestId = COORDINATOR.requestRandomWords(keyHash, subscriptionId,
    requestConfirmations, callbackGasLimit, numWords);

// SECURE: Block numbers for time-locks (cannot be manipulated)
mapping(address => uint256) public unlockBlock;
require(block.number >= unlockBlock[msg.sender], "Still locked");

// SECURE: Large time buffer (>> 900s miner window)
uint256 constant MIN_LOCK_DURATION = 3600; // 1 hour minimum
require(duration >= MIN_LOCK_DURATION, "Lock too short");

Avoid as sources of randomness or timing: block.timestamp, block.number, blockhash(), block.difficulty / block.prevrandao, msg.sender, and any predictable counter.

References