Skip to main content

Every Chainlink VRF request carries a number that almost nobody looks at. It is called requestConfirmations, it is set by the game and not by the oracle, and the coordinator’s own source comments describe it plainly: “How many blocks you’d like the oracle to wait before responding to the request. See SECURITY CONSIDERATIONS for why you may want to request more.”

I pulled every VRF request on Base for just under eight days to see what the chain actually puts in that field. Of 15,740 draws, 13,166 asked the oracle to wait zero blocks. That number is not the finding. The finding is what happened next: those draws were answered after a median of two blocks anyway, and not one of them was answered as early as it had permission to be.

TL;DR

  • Across 15,740 Chainlink VRF draws on Base between 3 and 11 October 2026, 83.65% requested zero block confirmations, 9.54% asked for one, 6.49% asked for three, and exactly one request in eight days asked for twenty.
  • Nothing was ever answered as fast as it was allowed to be. The zero-confirmation cohort still waited a median of two blocks, with a floor of one. Half of all randomness on the chain was delivered at a depth of two blocks or less.
  • That protection is supplied by how long the oracle takes, not by anything the game asked for. If fulfilment got faster tomorrow, 83.65% of Base’s draws would quietly lose a safety margin they never requested and no fairness page anywhere would need editing.
  • The legal range is 0 to 200. Five values were used. 68 of 69 consumer contracts hard-code a single number forever, and the only address that varies it is Chainlink’s own pass-through wrapper.
  • The other unexamined knob is cheaper than people think: measured on one contract holding everything else constant, ten random words cost 1.53x what one word costs, against 10x for ten separate requests.

What the number actually does

A VRF draw has exactly one unpredictable ingredient, and it is not the one most players would guess. Reading the deployed coordinator on Base, the request identifier and its pre-seed come from keccak256(abi.encode(keyHash, sender, subId, nonce)). Every input there is public and known before you bet: which gas lane the game uses, which contract is asking, which subscription pays, and how many times that contract has asked before.

The randomness enters one line later, when the oracle’s proof is verified:

uint256 actualSeed = uint256(keccak256(abi.encodePacked(proof.seed, blockHash)));

and blockHash is the hash of the block your request landed in. The oracle’s secret key makes the output unforgeable, so nobody is guessing your result. But the seed that output is computed from is bound to one specific block of Base history. Change that block and you change the seed, and a different seed is a different winner. Not a stolen one. A perfectly legitimate draw that happens to have a different answer.

That is the entire job of requestConfirmations. It is how long you make the oracle wait before it treats your block as settled enough to compute against. We have called that delay the anti-manipulation mechanism rather than latency before, and described confirmations as a price rather than a countdown. Neither of those posts measured what the chain pays.

What Base asked for

The window runs from block 52,130,633 (3 October, 18:03 UTC) to block 52,476,014 (11 October, 17:56 UTC): 345,382 blocks, 15,740 requests, every one of them fulfilled. The coordinator is 0xd5d517abe5cf79b7e95ec98db0f0277788aff634, and its configuration permits anything from minimumRequestConfirmations, which is set to 0 on Base, up to a MAX_REQUEST_CONFIRMATIONS constant of 200.

Two hundred and one legal values. Five of them were used.

Confirmations requested Draws Share Contracts
0 13,166 83.65% 9
1 1,502 9.54% 13
2 50 0.32% 1
3 1,021 6.49% 47
20 1 0.01% 1

Read that table twice, because the two columns disagree about what Base believes. Weighted by draws, the chain overwhelmingly asks for nothing. Weighted by contracts, the most popular answer is three. Eight contracts that always send zero produced 13,164 draws between them, while forty-six contracts that always send three produced barely a thousand.

Blockscout names most of them. The zero cohort is GridMining (10,169 draws), a GameManager proxy (2,602), Coinflip (261), Roulette (70), a Dice contract (19) and Crash (15). The three cohort is the long tail: lottery contracts, lucky-number contracts, one-off draws. The pattern is not prize size. It is game speed. The contracts that sell instant results bought their speed out of the confirmation budget, and the contracts where nobody is watching a spinner left the default alone.

What Base got

Here is the part that is not in anyone’s documentation. Requesting zero confirmations does not get you zero confirmations.

