Last active
July 10, 2022 15:15
-
-
Save Lohann/180ea19714b94e8fce6f5275b55fce39 to your computer and use it in GitHub Desktop.
Example of a simple lottery smart-contract written in solidity
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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