Encrypted Lottery
Provably fair lottery with encrypted ticket numbers and FHE randomness. Players buy tickets with hidden numbers. Winners are determined by comparing encrypted values, ensuring no one sees numbers before the draw.
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;
import {
FHE,
euint64,
euint8,
ebool,
externalEuint64
} from "@fhevm/solidity/lib/FHE.sol";
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
/**
* @notice Provably fair lottery with encrypted ticket numbers and FHE randomness.
* Players buy tickets with hidden numbers. Winners are determined by
* comparing encrypted values, ensuring no one sees numbers before the draw.
*
* @dev Flow: buyTicket() → startDrawing() → checkAndClaim() → revealWinner()
*/
contract EncryptedLottery is ZamaEthereumConfig {
enum LotteryState {
Open, // Accepting tickets
Drawing, // Drawing in progress
Completed // Winner revealed
}
struct Ticket {
address owner;
euint64 number;
}
address public owner;
/// Current lottery state
LotteryState public state;
/// Ticket price in wei
uint256 public ticketPrice;
/// Lottery end time
uint256 public endTime;
/// All tickets
Ticket[] private _tickets;
/// Mapping from address to ticket indices
mapping(address => uint256[]) private _playerTickets;
/// Encrypted winning number
euint64 private _winningNumber;
/// Winner address (if found)
address public winner;
/// Prize pool
uint256 public prizePool;
/// Lottery round number
uint256 public roundNumber;
/// Emitted when a ticket is purchased
/// @param buyer Address of ticket buyer
/// @param ticketIndex Index of the ticket
event TicketPurchased(address indexed buyer, uint256 indexed ticketIndex);
/// @notice Emitted when drawing starts
/// @param roundNumber Current round
event DrawingStarted(uint256 indexed roundNumber);
/// @notice Emitted when winner is found
/// @param winner Address of winner
/// @param prize Amount won
event WinnerFound(address indexed winner, uint256 prize);
/// @notice Emitted when no winner found
/// @param roundNumber Current round
/// @param rollover Amount rolled to next round
event NoWinner(uint256 indexed roundNumber, uint256 rollover);
modifier onlyOwner() {
require(msg.sender == owner, "Only owner");
_;
}
constructor(uint256 _ticketPrice, uint256 _duration) {
require(_ticketPrice > 0, "Ticket price must be > 0");
require(_duration > 0, "Duration must be > 0");
owner = msg.sender;
ticketPrice = _ticketPrice;
endTime = block.timestamp + _duration;
state = LotteryState.Open;
roundNumber = 1;
}
/// @notice Purchase a lottery ticket with encrypted number
/// @param encryptedNumber Your encrypted ticket number
/// @param inputProof Proof validating the encrypted input
function buyTicket(
externalEuint64 encryptedNumber,
bytes calldata inputProof
) external payable {
require(state == LotteryState.Open, "Lottery not open");
require(block.timestamp < endTime, "Lottery ended");
require(msg.value >= ticketPrice, "Insufficient payment");
// Convert and store encrypted ticket number
euint64 ticketNumber = FHE.fromExternal(encryptedNumber, inputProof);
// ✅ Grant contract permission
FHE.allowThis(ticketNumber);
// 📋 Store ticket
uint256 ticketIndex = _tickets.length;
_tickets.push(Ticket({owner: msg.sender, number: ticketNumber}));
_playerTickets[msg.sender].push(ticketIndex);
prizePool += msg.value;
emit TicketPurchased(msg.sender, ticketIndex);
}
/// @notice Start the drawing process
/// @dev Only owner can call after lottery ends
function startDrawing() external onlyOwner {
require(state == LotteryState.Open, "Wrong state");
require(block.timestamp >= endTime, "Lottery not ended");
require(_tickets.length > 0, "No tickets sold");
// 🎲 Generate "random" winning number using block data
// ⚠️ WARNING: This is predictable! Use Chainlink VRF in production
uint64 randomSeed = uint64(
uint256(
keccak256(
abi.encodePacked(
block.prevrandao,
block.timestamp,
_tickets.length,
msg.sender
)
)
)
);
// 🔐 Encrypt the winning number
_winningNumber = FHE.asEuint64(randomSeed);
FHE.allowThis(_winningNumber);
state = LotteryState.Drawing;
emit DrawingStarted(roundNumber);
}
/// @notice Check if a ticket is a winner and claim prize
/// @param ticketIndex Index of the ticket to check
function checkAndClaim(uint256 ticketIndex) external {
require(state == LotteryState.Drawing, "Not in drawing phase");
require(ticketIndex < _tickets.length, "Invalid ticket");
require(_tickets[ticketIndex].owner == msg.sender, "Not your ticket");
// 🔍 Check if ticket number matches winning number
// This comparison happens in encrypted space!
ebool isWinner = FHE.eq(_tickets[ticketIndex].number, _winningNumber);
// 🔓 Make result publicly decryptable
FHE.allowThis(isWinner);
FHE.makePubliclyDecryptable(isWinner);
// Store for later reveal
// In production, use callback pattern
}
/// @notice Reveal winner with decryption proof
/// @param ticketIndex Ticket being checked
/// @param abiEncodedResult ABI-encoded bool result
/// @param decryptionProof KMS signature proving decryption
function revealWinner(
uint256 ticketIndex,
bytes memory abiEncodedResult,
bytes memory decryptionProof
) external {
require(state == LotteryState.Drawing, "Not in drawing phase");
require(ticketIndex < _tickets.length, "Invalid ticket");
// Rebuild the comparison for verification
ebool isWinner = FHE.eq(_tickets[ticketIndex].number, _winningNumber);
bytes32[] memory cts = new bytes32[](1);
cts[0] = FHE.toBytes32(isWinner);
// Verify decryption proof
FHE.checkSignatures(cts, abiEncodedResult, decryptionProof);
bool won = abi.decode(abiEncodedResult, (bool));
if (won) {
winner = _tickets[ticketIndex].owner;
state = LotteryState.Completed;
uint256 prize = prizePool;
prizePool = 0;
// Transfer prize
(bool sent, ) = winner.call{value: prize}("");
require(sent, "Prize transfer failed");
emit WinnerFound(winner, prize);
}
}
/// @notice End drawing with no winner (rollover)
/// @dev Called if all tickets checked with no match
function endDrawingNoWinner() external onlyOwner {
require(state == LotteryState.Drawing, "Not in drawing phase");
state = LotteryState.Completed;
emit NoWinner(roundNumber, prizePool);
}
/// @notice Start a new lottery round
/// @param _duration Duration for next round
function startNewRound(uint256 _duration) external onlyOwner {
require(state == LotteryState.Completed, "Current round not complete");
require(_duration > 0, "Duration must be > 0");
// Reset for new round
delete _tickets;
winner = address(0);
endTime = block.timestamp + _duration;
state = LotteryState.Open;
roundNumber++;
// prizePool carries over if no winner
}
function getTicketCount() external view returns (uint256) {
return _tickets.length;
}
/// @notice Get ticket indices for a player
function getPlayerTickets(
address player
) external view returns (uint256[] memory) {
return _playerTickets[player];
}
/// @notice Check time remaining
function timeRemaining() external view returns (uint256) {
if (block.timestamp >= endTime) return 0;
return endTime - block.timestamp;
}
/// @notice Get lottery info
function getLotteryInfo()
external
view
returns (
LotteryState currentState,
uint256 currentPrizePool,
uint256 currentEndTime,
uint256 currentRound,
uint256 totalTickets
)
{
return (state, prizePool, endTime, roundNumber, _tickets.length);
}
}
Last updated