Skip to main content

There is a countdown on the screen. It says four minutes and eleven seconds. You have a ticket in your hand, a wallet open in the next tab, and a very clear idea of what that number means.

It does not mean what you think. Not because anyone is lying to you, and not because the contract is badly written. It is because the chain your raffle lives on has never measured a second in its life, and the number your entry is actually racing is not a clock at all. It is a counter.

TL;DR

  • A smart contract cannot read a clock. It reads block.timestamp, which is a value written into the block header, not a measurement of anything.
  • On Base, that value is pure arithmetic: genesis plus two seconds per block, holding exactly across all 51,983,954 blocks we checked. You can reproduce it in three RPC calls.
  • Ethereum L1 is the same idea with different constants: its timestamps are the beacon genesis time plus twelve seconds per slot, exactly.
  • The OP Stack rules let an L2 timestamp sit up to max_sequencer_drift ahead of its L1 origin. Since Fjord that is a constant 1,800 seconds, a permitted band thirty minutes wide, and Base is Fjord-activated.
  • None of this touches the random word. It cannot change who wins. It decides something else entirely: who was in the draw.
  • Our own earlier advice, to enforce a close as a block height rather than a rendered timestamp, is correct but does less than it sounds like on a chain where the two are the same number.

What the contract is actually comparing

Write a raffle in Solidity and you will type something close to this:

require(block.timestamp < closesAt, "entries closed");

That line looks like it consults time. It does not. block.timestamp is a field in the header of the block your transaction landed in. Somebody put a number there. The protocol checked that the number was allowed. Nothing in that process involved observing the passage of time, and nothing in it involved you.

So the honest reading of that line is not “did you enter before eight o’clock”. It is “is the number in this block’s header smaller than the number in contract storage”. Two integers, one comparison. Your phone, your wallet, the countdown widget, the exchange you bought from and the clock on the wall are all outside the system that decides.

The arithmetic, which you can check yourself

Here is the part that surprised us when we went and looked rather than assuming. We pulled three blocks from Base mainnet:

  • Block 1: timestamp 1,686,789,349
  • Block 1,000,000: timestamp 1,688,789,347
  • Block 51,983,954 (the head when we ran this): timestamp 1,790,757,255

The gap between the first two is 1,999,998 seconds across 999,999 blocks. That is exactly two seconds per block, to the second, with no remainder. Run the implied formula forward to the head and you get 1,686,789,347 plus two times 51,983,954, which is 1,790,757,255. That is not approximately the head timestamp. It is the head timestamp, digit for digit, nearly fifty-two million blocks later.

Base’s clock has not drifted, corrected, leaped or wobbled, because it is not a clock. It is t0 + 2n. It is the mechanical counter in the photograph at the top of this post: a device that shows you numbers in order and has no opinion whatsoever about how long it took to get there.

Ethereum L1 works the same way with a longer stride. Take the L1 head timestamp we recorded, 1,790,757,263, subtract the beacon chain genesis time of 1,606,824,023, and you get 183,933,240. Divide by the twelve-second slot and you get 15,327,770 with nothing left over. The timestamp is the slot number in disguise. It always was.

The band nobody mentions

If the counter is arithmetic, what stops a sequencer from simply running it fast? The OP Stack derivation rules do, and they are worth quoting rather than paraphrasing:

l1_origin.timestamp <= block.timestamp <= max_l2_timestamp, where max_l2_timestamp = max(l1_origin.timestamp + max_sequencer_drift, prev_l2_timestamp + l2_block_time)

So there is a floor and a ceiling, and the ceiling is set by max_sequencer_drift. The Fjord upgrade turned that from a per-chain setting into a fixed protocol constant, and the specification is unambiguous about the size: it “becomes a constant of value 1800 seconds, translating to a fixed maximum sequencer drift of 30 minutes”. Before Fjord the usual value was 600 seconds. We checked that Base is on the new rule rather than assuming it, by calling isFjord() on the GasPriceOracle predeploy at 0x420000000000000000000000000000000000000F. It returns true.

Read that carefully, because it is not a bug report. There is no correct timestamp that the sequencer is deviating from. There is a permitted range, and every value inside it is equally canonical. Derivation will reject a batch outside the band and accept anything within it without comment, because “within the band” is the entire specification. The width of that band is thirty minutes, and the reason it exists is entirely reasonable: the specification explains that if a sequencer’s connection to L1 breaks, the drift value “determines how long it can still produce blocks without violating the timestamp drift derivation rules”.

Now look at what happens when the band is exhausted, because this is the detail that made us write the post. The batch rules say that once the drift is exceeded, a batch containing transactions is dropped outright. The sequencer may keep the counter advancing. It may not keep letting people in.

Hold those two facts next to a raffle. Time continues to arrive on schedule, and entries stop being accepted. That is precisely the worst combination available: the deadline shows up exactly when advertised, and the door is already bolted. No admin key was touched. Nobody made a decision. The protocol behaved correctly in every respect.

