Anatomy of a fake dice game
I went spelunking through some old repos and found a dice game from 2018 — YungBet, the on-chain half of a gambling project whose parent pitch deck claims it “sold for $4.5m.” I can’t verify that number, and after reading the contract I’d suggest not taking the deck at face value. It’s 48 lines, and it is two bugs wearing a trenchcoat.
Bug 1: the randomness is always zero
function RandomNumber() public returns(uint) {
total_bets[msg.sender]++;
uint random_number = uint(keccak256(abi.encodePacked(
blockhash(block.number),
total_bets[msg.sender]
)));
...
}
blockhash(block.number) — the hash of the block currently executing — is always 0. The EVM only exposes the previous 256 block hashes; the current block isn’t mined yet, so it has no hash. So the “random” number collapses to keccak256(0, total_bets[msg.sender]) — a pure function of your own bet counter. You can compute every future roll you’ll ever get, off-chain, before you bet. It’s the same family as the SmartBillions lottery, which used a block hash that resolved to zero and got drained for ~400 ETH.
Bug 2: it never uses the roll anyway
That bug almost doesn’t matter, because of the second one:
function makeBet() public payable {
uint bet_roll = RandomNumber(); // computed...
uint bet_payout = bet_amount.div(2); // ...and ignored
require(bet_payout < address(this).balance, "...");
user.transfer(bet_payout);
emit betPlaced(bet_amount, bet_payout, bet_roll);
}
bet_roll is computed, emitted in an event for flavour, and never read. The payout is unconditionally half your stake. There is no win condition, no target, no comparison — you send x, you get x/2 back, every time. It’s not a dice game with bad randomness; it’s a guaranteed 50% drain with a random-number generator bolted on for decoration. The broken RNG is a red herring sitting on top of a contract that was never a game.
Building the one it pretended to be
So I built the real thing — Dice.sol. The starting constraint is the lesson of every randomness post I’ve written: there is no safe single-transaction randomness on the EVM. Anything you can read in the bet transaction, the bettor can read too. So you need a value that’s fixed before the bet and unknown until after it — commit-reveal.
The shape:
- The house commits
keccak256(serverSeed)before any bet references it, so it can’t choose the seed after seeing the action. - Each bet carries a player-chosen
clientSeed. The roll iskeccak256(serverSeed, clientSeed, betId) % 100— the house can’t predict it without knowing the player’s seed, the player can’t predict it without the still-secret server seed, and neither can grind it. - The roll decides: bet that it lands under a target, paid with a real house edge — a 50/50 bet pays
1.96×, not2×. (The original couldn’t even charge an edge; it just kept half.)
The one residual is the house withholding a losing reveal — the last-revealer problem that haunts every commit-reveal scheme. claimRevealTimeout closes it: miss the deadline and every bet in the round pays as a win, so withholding is strictly worse for the house than just revealing. Plus the boring-but-load-bearing parts the original lacked entirely: a bankroll that reserves each open bet’s maximum loss (unbacked bets revert), pull-payment withdrawals, reentrancy guards. It’s seven Foundry tests, including one that confirms the outcome actually tracks the roll and one that pays the player when the house goes quiet.
The lesson
The funny thing about YungBet is that the headline bug — the always-zero randomness — is the less serious one. You could fix the RNG perfectly and the contract would still just take half your money, because it never looked at the roll. It’s the purest example of a pattern I keep running into: the cryptography is rarely where these things actually break. The break is in whether the outcome people are betting on is bound to the cryptography at all — the randomness, the proof, the roll. Here it wasn’t bound to anything. It was decoration on a coin-flip you always lose.
The audit and the rebuild are at github.com/0xSoftBoi/dice.