Skip to content

Instantly share code, notes, and snippets.

@Lohann
Last active July 10, 2022 15:15
Show Gist options
  • Save Lohann/180ea19714b94e8fce6f5275b55fce39 to your computer and use it in GitHub Desktop.
Save Lohann/180ea19714b94e8fce6f5275b55fce39 to your computer and use it in GitHub Desktop.
Example of a simple lottery smart-contract written in solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Lottery {
// PRICE PER TICKET
uint256 constant TICKET_PRICE = (10 ** 18) / 100; // 0.01eth
uint256 constant TICKET_OPTIONS = 30;
uint256 constant OPTIONS_PER_TICKET = 3;
uint256 constant TICKET_BITMASK = 2 ** TICKET_OPTIONS - 1;
// ref: https://docs.soliditylang.org/en/v0.4.24/units-and-global-variables.html#time-units
uint256 constant MINUTE = 60; // 60 seconds
uint256 constant HOUR = MINUTE * 60; // 3600 seconds
uint256 constant DAY = HOUR * 24; // 86400 seconds
uint256 constant BLOCK_INTERVAL = 16; // ~16 seconds average for ethereum mainnet
uint256 constant LOTTERY_DURATION = (DAY * 5) / BLOCK_INTERVAL; // 5 days
uint256 constant DISTRIBUTION_DURATION = (DAY * 2) / BLOCK_INTERVAL; // 2 days
uint256 constant ROUND_DURATION = LOTTERY_DURATION + DISTRIBUTION_DURATION;
uint256 immutable START_BLOCK = block.number;
enum Status { OPEN, CLOSE, DISTRIBUTING_REWARDS }
struct Round {
mapping(address => mapping(bytes32 => uint256)) tickets;
mapping(bytes32 => uint256) ticketCount;
bytes32 winnerTicket;
}
mapping(uint256 => Round) private _rounds;
constructor() payable {}
// Count the number of ones in the binary representation of the ticket
function _countOnes(uint256 num) private pure returns (uint256) {
uint256 count = 0;
unchecked {
while (num > 0) {
count++;
num -= num & ((~num) + 1);
}
}
return count;
}
// Verify if the ticket is valid
function _validateTicket(bytes32 ticket) private pure {
uint256 bitflags = uint256(ticket);
require(bitflags <= TICKET_BITMASK, "Invalid options selected");
require(_countOnes(bitflags) == OPTIONS_PER_TICKET, "Invalid number of options");
}
function _nextRandom() private view returns (uint256) {
// Do not rely on block.timestamp or blockhash as a source of randomness, unless you know what you are doing.
// Both the timestamp and the block hash can be influenced by miners to some degree. Bad actors in the mining
// community can for example run a casino payout function on a chosen hash and just retry a different hash if
// they did not receive any money.
// The current block timestamp must be strictly larger than the timestamp of the last block, but the only
// guarantee is that it will be somewhere between the timestamps of two consecutive blocks in the canonical chain.
// TODO: Replace by a secure VRF
return uint256(blockhash(block.number - 1));
}
function _roundIndex() private view returns (uint256) {
return (block.number - START_BLOCK) / ROUND_DURATION;
}
function _roundStatus() private view returns (Status) {
uint256 round = _roundIndex();
uint256 closeBlock = START_BLOCK + LOTTERY_DURATION + (round * ROUND_DURATION);
if (block.number < closeBlock) {
return Status.OPEN;
} else if (_rounds[round].winnerTicket == bytes32(0)) {
return Status.CLOSE;
} else {
return Status.DISTRIBUTING_REWARDS;
}
}
function _getCurrentRound() private view returns (Round storage) {
return _rounds[_roundIndex()];
}
modifier checkIsOpen() {
require(_roundStatus() == Status.OPEN, "The lottery is not open");
_;
}
modifier checkIsClose() {
require(_roundStatus() == Status.CLOSE, "The lottery is not close");
_;
}
modifier checkIsDistributingRewards() {
require(_roundStatus() == Status.DISTRIBUTING_REWARDS, "The lottery is not distributing rewards");
_;
}
function buyTicket(bytes32 ticket) external payable checkIsOpen {
require(msg.value >= TICKET_PRICE, "Not enough funds");
_validateTicket(ticket);
Round storage round = _getCurrentRound();
round.tickets[msg.sender][ticket]++;
round.ticketCount[ticket]++;
}
function _generateRandomTicket(uint256 random) private pure returns (bytes32) {
unchecked {
uint256 ticket = 0;
uint256[TICKET_OPTIONS] memory options;
// Set on N random bits
for (uint256 i=0; i<OPTIONS_PER_TICKET; i++) {
// Pick one random byte
uint256 randomByte = random & 0xFF;
random >>= 8;
// Random index between 0 and OPTIONS-i
uint256 index = randomByte % (TICKET_OPTIONS - i);
// Set byte ON
uint256 aux = options[index];
if (aux == 0) {
aux = index;
}
ticket |= 1 << aux;
// Remove byte from the list
aux = options[TICKET_OPTIONS - 1 - i];
if (aux == 0) {
aux = TICKET_OPTIONS - 1 - i;
}
options[index] = aux;
}
return bytes32(ticket);
}
}
function closeLottery() external payable checkIsClose {
// Random winnerTicket
uint256 random = _nextRandom();
bytes32 ticket = _generateRandomTicket(random);
// Sanity check
_validateTicket(ticket);
Round storage round = _getCurrentRound();
round.winnerTicket = ticket;
}
function claimRewardFor(address payable account) public checkIsDistributingRewards returns (bool) {
Round storage round = _getCurrentRound();
bytes32 ticket = round.winnerTicket;
uint256 accountTickets = round.tickets[account][ticket];
require(accountTickets > 0, "The account doesn't have a winner ticket");
uint256 winnersCount = round.ticketCount[ticket];
// Remove account's tickets
round.tickets[account][ticket] = 0;
round.ticketCount[ticket] -= accountTickets;
// Calculate reward
uint256 totalReward = address(this).balance;
uint256 reward = (accountTickets * totalReward) / winnersCount;
// Transfer reward
account.transfer(reward);
return true;
}
function claimReward() external returns (bool) {
return claimRewardFor(payable(msg.sender));
}
// allow anyone to deposit some value to this contract
receive() external payable {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment