Every few weeks somebody drops the same objection into a thread about on-chain raffles, and they drop it like a grenade: you pick the winner with a modulo, so your draw is biased.
They are not wrong about modulo bias. It is a genuine bug class, it has cost real gambling operators real money, and if you have ever shipped rand() % n in C you have probably written it. The objection deserves an answer rather than a shrug.
So here is the answer with the arithmetic in the open. Modulo bias is a small-source problem, and Satoshie’s source is 256 bits wide. At that width the bias does not shrink to something acceptable; it shrinks to something you could not observe if you ran draws until the sun burned out.
TL;DR
- Modulo bias is real and it is what happens when your random source cannot be divided evenly by the number of outcomes you are mapping it onto.
- It bites hard on small sources. Reduce a single byte to three outcomes and one of them gets a 1.18% relative advantage, which is larger than the entire house edge on most casino games.
- Chainlink VRF returns a 256-bit word. A 10,000 ticket raffle drawn from it gives favoured indices a relative advantage of roughly 0.000000000000000000000000000000000000000000000000000000000000000000000000086, or 8.6 x 10-74.
- Satoshie’s coinflip has exactly zero modulo bias, not a small amount, because 2 divides 2256 with no remainder.
- The denominator is the bug that actually matters. A uniform random word mapped onto a ticket count the operator can change after you enter is the attack nobody asks about.
What modulo bias actually is
Take a random source that produces one byte, uniformly. That is 256 equally likely values, 0 through 255. Now map it onto three outcomes with byte % 3.
256 divided by 3 is 85 with 1 left over. So residue 0 is reachable from 86 of the 256 input values, while residues 1 and 2 are reachable from 85 each. Outcome 0 lands 33.59% of the time, the other two land 33.20% of the time.
That gap looks tiny written out. It is not. It is a 1.18% relative edge handed to one outcome for free, in an industry where the house survives on margins of one to two percent. Run a three-way game on that byte and you would never need to cheat. The arithmetic would be cheating on your behalf, quietly, forever.
The general shape: if your source is uniform over R values and you take % n, then R mod n of the residues are reachable one extra time. The bias vanishes only when n divides R exactly, and otherwise scales with n/R. So the fix is never cleverness. It is making R enormous relative to n, or rejecting the overhanging values.
Why this ruined real gambling software
The famous case is not strictly a modulo bug but it rhymes. In 1999, researchers at Reliable Software Technologies took apart the shuffle used by ASF Software’s online poker client and found the deck was being permuted from a 32-bit seed drawn off the system clock. Milliseconds since midnight gave roughly 86 million distinct shuffles. Synchronise your clock with the server, watch a few cards, and the rest of the deck fell out in real time. The randomness was not tampered with. It was simply too narrow to hide anything inside.
The same narrowness is where modulo bias lives. Run % 10000 on a 32-bit source and 7,296 of the 10,000 indices get an extra slot out of 429,496. That is a relative excess of 2.3 x 10-6. Invisible in a hundred draws. Perfectly detectable across a few hundred million, which is a volume a busy platform reaches inside a year, and detectable is the same word as exploitable if you are the one who noticed first.
The arithmetic at 256 bits
Satoshie’s raffle winner is computed as keccak256(VRF word + prior blockhash) % ticketsMinted, inside the VRF callback, in verified immutable code on Base. The Chainlink VRF coordinator verifies the randomness proof on-chain before that callback is allowed to fire, so the word going in is one nobody chose.
Both the VRF word and the keccak256 digest are 256 bits. So R = 2256, which is 115,792,089,237,316,195,423,570,985,008,687,907,853,269,984,665,640,564,039,457,584,007,913,129,639,936. Roughly 1.16 x 1077.
Take a fat raffle, 10,000 tickets. 2256 mod 10,000 is 9,936, so 9,936 of the indices are reachable one extra time out of about 1.16 x 1073. The relative advantage that buys a favoured ticket is 8.6 x 10-74.
What that means in practice: to accumulate one extra expected win for a favoured index you would need roughly 1073 draws. There have been about 4 x 1017 seconds since the Big Bang. Draw a raffle every nanosecond for the entire age of the universe and you are not one part in a trillion trillion trillion of the way to noticing.
Scale to a million tickets and the bias scales linearly with it, to 8.6 x 10-72. Still nothing. Modulo bias is not a threshold you sneak under; at this width it stops being a physical phenomenon.
The coinflip case is better than negligible
Satoshie’s coinflip is one modulo on the VRF word. The modulus is 2. And 2 divides 2256 exactly, with zero remainder, because 2256 is a power of two.
That is not a very small bias. That is no bias, as a matter of arithmetic rather than measurement. Every one of the 2255 even words maps to heads and every one of the 2255 odd words maps to tails, and the two sets are the same size. There is no rounding error to argue about because there is no rounding.
Any power-of-two outcome count gets that guarantee for free. It is one of the few places in this industry where the honest answer to “how fair is it” is a full stop rather than a decimal.
The bug that actually matters: the denominator
Now the part nobody puts in a thread, because it is less fun than pasting a Stack Overflow link about rand(). A perfectly uniform random word buys you nothing if the number you divide it by can move after you commit. Suppose you buy 1 ticket in what the interface tells you is a 100 ticket raffle, and the operator mints 900 more before the draw resolves. Your odds silently went from 1% to 0.1%. Not one bit of the randomness was touched. The maths above stays flawless. You still got robbed.
That is the real attack surface on any raffle, on-chain or otherwise, and it is why the denominator matters more than the numerator. On Satoshie, ticketsMinted is readable contract state before you enter, tickets are escrowed in the same transaction that mints them, and escrow, VRF resolution and payout happen in one transaction rather than across three systems that have to agree afterwards. There is no admin key, no proxy and no setter for any parameter that decides an outcome. The contract is verified and immutable on BaseScan, which means that last sentence is falsifiable: read the bytecode, and if a function exists that can change an outcome parameter, we are lying.
Ask better questions
If you want to interrogate a supposedly fair game, modulo bias is the wrong first question. Try these instead.
- Can you calculate your own probability before you play, from data the operator does not control? If the ticket count lives in a database you cannot read, nothing else matters.
- Can you check the result without asking the operator? A dashboard with a green tick is transparency. Querying a chain the operator can neither see nor block is evidence. Different products.
- Can anything change between your entry and the resolution? Upgradeable contracts and admin setters are where fairness actually dies, usually without a line of dodgy cryptography.
The caveats we are obliged to state
Our front end is an ordinary web app and it is the least trustworthy component we ship. Base is an L2 whose sequencer is operated by Coinbase and is not decentralised today, so its soft confirmations are a promise rather than finality. Neither of those is fixed by arithmetic, and pretending otherwise would be the same category of dishonesty as pretending modulo bias is a crisis.
What we do claim is narrow and checkable. The randomness is verified on-chain before it is used, the mapping from it to a winner adds a bias of one part in 1073, the coinflip adds none at all, and the denominator is published before you commit. Every one of those you can falsify with a block explorer we do not operate.
Bring the modulo objection. It is a good instinct applied to the wrong layer, and the answer is a number rather than a promise. That is the whole point.
📷 Photo by Erik Mclean on Unsplash


