Private Lottery
🟡 Intermediate | 🚀 Advanced
Fair lottery with encrypted ticket purchases and verifiable randomness
Overview
A lottery system where ticket purchases are encrypted - no one knows who bought how many tickets. Uses encrypted random number generation for winner selection. Demonstrates fair randomness with FHE and private participation amounts.
Quick Start
# Create new project from this template
npx labz create lottery my-project
# Navigate and install
cd my-project
npm install
# Run tests
npx hardhat testContract
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;
import { FHE, euint64, euint8, ebool, eaddress, externalEuint64, externalEuint8, externalEbool, externalEaddress } from "@fhevm/solidity/lib/FHE.sol";
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";
/// @title Lottery - Provably Fair Encrypted Lottery
/// @notice A lottery where the winner selection is encrypted and cannot be manipulated
/// @dev Uses FHE.random() for truly fair winner selection with async public decryption
contract Lottery is ZamaEthereumConfig {
// ============ Errors ============
/// @dev Lottery has already been drawn
error LotteryAlreadyDrawn();
/// @dev Lottery has not been drawn yet
error LotteryNotDrawn();
/// @dev Winner not yet revealed
error WinnerNotRevealed();
/// @dev Not enough participants
error NotEnoughParticipants();
/// @dev Caller is not the winner
error NotWinner();
/// @dev Prize already claimed
error PrizeAlreadyClaimed();
/// @dev Insufficient payment for ticket
error InsufficientPayment();
/// @dev Already entered the lottery
error AlreadyEntered();
/// @dev Invalid decryption proof
error InvalidDecryptionProof();
// ============ Events ============
/// @notice Emitted when someone enters the lottery
event LotteryEntered(address indexed participant, uint256 participantIndex);
/// @notice Emitted when the winner is drawn (encrypted)
event WinnerDrawn(uint256 totalParticipants);
/// @notice Emitted when winner index is ready for decryption
event WinnerReadyForReveal();
/// @notice Emitted when the winner is revealed
event WinnerRevealed(uint256 winnerIndex, address winner);
/// @notice Emitted when the prize is claimed
event PrizeClaimed(address indexed winner, uint256 amount);
// ============ State Variables ============
/// @dev List of lottery participants
address[] private _participants;
/// @dev Mapping to check if address has entered
mapping(address => bool) private _hasEntered;
/// @dev Ticket price in wei
uint256 private _ticketPrice;
/// @dev Encrypted winner index
euint64 private _encryptedWinnerIndex;
/// @dev Decrypted winner index (set after reveal)
uint256 private _revealedWinnerIndex;
/// @dev Whether the lottery has been drawn
bool private _isDrawn;
/// @dev Whether the winner has been revealed
bool private _isRevealed;
/// @dev Whether the prize has been claimed
bool private _isClaimed;
/// @dev Lottery end time
uint256 private _endTime;
// ============ Modifiers ============
/// @dev Ensures lottery is still open
modifier onlyBeforeDraw() {
if (_isDrawn) revert LotteryAlreadyDrawn();
if (block.timestamp > _endTime) revert LotteryAlreadyDrawn();
_;
}
/// @dev Ensures lottery has been drawn
modifier onlyAfterDraw() {
if (!_isDrawn) revert LotteryNotDrawn();
_;
}
/// @dev Ensures winner has been revealed
modifier onlyAfterReveal() {
if (!_isRevealed) revert WinnerNotRevealed();
_;
}
// ============ Constructor ============
/// @param ticketPriceWei The price per ticket in wei
/// @param durationSeconds How long the lottery runs
constructor(
uint256 ticketPriceWei,
uint256 durationSeconds
) {
_ticketPrice = ticketPriceWei;
_endTime = block.timestamp + durationSeconds;
}
// ============ External Functions ============
/// @notice Enter the lottery by paying the ticket price
function enter() external payable onlyBeforeDraw {
if (msg.value < _ticketPrice) revert InsufficientPayment();
if (_hasEntered[msg.sender]) revert AlreadyEntered();
// Add participant
_participants.push(msg.sender);
_hasEntered[msg.sender] = true;
emit LotteryEntered(msg.sender, _participants.length - 1);
}
/// @notice Draw the winner using encrypted randomness
/// @dev Uses FHE.random() and FHE.rem() for fair selection
function draw() external onlyBeforeDraw {
if (_participants.length < 2) revert NotEnoughParticipants();
// Generate encrypted random number
euint64 randomValue = FHE.randEuint64();
// Calculate winner index: random % participantCount
uint64 participantCount = uint64(_participants.length);
_encryptedWinnerIndex = FHE.rem(randomValue, participantCount);
// Set ACL - contract can use this value
FHE.allowThis(_encryptedWinnerIndex);
_isDrawn = true;
emit WinnerDrawn(_participants.length);
}
/// @notice Request the winner index to be publicly decryptable
/// @dev Step 1 of public decryption - marks ciphertext for off-chain decryption
function requestWinnerReveal() external onlyAfterDraw {
require(!_isRevealed, "Already revealed");
// Mark the encrypted winner index as publicly decryptable
FHE.makePubliclyDecryptable(_encryptedWinnerIndex);
emit WinnerReadyForReveal();
}
/// @notice Finalize winner reveal with decryption proof
/// @dev Step 3 of public decryption - verifies and stores the decrypted winner
/// @param winnerIndex The decrypted winner index from off-chain
/// @param decryptionProof The proof from Zama KMS validating the decryption
function finalizeWinnerReveal(
uint256 winnerIndex,
bytes calldata decryptionProof
) external onlyAfterDraw {
require(!_isRevealed, "Already revealed");
require(winnerIndex < _participants.length, "Invalid index");
// Verify the decryption proof
bytes32[] memory cts = new bytes32[](1);
cts[0] = euint64.unwrap(_encryptedWinnerIndex);
bytes memory cleartexts = abi.encode(winnerIndex);
// This reverts if proof is invalid
FHE.checkSignatures(cts, cleartexts, decryptionProof);
// Store the revealed winner
_revealedWinnerIndex = winnerIndex;
_isRevealed = true;
emit WinnerRevealed(winnerIndex, _participants[winnerIndex]);
}
/// @notice Claim the prize if you are the winner
function claimPrize() external onlyAfterReveal {
if (_isClaimed) revert PrizeAlreadyClaimed();
// Check if caller is the winner
if (_participants[_revealedWinnerIndex] != msg.sender) {
revert NotWinner();
}
_isClaimed = true;
// Transfer prize
uint256 prize = address(this).balance;
(bool success, ) = payable(msg.sender).call{value: prize}("");
require(success, "Transfer failed");
emit PrizeClaimed(msg.sender, prize);
}
/// @notice Get the encrypted winner index handle (for off-chain decryption)
/// @return The encrypted winner index
function getEncryptedWinnerIndex() external view onlyAfterDraw returns (euint64) {
return _encryptedWinnerIndex;
}
// ============ View Functions ============
/// @notice Get the number of participants
function getParticipantCount() external view returns (uint256) {
return _participants.length;
}
/// @notice Get the ticket price
function getTicketPrice() external view returns (uint256) {
return _ticketPrice;
}
/// @notice Get the current prize pool
function getPrizePool() external view returns (uint256) {
return address(this).balance;
}
/// @notice Get the lottery end time
function getEndTime() external view returns (uint256) {
return _endTime;
}
/// @notice Check if lottery has been drawn
function isDrawn() external view returns (bool) {
return _isDrawn;
}
/// @notice Check if winner has been revealed
function isRevealed() external view returns (bool) {
return _isRevealed;
}
/// @notice Check if prize has been claimed
function isClaimed() external view returns (bool) {
return _isClaimed;
}
/// @notice Get the revealed winner address (only after reveal)
function getWinner() external view onlyAfterReveal returns (address) {
return _participants[_revealedWinnerIndex];
}
/// @notice Get the revealed winner index (only after reveal)
function getWinnerIndex() external view onlyAfterReveal returns (uint256) {
return _revealedWinnerIndex;
}
/// @notice Check if an address has entered
function hasEntered(address participant) external view returns (bool) {
return _hasEntered[participant];
}
/// @notice Get participant at index
function getParticipant(uint256 index) external view returns (address) {
require(index < _participants.length, "Index out of bounds");
return _participants[index];
}
// ============ Internal Functions ============
}
FHE Operations Used
FHE.add()FHE.mod()FHE.eq()FHE.allowThis()FHE.allow()FHE.fromExternal()FHE.makePubliclyDecryptable()
FHE Types Used
euint64externalEuint64ebool
Tags
lottery gambling random privacy fairness
Related Examples
Prerequisites
Before this example, you should understand:
Next Steps
After this example, check out:
Generated with Lab-Z
Last updated
