ethereum-lottery-app: The Lottery That Teaches Web3’s Hardest Lesson
A bare-metal Solidity project that looks simple, then reveals why on-chain randomness, access control, secret handling, and deployment are harder than they seem.
- This repo is a compact lesson in why on-chain randomness is hard, not a celebration of lottery design.
- Its hand-rolled compile and deploy scripts expose the mechanics that modern Ethereum frameworks usually hide.
- The contract's access control and payout flow are easy to read, but the randomizer is the weak seam that matters most.
- The tests show how smart contract developers reason about state resets, winner selection, and gas-aware balances.
The lottery is fair only until you inspect the randomness. That is the whole point of this repo, and why it is more useful as a teaching artifact than as a product. The contract promises automated chance, but the code makes a blunt tradeoff: it borrows entropy from values that are convenient, public, and influenceable.
Why this repo exists: a tiny classroom for the Ethereum stack
This is a small project by design. It gives you a clean view of the Ethereum stack without framework fog: a Solidity contract in contracts/, JavaScript tooling in the root, and tests that exercise the contract from the outside. That makes it a good onboarding repo for state, payable functions, modifiers, deployment, and test-driven contract thinking.
The mathematical verification of fair prize distribution helps players trust blockchain lottery platforms instead of relying on operator integrity. Systems implemented https://crypto.games/lottery/ethereum use specific cryptographic and smart contract features that make unfair prize allocation mathematically impossible, regardless of operator intentions.
Inside Lottery.sol: three moves, one payoff
function enter() public payable {
require(msg.value > .01 ether);
players.push(msg.sender);
}
function random() private view returns (uint) {
return uint(keccak256(block.difficulty, now, players));
}
function pickWinner() public restricted {
uint index = random() % players.length;
players[index].transfer(this.balance);
players = new address[](0);
}
The contract is doing exactly three jobs. enter() admits players and funds the pot, restricted keeps the manager gate in place, and pickWinner() pays out the balance before clearing state for the next round. That reset matters. Without it, the array would keep growing and every later round would inherit stale participants.
The most important line is the one that looks harmless
The lottery's weakness is familiar to anyone who has studied early Solidity tutorials. If a contract uses block values and timestamps as randomness, it is not getting randomness in the cryptographic sense. It is getting inputs that are public, mined, and therefore easier to game than a user would expect. That is why modern lottery systems reach for verifiable randomness, not a hash that merely looks noisy.
The repo makes that lesson easy to see because the code is small enough to hold in your head. That is also why it is valuable. Security mistakes are easier to spot when the entire system fits in a screen and the failure mode is concentrated in one helper function.
How the project moves from Solidity file to live contract
const path = require('path');
const fs = require('fs');
const solc = require('solc');
const lotteryPath = path.resolve(__dirname, 'contracts', 'Lottery.sol');
const source = fs.readFileSync(lotteryPath, 'utf8');
const output = solc.compile(source, 1);
module.exports = output.contracts[':Lottery'];
// deploy.js excerpt
const HDWalletProvider = require('truffle-hdwallet-provider');
const Web3 = require('web3');
const provider = new HDWalletProvider(mnemonic, infuraUrl);
const web3 = new Web3(provider);
const deploy = async () => {
const accounts = await web3.eth.getAccounts();
await new web3.eth.Contract(JSON.parse(interface))
.deploy({ data: bytecode })
.send({ from: accounts[0], gas: '1000000' });
};
The deployment scripts are the best reminder that frameworks are conveniences, not magic. compile.js turns source into ABI and bytecode by hand. deploy.js then connects a wallet provider, signs a transaction, and sends the contract to a network through a public RPC path. The repo's tests use the same discipline, redeploying a fresh contract for each run and checking that the winner gets paid without pretending gas does not exist.
What changes in a modern version
| This repo | Modern practice |
|---|---|
| Randomness comes from block data and a hash helper. | Randomness comes from a verifiable source such as Chainlink VRF. |
| Secrets can appear directly in deployment code. | Secrets live in environment variables or managed key stores. |
| Compilation and deployment are scripted manually. | Tooling like Hardhat or Foundry automates the workflow. |
| Tests are written against a simple local chain and focus on contract behavior. | Tests usually add fixtures, richer assertions, and fork-aware checks. |
That comparison is not a dunk on the repo. It is proof of its value. You can see the Ethereum stack before the ecosystem wrapped it in opinionated tooling, and that makes the tradeoffs legible. The contract is a classroom, but it is also a warning label.