- Manage participants with a struct, a mapping and an array.
- Chain several modifiers to enforce business rules.
- Generate a pseudo-random index with
keccak256– and understand why on-chain randomness is risky.
Synopsis
Traditional lotteries depend on intermediaries to sell tickets, hold the money and pick the winner, so players must trust that the draw is fair and the prize is paid. A smart contract can make the rules – who may buy, how many tickets exist, how the winner is chosen and how much is won – public and automatic.
In this project the deploying account is the BlockAdmin. Other users buy one ticket each (the admin may not buy) until the ticket limit is reached. The admin then declares the result: the contract computes a pseudo-random index, selects that ticket holder as the winner and records the total collected amount as the winning amount.
Contract Architecture
| Part | Details |
|---|---|
| UserRecord.sol | File-level struct UserRecord { address useraddress; string username; uint amount; uint winningAmount; } |
| LotteryContract.sol | Contract LotteryContract; imports contracts/UserRecord.sol |
| state | TICKET_AMOUNT = 500, userlimit = 5, userCount, totalBalance, resultDeclared, address[] userAddressList |
| mapping | mapping(uint => UserRecord) public lotteryUserList – ticket number → buyer |
| modifiers | checkBlockAdmin (admin only), checkTicketUser (admin cannot buy), checkTicketUserCount (one ticket per address), checkUserCount (ticket limit), resultStatus (result not yet declared) |
lotteryTicketPurchase(name) | Stores the buyer, adds TICKET_AMOUNT to totalBalance, increases userCount. |
declareLotteryResult() | Admin only, once. Computes the winner index and stores winner with winningAmount = totalBalance. |
The Random Function
The contract picks a winner index with a hash of values that change from call to call:
uint winnerIndex = uint(keccak256(abi.encodePacked(msg.sender, block.timestamp, startvalue))) % userlimit;
abi.encodePackedjoins the caller’s address, the block timestamp and a constant into bytes.keccak256hashes them into a 256-bit number.% userlimitreduces it to an index between 0 and 4.
Key Requirements (Concepts Used)
- Struct declared at file level and imported (Chapters 2, 13).
- Mapping and dynamic array (Chapters 10, 12).
- Functions with string parameters in memory (Chapters 9, 15).
- Several modifiers on one function (Chapter 22).
- Global variables and hashing:
msg.sender,block.timestamp,keccak256,abi.encodePacked(Chapter 14).
How to Run in Remix
- Create
contracts/UserRecord.solandcontracts/LotteryContract.solfrom the resource files. - Compile and deploy LotteryContract from the first account (BlockAdmin).
- From five different accounts, call
lotteryTicketPurchasewith a name. Try buying twice, or buying from the admin account, to see the errors. - From the admin account call
declareLotteryResult(), then readwinner.
Limitations and Extension Exercises
| Observation in the teaching version | Improvement to try |
|---|---|
The index uses % userlimit (5). If fewer than 5 tickets were sold, the index can point to an empty record and the “winner” is the zero address. | Use % userCount and require at least one player before declaring. |
| Ticket amounts are numbers only; no Ether is paid or won. | Make the purchase function payable, require msg.value == ticketPrice and let the winner withdraw the pot (Chapter 24). |
| Randomness can be predicted or influenced by the admin. | Use a VRF oracle or a commit–reveal scheme. |
| The duplicate-buyer check loops over all buyers. | Use mapping(address => bool) hasTicket. |
Exercise solution (one possible approach – randomness still for learning only)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract LotteryV2 {
address public immutable admin = msg.sender;
uint public constant TICKET_PRICE = 0.01 ether;
uint public constant MAX_PLAYERS = 5;
address[] public players;
mapping(address => bool) public hasTicket;
address public winner;
function buyTicket() external payable {
require(msg.sender != admin, "Admin cannot play");
require(msg.value == TICKET_PRICE, "Wrong ticket price");
require(!hasTicket[msg.sender], "Already bought");
require(players.length < MAX_PLAYERS, "Tickets over");
hasTicket[msg.sender] = true;
players.push(msg.sender);
}
function declareResult() external {
require(msg.sender == admin, "Only admin");
require(winner == address(0), "Already declared");
require(players.length > 0, "No players");
uint index = uint(keccak256(abi.encodePacked(block.prevrandao, block.timestamp, players.length)))
% players.length; // only valid indexes
winner = players[index];
}
function claimPrize() external {
require(msg.sender == winner, "Not the winner");
uint prize = address(this).balance;
(bool ok, ) = payable(winner).call{value: prize}("");
require(ok, "Payment failed");
}
}
Key Takeaways
- Modifiers encode lottery rules: admin-only actions, one ticket per address, limited tickets, single draw.
- keccak256 over block data gives pseudo-randomness that is fine for learning but unsafe for real money.
- Always take the modulo of the actual number of players to stay within valid indexes.
Video : Project 2 : Smart Contract for Lottery System
Resource Files
Question / Answer (15 questions)
struct UserRecord { address useraddress; string username; uint amount; uint winningAmount; }, defined in UserRecord.sol.uint(keccak256(abi.encodePacked(msg.sender, block.timestamp, startvalue))) % userlimit – a hash of the caller, the block timestamp and a constant, reduced modulo 5.mapping(address => bool) hasTicket, which checks in constant time whether an address has already bought a ticket.MCQ Test : Check Your Understanding (5 questions)
Select an option and press Check Answer, or answer all questions and press Submit Test to see your score. Each answer comes with an explanation.
Bibliography/References
The notes, questions and MCQs on this page are original explanations written for this course. The works listed below were consulted and are cited for attribution and further reading. Product names, logos and trademarks belong to their respective owners. Short quotations, where used, are marked and attributed.
- Solidity Team. (n.d.). Units and Globally Available Variables. Solidity Documentation. https://docs.soliditylang.org/en/latest/units-and-global-variables.html
- Solidity Team. (n.d.). Security Considerations. Solidity Documentation. https://docs.soliditylang.org/en/latest/security-considerations.html
- Chainlink. (n.d.). Chainlink VRF (Verifiable Random Function) documentation. https://docs.chain.link/vrf
- ethereum.org. (n.d.). Oracles. Ethereum Developer Documentation. https://ethereum.org/en/developers/docs/oracles/
- Remix Project. (n.d.). Remix IDE Documentation. https://remix-ide.readthedocs.io/en/latest/
- Modi, R. (2018). Solidity Programming Essentials: A beginner’s guide to build smart contracts for Ethereum and blockchain. Packt Publishing. ISBN 978-1-78883-138-3.