Free snapshot slots open this week info@inversez.com
DetectionVulnerabilitiesToolsAuditsPricingBlogFree monitoring assessmentContact
Home/Vulnerabilities/Block timestamp manipulation
SWC-116 · Typically medium

Block timestamp manipulation

block.timestamp is set by the block proposer, within limits the protocol enforces. That drift is small, but it is enough to matter for anything with a short window, and it is fatal for anything using the timestamp as a source of randomness.

Vulnerable code

Do not ship this
function playLottery() external payable {
    require(msg.value == 1 ether);
    uint256 rand = uint256(
        keccak256(abi.encodePacked(block.timestamp, block.prevrandao))
    );
    if (rand % 10 == 0) {
        payable(msg.sender).transfer(10 ether);
    }
}

function endAuction() external {
    require(block.timestamp >= start + 30, "too early");  // 30s window
    _settle();
}

Why it breaks

The lottery's randomness is derived from values the proposer influences and every caller can read. A contract can compute the same value in the same block and only submit the transaction when it wins, since reverting otherwise costs only gas. The auction's thirty-second window is inside the range a proposer can shift, so whoever proposes the block chooses whether settlement happens now or next block.

Watch the attack run

The same bug as a stepped sequence. Play it, or walk through with the arrow keys.

The fix

Corrected
// Randomness: use a verifiable source, not chain state.
function requestRandom() external {
    requestId = COORDINATOR.requestRandomWords(
        keyHash, subId, confirmations, gasLimit, 1
    );
}

function fulfillRandomWords(
    uint256, uint256[] memory words
) internal override {
    _settleLottery(words[0]);
}

// Durations: keep windows long relative to proposer drift.
uint256 constant AUCTION_LENGTH = 3 days;

function endAuction() external {
    require(block.timestamp >= start + AUCTION_LENGTH, "too early");
    _settle();
}

The invariant

The fix above is what an audit gives you. This is what monitoring gives you: the thing that should always be true once the contract is live, stated precisely enough to check every block.

InvariantEVM · timestamp dependence

Timestamps that decide an outcome fall inside the expected block cadence for the chain.

invariant  block.timestamp - parent.timestamp
           within [min_cadence, max_cadence]
  at any block that settles a payout
Backtest: not yet run. This entry carries the reasoning, not the evidence. When the invariant has been replayed against the real transactions around the exploit, the firing block, how far ahead it fired, and its false-positive rate over a clean control period will be published here, along with the recording of the run.

How to test for it

A finding you cannot reproduce is an opinion. Write the test before you write the fix, watch it fail, then make it pass.

function testTimestampDriftDoesNotChangeOutcome() public {
    uint256 snap = vm.snapshot();

    vm.warp(start + AUCTION_LENGTH);
    auction.endAuction();
    address winnerA = auction.winner();

    vm.revertTo(snap);
    vm.warp(start + AUCTION_LENGTH + 12);   // one block later
    auction.endAuction();
    assertEq(auction.winner(), winnerA);
}

Where it has caused real losses

On-chain lotteries and games seeded from block variables have been drained repeatedly, usually by a contract that computes the outcome first and reverts when it would lose. The attack costs almost nothing and can be repeated every block.

What we check during review

The practical rule: timestamps are fine for durations measured in hours or days, unsafe for windows measured in seconds, and never acceptable as randomness. If you need randomness on-chain, use a VRF with a commitment made before the outcome is knowable. And remember that block.timestamp behaves differently on L2s: on some rollups it reflects sequencer time and can be manipulated more freely than on mainnet.

Want us to check your contract for this?

A free monitoring assessment covers one contract up to 200 lines, manually reviewed, findings back within 72 hours. This class is on the checklist for every review we run.

Free monitoring assessment

Found this useful?

We publish these because the alternative, asking you to trust us, is worth less. Send a contract and we’ll apply the same thinking to your code.

  • Reply written by the auditor who read your code
  • No sales sequence, no drip campaign, no retargeting
  • We’ll tell you if you don’t need a paid audit yet
  • Report published only with your written permission

Request your snapshot

We read every submission. If it doesn’t fit the free tier we’ll say so and quote you instead, no obligation.