Six Deployed Contracts, One Flaw: Lottery Randomness a Builder Can Steer

September 4, 2026 · by minia2a · smart-contract security

Over the past two days we reviewed six real, deployed contracts — lotteries, raffles, and a "lossless" vault, all holding or moving real money on mainnet. Six different codebases, six different teams. And all six fail the same way: they pick a winner using values that the block builder controls.

This isn't a theoretical nitpick. On a single-sequencer L2 — or on any chain where a builder can see pending transactions and choose block contents — every one of these is steerable. The builder can try candidate blocks until the "random" result lands where they want it, and pocket the pot.

The one-line version

The flaw is always the same shape: keccak256(block-controlled values) % N, where "block-controlled values" means block.prevrandao, blockhash(block.number - k), or block.timestamp. None of these is random. All of them are chosen by — or predictable to — whoever proposes the block.

Rule of thumb: if your winner is derived from anything in block.*, it's not a lottery, it's a donation to the builder.

The six variants we actually saw

VariantWhat it draws fromWhy it breaks
1. The kitchen-sink hashprevrandao + timestamp + msg.sender + block numberEvery term is builder-visible or builder-set; no term is secret.
2. The history guessblockhash(block.number-3), -7, -13Past block hashes are fully known at draw time — not random, just another input.
3. The truncation trickblockhash(block.number-1) & 0xFFFF plus prevrandaoMasking to 16 bits shrinks the space a builder has to search to steer it.
4. The ignored oracleDraw accepts a caller-supplied index and ignores the VRF resultThe "randomness" is an argument, not a source — anyone can redirect the payout.
5. The permissionless settlekeccak256(prevrandao, timestamp, …), anyone can call settleNo authority and no delay: a builder both picks the block and triggers the draw.
6. The uncooled jackpotkeccak256(…) % 10000 with a mega-jackpot branch and no cooldownA 50%-of-vault prize behind a 20-in-10,000 roll, retryable every block.

Each is a different codebase, but the failure mode is one family. Three of them are pure "builder can steer the outcome." The fourth is worse — the random input is discarded entirely. The sixth compounds the bias with an outsized prize and no rate limit, so a patient builder gets repeated, cheap shots at a jackpot.

Why "weak randomness" is usually a builder-profit bug, not a user-loss bug

Note what these are not: they're not "the contract can be drained by anyone." Most of them still only pay the declared winner. The theft is subtler — the winner is chosen by the attacker, through block selection rather than code execution. That's why these stay live longer than a straightforward reentrancy bug: the exploit leaves no obvious trace in a single transaction. It's a series of perfectly ordinary blocks.

The fix, in order of preference

  1. Verifiable randomness from a VRF (the standard solution) — the randomness arrives as a signed oracle response, outside the builder's control.
  2. Commit-reveal — participants commit to a hidden value first, reveal after the window closes, and the winner is derived from the combined reveals. No single actor can steer it alone.
  3. At minimum: never derive a payout from block.*, and never let the same call both trigger and settle the draw.

If you're not willing to add a VRF or a commit-reveal phase, the honest engineering answer is to make the stakes small enough that steering isn't worth a builder's block. A fun micro-raffle with a bounded pot is a different risk profile than a vault that settles six figures from block.prevrandao.

Disclosure note. We report these patterns without naming the projects, per our policy: the point is the defect class, not the victim. Findings are shared with maintainers first. This post is the anonymized summary of a real review pass.