The public EtherPot lottery, an August 2015 Frontier contract whose blockhash-based winner selection became an early cautionary tale about on-chain randomness.
Historical Significance
EtherPot is one of the earliest decentralized lotteries on Ethereum and an early, well-documented lesson in smart contract security. Two flaws surfaced within days of launch. On August 27, 2015, security researcher Andrew Miller reported that the cash function ignored the return value of send, which can silently fail, for example through a deliberately preloaded call stack at depth 1023 or through running out of gas; when that happens execution continues, the subpot is marked as cashed, and those funds are lost forever. The author, Aakil Fernandes, acknowledged the report the same day and the recommended fix was to check the return value with if (!winner.send(subpot)) return. Separately, winner selection relied on block.blockhash, which only returns a non-zero hash for the most recent 256 blocks, so any subpot cashed more than 256 blocks after its decision block resolved to the first buyer rather than a fair draw. That second bug, pointed out to the author by Liana Hus, produced the launch-day anomaly of a single player holding about 8 percent of the tickets being reported as the winner of all three subpots of the first round. The contract still holds 0.1 ether that can never be released, a direct consequence of the send-failure flaw, and both issues are frequently cited in later discussions of on-chain randomness and defensive programming. The source was published by the author, and this byte-exact reproduction ties that published source to the deployed bytecode.
Context
August 2015 was the first month of the Ethereum Frontier mainnet, which launched on July 30, 2015. Tooling was minimal, the Solidity compiler was at v0.1.1, and there was no established testnet workflow, so developers commonly iterated by deploying straight to mainnet. EtherPot was part of this first wave of application experiments, alongside early registrars, wallets, and prediction markets, and was among the first contracts to put real value behind an on-chain game.
Key Facts
Description
EtherPot was a decentralized lottery deployed on the Ethereum Frontier network in August 2015 by Aakil Fernandes and collaborators. Players bought tickets for 0.1 ether each by sending value to the contract. Rounds lasted 6800 blocks, roughly a day, and the winner of a round was selected from the blockhash of a block mined just after the round closed, an on-chain source of randomness with no operator. To blunt any single miner's incentive to grind that blockhash, a round's pot was split into subpots no larger than the 5 ether block reward, so the gain from manipulating a result stayed below the value of mining honestly. Each subpot was paid out independently through the cash function, which looked up the winning ticket and sent the subpot to that buyer.
This is the canonical public deployment, the version players actually used. It was deployed on August 25, 2015 from the same account that had deployed an earlier draft build four days before, and it still holds a small balance today. The source was published by the author in the etherpot/contract repository, and the runtime and creation bytecode here were reproduced byte-for-byte from that source compiled with soljson v0.1.1 (optimizer off), which confirms the published source is the one that was deployed.
The contract stored each round as a list of buyers, a running ticket count, a per-buyer ticket count, and a flag for each cashed subpot. calculateWinner mapped the blockhash-derived index back to a buyer by walking the buyers list and accumulating their ticket counts. The design depended on block.blockhash, which on Ethereum only returns a non-zero hash for the most recent 256 blocks, and that limitation is the root of the issues for which EtherPot is remembered.
Source Verified
Exact bytecode match (runtime + creation). Runtime: 2709 bytes. Creation: 2728 bytes (19 init + 2709 runtime payload, 0 constructor args). Runtime SHA-256 70faac9179cf23c0d3593a2fe13894f3f6687200a031a3b10fb16cfebebcb14a, creation SHA-256 fa517a5263b3f5bc73481486fe70cbc008a05ed564d03dba0e83b9d9e289ff59. Source is author-published (etherpot/contract); it compiles byte-exact with soljson v0.1.1 optimizer off.
Heuristic Analysis
The following characteristics were detected through bytecode analysis and may not be accurate.
Frontier Era
The initial release of Ethereum. A bare-bones implementation for technical users.
Bytecode Overview
Verified Source Available
Source verified through compiler archaeology and exact bytecode matching.
View Verification ProofShow source code (Solidity)
contract Lotto {
uint constant public blocksPerRound = 6800;
uint constant public ticketPrice = 100000000000000000;
uint constant public blockReward = 5000000000000000000;
function getBlocksPerRound() constant returns(uint){ return blocksPerRound; }
function getTicketPrice() constant returns(uint){ return ticketPrice; }
struct Round {
address[] buyers;
uint pot;
uint ticketsCount;
mapping(uint=>bool) isCashed;
mapping(address=>uint) ticketsCountByBuyer;
}
mapping(uint => Round) rounds;
function getRoundIndex() constant returns (uint){
return block.number/blocksPerRound;
}
function getIsCashed(uint roundIndex,uint subpotIndex) constant returns (bool){
return rounds[roundIndex].isCashed[subpotIndex];
}
function calculateWinner(uint roundIndex, uint subpotIndex) constant returns(address){
var decisionBlockNumber = getDecisionBlockNumber(roundIndex,subpotIndex);
if(decisionBlockNumber>block.number)
return;
var decisionBlockHash = getHashOfBlock(decisionBlockNumber);
var winningTicketIndex = decisionBlockHash%rounds[roundIndex].ticketsCount;
var ticketIndex = uint256(0);
for(var buyerIndex = 0; buyerIndex<rounds[roundIndex].buyers.length; buyerIndex++){
var buyer = rounds[roundIndex].buyers[buyerIndex];
ticketIndex+=rounds[roundIndex].ticketsCountByBuyer[buyer];
if(ticketIndex>winningTicketIndex){
return buyer;
}
}
}
function getDecisionBlockNumber(uint roundIndex,uint subpotIndex) constant returns (uint){
return ((roundIndex+1)*blocksPerRound)+subpotIndex;
}
function getSubpotsCount(uint roundIndex) constant returns(uint){
var subpotsCount = rounds[roundIndex].pot/blockReward;
if(rounds[roundIndex].pot%blockReward>0)
subpotsCount++;
return subpotsCount;
}
function getSubpot(uint roundIndex) constant returns(uint){
return rounds[roundIndex].pot/getSubpotsCount(roundIndex);
}
function cash(uint roundIndex, uint subpotIndex){
var subpotsCount = getSubpotsCount(roundIndex);
if(subpotIndex>=subpotsCount)
return;
var decisionBlockNumber = getDecisionBlockNumber(roundIndex,subpotIndex);
if(decisionBlockNumber>block.number)
return;
if(rounds[roundIndex].isCashed[subpotIndex])
return;
var winner = calculateWinner(roundIndex,subpotIndex);
var subpot = getSubpot(roundIndex);
winner.send(subpot);
rounds[roundIndex].isCashed[subpotIndex] = true;
}
function getHashOfBlock(uint blockIndex) constant returns(uint){
return uint(block.blockhash(blockIndex));
}
function getBuyers(uint roundIndex,address buyer) constant returns (address[]){
return rounds[roundIndex].buyers;
}
function getTicketsCountByBuyer(uint roundIndex,address buyer) constant returns (uint){
return rounds[roundIndex].ticketsCountByBuyer[buyer];
}
function getPot(uint roundIndex) constant returns(uint){
return rounds[roundIndex].pot;
}
function() {
var roundIndex = getRoundIndex();
var value = msg.value-(msg.value%ticketPrice);
if(value==0) return;
if(value<msg.value){
msg.sender.send(msg.value-value);
}
var ticketsCount = value/ticketPrice;
rounds[roundIndex].ticketsCount+=ticketsCount;
if(rounds[roundIndex].ticketsCountByBuyer[msg.sender]==0){
var buyersLength = rounds[roundIndex].buyers.length++;
rounds[roundIndex].buyers[buyersLength] = msg.sender;
}
rounds[roundIndex].ticketsCountByBuyer[msg.sender]+=ticketsCount;
rounds[roundIndex].ticketsCount+=ticketsCount;
rounds[roundIndex].pot+=value;
}
}