block.timestamp is set by the miner who proposes a block. Ethereum allows miners to adjust timestamps within a tolerance of approximately 15 seconds (in PoW; validators in PoS have similar flexibility within a slot). Any contract that uses block.timestamp as randomness or as a fine-grained trigger is potentially vulnerable.
If a contract pays out a reward to whoever calls it at exactly the right millisecond, a miner can mine a block with a crafted timestamp to ensure they win. More broadly, anything that derives "randomness" from block.timestamp, block.number, or blockhash is predictable to miners.
// VULNERABLE — using timestamp for randomness
function random() internal view returns (uint) {
return uint(keccak256(abi.encode(block.timestamp, msg.sender))) % 100;
}
// Or: a lottery that can be gamed by a miner
function isWinner() public view returns (bool) {
return block.timestamp % 15 == 0; // miner picks timestamp to win
}
// SAFE — use Chainlink VRF for verifiable randomness
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
// Request random number from Chainlink VRF
function rollDice() public returns (uint256 requestId) {
requestId = COORDINATOR.requestRandomWords(
keyHash, subscriptionId, requestConfirmations, callbackGasLimit, numWords
);
}
// Callback with proven-random value
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
result = randomWords[0] % 100;
}
Using block.timestamp is fine for coarse-grained time checks where 15-second manipulation doesn't matter — for example, checking that a lock period of 30 days has elapsed. It's only dangerous when the outcome depends on timestamp precision within seconds.
block.timestamp for durations that are large compared to the ~15-second manipulation window.