Requested Median delivered depth Fastest delivered 99th percentile
0 2 blocks 1 block 7 blocks
1 3 blocks 2 blocks 9 blocks
2 4 blocks 4 blocks 29 blocks
3 5 blocks 4 blocks 165 blocks
20 21 blocks 21 blocks 21 blocks

Zero draws out of 15,740 were answered at or before the depth they permitted. There were no same-block fulfilments at all. The oracle takes roughly two blocks to see a request, build a proof and land a transaction, and it adds whatever you asked for on top of that.

Which means the gap between asking for nothing and asking for three is three blocks. Six seconds. The difference between the most reckless setting on the chain and the most popular one among contract authors is six seconds of waiting, and 83.65% of Base’s draws declined to pay it.

The deeper problem is where the two blocks come from. They are not a guarantee, a commitment or a parameter. They are how long Chainlink’s infrastructure currently takes, and 145 draws in this window were answered after a single block, which is what happens when that figure has a bad day in your favour. Half of all randomness on Base in eight days, 7,850 draws, settled at a depth of two blocks or less.

Every oracle in the industry is trying to get faster. If Base’s VRF fulfilment tightened to one block, 13,166 draws per week would lose half the protection they are currently getting, no contract would change, no setting would be edited, and no disclosure would be triggered, because none of those games ever claimed the two blocks in the first place. A margin you did not ask for is a margin nobody owes you.

The case for zero is not stupid, and it deserves stating properly before it gets knocked down. On Base the only party who could rewrite a two-block-old block is the sequencer, and if you genuinely believe Coinbase is going to reorganise its own chain to flip your dice roll, the confirmation field is not the thing standing between you and ruin. On top of that, the oracle hands you two blocks whether you ask or not, so the setting looks like a tax on user experience that buys nothing. Both halves of that argument are true today. Both halves are also statements about who happens to run the sequencer this year and how fast Chainlink’s infrastructure happens to be this month. A fairness guarantee that rests on two facts nobody has promised to keep is not a guarantee. It is a weather forecast.

The setting is a floor, and nothing is a ceiling

One more thing falls out of the same data. requestConfirmations sets the earliest the oracle may answer. Nothing sets the latest. In the whole window, 28 draws out of 15,740 took more than a hundred blocks to come back, the worst of them 3,388 blocks, which is close to two hours for a request that asked to wait three.

Every one of those 28 came from the same contract, a Lotto39 deployment asking for five words with a 250,000 gas callback, and 27 of the 28 callbacks reverted on arrival. The randomness was produced, verified, charged for and delivered into a function that could not run. Its subscription paid 30 fulfilment fees across the window for results that never reached a game, 27 of them this contract’s.

That is a reassuring result for the chain and a brutal one for that operator. Base’s VRF is not flaky: 99.8% of draws landed inside ten blocks and the entire long tail is one broken consumer. But it makes the asymmetry concrete. The number in your request controls when randomness becomes allowed. Whether it becomes useful is controlled by a different number entirely, and the two are set in the same constructor by the same person on the same afternoon.

Sixty-eight contracts, sixty-eight constants

The other thing you can see in eight days of data is that nobody treats this as a variable. Of 69 distinct consumer contracts, 68 used exactly one confirmation value for every request they ever made. Forty-six always send three, thirteen always send one, eight always send zero, one always sends two.

The sixty-ninth is 0xb0407dbe851f8318bd31404a49e658143c982f23, which sent 0, 3 and 20 across 221 draws, along with four different callback gas limits. It is not a game. It is VRFV2PlusWrapper_Optimism, Chainlink’s own direct-funding wrapper, and it varies because it passes through whatever the caller hands it. So the honest version of the finding is this: among contracts that actually run games on Base, the number of them that adjust their reorg tolerance to the draw in front of them is zero.

That is a design decision made once, usually at deployment, usually by copying a tutorial, and then applied identically to a one-dollar dice roll and to whatever the largest prize that contract will ever pay turns out to be.

The knob that is cheaper than everyone assumes

While the data was open, the second request-side parameter is worth settling, because the folklore is wrong in the expensive direction. numWords ranges from 1 to a MAX_NUM_WORDS of 500. The window’s 15,740 draws asked for 38,394 words: 10,174 requests took three words, 5,060 took one, 145 took ten, and precisely one contract ever asked for twelve.

One consumer gives a clean controlled experiment. A Blackjack contract behind a proxy made 441 requests on a single subscription, at a single callback gas limit of 1,000,000, at a single confirmation setting, varying only the word count. Median native payment:

Words requested Draws Median fee (ETH) Relative
1 98 0.000001776 1.00x
2 188 0.000002072 1.17x
10 145 0.000002716 1.53x
11 9 0.000002797 1.57x

Nine extra words cost about 0.0000001 ETH each, a hair over a hundredth of a cent. Ten separate requests for one word each would have cost ten times the single-word fee, because the expensive part of a VRF request is verifying one elliptic-curve proof on-chain, and that happens once regardless. A game that needs ten numbers and makes ten requests is paying roughly six and a half times too much for the privilege of pretending they are independent, and they are not independent anyway: the extra words are derived from the one verified output by hashing it with an index, which is correct design but worth saying out loud.

For reference, the median draw on Base cost 0.000003968 ETH, about one cent at 2,538 dollars per ETH, and the entire chain’s randomness bill for the eight days came to 0.056948 ETH plus 0.0986 LINK. Around 145 dollars bought every provably fair outcome on Base for a week.

What Satoshie will claim, and what it will not

Our games resolve in the VRF callback, so the outcome, the escrow release and the payout are one transaction, and there is no window where a result exists but has not been applied. That design is only as durable as the block it lands in, which is exactly why the confirmation number matters to us and why we will say what ours is rather than leaving it to be discovered by someone reading logs.

The narrow claim is this: confirmation depth should be a function of what is at stake, not of what the deployment script had in it. A coinflip settling a few dollars and a raffle settling a pot are not the same risk, and treating them as the same number is a choice somebody made by not making it. The industry’s revealed preference, measured above, is to optimise that number for the spinner animation.

The second claim is cheaper to keep: a multi-winner raffle should take its words from one request. It is 1.53x, not 10x, and the failure mode to watch is the callback gas limit, not the word count, as the Lotto39 tail above shows. Asking for ten words is close to free. Budgeting gas to handle ten words is where the money and the risk actually sit.

Honest limits

I cannot show you that Base has ever reorganised. An RPC only serves canonical history, so the thing the confirmation parameter protects against is precisely the thing this method cannot observe. What I can say is that the chain’s own safe head sat 48 blocks behind the tip and its finalized head 538 blocks behind when I checked on 11 October, and that 99.79% of these draws were decided shallower than the safe head and 99.97% shallower than finality. Base’s sequencer is operated by Coinbase and is not decentralised today. The party that could rewrite a two-block-old block is not a hypothetical attacker with a hash rate. It is the operator.

A second limit: zero is not an exotic choice by these games, it is the floor their coordinator allows. Base’s s_config sets minimumRequestConfirmations to 0, and the coordinator owner could raise the floor for the entire chain with one transaction. Nobody has.

Third, depth is blocks, and blocks are only seconds because Base currently produces one every two. Every “four seconds” in this post is arithmetic on a constant, not a measurement of time.

Three questions worth asking your platform

  • How many confirmations does your contract request, and does that number change when the prize does? If the answer is in a deployment script from last year, that is the answer.
  • If the block holding your result were rewritten, who decides whether the payout stands? The contract cannot, because from its view the draw never happened.
  • When your game draws several winners, is that one request or several? One is cheaper, and it is also the only version where the words provably came from the same verified proof.

How to redo this

Everything above is one address and one method. Pull eth_getLogs on the coordinator in 1,000-block spans from an archive endpoint, decode RandomWordsRequested (key hash, subscription and sender are indexed; request id, pre-seed, confirmations, callback gas limit and word count are in the data) and match it to RandomWordsFulfilled on request id. The delivered depth is the subtraction of two block numbers. The configuration comes from s_config(), MAX_REQUEST_CONFIRMATIONS() and MAX_NUM_WORDS() on the same contract, and the verified source is on Blockscout if you want to read the seed derivation yourself. I checked a 500-block slice against a second independent endpoint and got byte-identical request ids before publishing any of these counts.

The number is in every log entry. It has been there the whole time. Nobody was reading it, which is how 13,166 draws came to ask for no protection at all and quietly accept two blocks of it from an oracle that was simply busy.

Photo by Jeremy Thomas on Unsplash

Valentina Ní Críonna

Author Valentina Ní Críonna

More posts by Valentina Ní Críonna