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):
- A lottery selects a winner with
winnerIndex = block.timestamp % numPlayers. - The attacker (as miner or in coordination with one) calculates which timestamp maps to their player index:
desiredTs = currentTime + offsetwheredesiredTs % numPlayers == myIndex. - The miner sets
block.timestamp = desiredTs(within the ±900s window) when mining the draw block. - The attacker wins with certainty.
Time-lock bypass:
- Vesting unlocks at
unlockTime = 1000000000. - Current real time is
999999200— 800 seconds before unlock. - A miner adds 800 seconds to
block.timestamp, setting it to1000000000. require(block.timestamp >= unlockTime)passes. The attacker claims vesting 800 seconds early.- Manipulations up to 900 seconds are undetectable under consensus rules.
Auction timing:
- An auction ends at a specific
block.timestamp. - 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
- Detector: Timestamp Detector
- Remediation Guide: Timestamp Remediation
For randomness, replace block.timestamp with Chainlink 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: Chainlink 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.