The countdown is reading the past

There is a second gap, and it sits in the interface rather than the protocol.

A front end builds its countdown one of two ways. It can ask an RPC node for the latest block and compute from that block’s timestamp, in which case it is reading a block that has already been superseded by the time the response reaches your browser. Or it can compare the deadline against your own operating system’s clock, in which case it is comparing a number Base will reach on its own schedule against an unrelated clock maintained by NTP, drifting by device, and settable by anyone with access to your settings.

The first option is structurally biased in one direction. A node tells you about the chain as it was, never as it is, so a countdown built that way always reports slightly more time remaining than exists. It cannot report less. The error only ever runs the way that encourages you to cut it fine.

We noticed this while measuring, which is the honest way to have noticed it. We took the Base head timestamp and the L1 head timestamp seconds apart with separate calls, and got 1,790,757,255 and 1,790,757,263, with our own machine reading 1,790,757,267. Eight seconds and twelve seconds of apparent difference, most of it not drift at all but the lag of the instruments. We sat down specifically to measure this and still produced a number contaminated by exactly the effect we were measuring. A countdown widget is not trying as hard as we were.

What this is not

This is not the randomness argument, and we want to be strict about that, because the temptation to inflate it is obvious and we would rather keep the credibility.

Nothing here reaches the random word. A VRF proof verifies against a public key and a request seed, and it does not care what number sat in the header of the block that closed entries. Nobody gains the ability to choose a winner. What moves is the composition of the entrant set, which is upstream of the draw and invisible to every proof downstream of it.

It is also not the point we made about VRF liveness, which asks whether a draw resolves at all once a mechanism fails. Here every mechanism works. Nor is it the transaction ordering piece, which covered block producer discretion over entropy drawn from blockhash or timestamp. That is the timestamp as an ingredient of a random number. This is the timestamp as a boundary of a competition, a completely different job for the same field. And it is not who supplies the last ingredient of the number, which is a question about the draw itself rather than the queue in front of it.

The nearest neighbour is our own post on the underfilled raffle, which listed the ways a close time can be changed after tickets are sold. Every route in that piece required somebody to act. This one requires nobody to act, which is why we think it is worth a separate treatment. A deadline that cannot be moved by anyone is still not a moment in time.

Where this reaches us

Satoshie runs on Base. Everything above is a description of our own chain, so here is the part that costs us something to write.

First, the correction. In that underfilled-raffle post we recommended enforcing a close as a block height rather than as a timestamp rendered by an interface, and we presented block height as the solid ground underneath the slippery thing. On Base those two quantities are related by an exact affine function, as the arithmetic above demonstrates. Choosing block height removes the interface from the loop, which is a real improvement and worth keeping. It does not move you onto firmer ground, because there is no firmer ground on this chain. The advice was right and the framing oversold it.

Second, the part we cannot engineer away. The party that decides which side of the boundary your entry lands on is whoever orders transactions, and on Base that is a single sequencer operated by Coinbase. Base’s forced-inclusion route through L1 is genuine and we have pointed at the verification path before, but it operates on a timescale measured in hours. Against a raffle deadline measured in minutes it is not a remedy. It is a footnote. We would rather say that plainly than let the word permissionless do work it cannot do here.

Third, the design conclusion, which is less exciting than a fix and more useful. The answer is not a better clock, because nobody is selling one. The answer is a draw where being on the right side of a boundary is not worth much: entry windows long enough that the final block is uninteresting, a close published as the number the contract will actually compare, and a published rule for the boundary block that you can read before you pay rather than infer afterwards. A raffle where a meaningful share of entries arrive in the last block does not have a clock problem. It has a design problem, and it was always going to find some way to show you.

Three questions to ask any on-chain game

  • Is the close a number the contract reads, or a time a website renders? One is a rule and the other is a picture of a rule.
  • What happens to an entry that lands in the boundary block, and is that written down anywhere you can read before committing money?
  • Can the close move after tickets are sold, and if so, who moves it? If the answer is the operator, the odds you agreed to were provisional.

We are not telling you that Base’s clock is wrong. We checked, and it is sitting within seconds of everything else, exactly as you would hope. We are telling you that “wrong” is not a category that applies. There is a counter, there is a band of permitted values thirty minutes wide, and there is a number in a header that your entry either beat or did not.

The countdown said four minutes and eleven seconds. What it meant was that a counter somewhere had roughly one hundred and twenty-five increments left to go, give or take whatever the network felt like, and that a single company would decide which increment your transaction belonged to. That is a perfectly workable arrangement. It is just not a clock, and it never said it was. We assumed that part.

📷 Photo by Alexandre Daoust on Unsplash

Valentina Ní Críonna

Author Valentina Ní Críonna

More posts by Valentina Ní Críonna