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.

11 min read • View on GitHub • More from zubair-trabzada

A brass lottery machine sits on top of a ledger-like blockchain base, with player tokens queued below and a manager hand on the lever. The image explains the article's central idea: a lottery can look fair while still hiding control in the mechanism.
The system looks impartial from a distance, but the mechanism still decides who has leverage.
Key Takeaways

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.

Jesus L. Federico, Author · Agentri Poker article

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 small, but its control flow is strict: join, choose, pay, reset.

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

A close-up view shows three conduits feeding into a hashing chamber, with one control dial nudged by a miner's hand and a narrow chute marked by a single winner outcome. The illustration explains why block-derived values can create the appearance of randomness without providing real unpredictability.
The problem is not the formula. It is the inputs, which are too visible and too influenceable.

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

A desk holds a terminal window, a deployment script, and a folded paper mnemonic slip moving toward a network bridge that leads to a sealed contract on a public chain. The image explains the repo's manual deployment flow and why secret handling belongs in the workflow, not in source files.
The deployment path is simple enough to read line by line, which is also why secret handling matters so much.
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 repoModern 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.