Lottery with ERC7984
🔴 Advanced | 🏗️ OpenZeppelin Contracts
A fair lottery system using ERC7984 tokens for ticket purchases and prizes
Overview
This example demonstrates a lottery contract that uses ERC7984 tokens for ticket purchases. The ticket price is paid in confidential tokens, hiding how much each participant has wagered. Winner selection uses encrypted randomness, and the prize pool remains confidential until claimed.
Quick Start
# Create new project from this template
npx labz create lottery-erc7984 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.27;
import {FHE, externalEuint64, euint64, ebool} from "@fhevm/solidity/lib/FHE.sol";
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {IERC7984} from "@openzeppelin/confidential-contracts/interfaces/IERC7984.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/// @title LotteryERC7984 - Provably Fair Lottery with Confidential Token Payments
/// @notice A lottery where entries and prizes are paid in ERC7984 confidential tokens
/// @dev Combines FHE random number generation with ERC7984 token transfers
contract LotteryERC7984 is ZamaEthereumConfig, Ownable, ReentrancyGuard {
// ============ Errors ============
error LotteryAlreadyDrawn();
error LotteryNotDrawn();
error WinnerNotRevealed();
error NotEnoughParticipants();
error NotWinner();
error PrizeAlreadyClaimed();
error AlreadyEntered();
error InvalidDecryptionProof();
error NotOperator();
// ============ Events ============
event LotteryEntered(address indexed participant, uint256 participantIndex);
event WinnerDrawn(uint256 totalParticipants);
event WinnerReadyForReveal();
event WinnerRevealed(uint256 winnerIndex, address winner);
event PrizeClaimed(address indexed winner);
// ============ State ============
/// @dev The ERC7984 token used for payments
IERC7984 public immutable paymentToken;
/// @dev Ticket price (public, but paid in encrypted token)
uint64 public ticketPrice;
/// @dev List of participants
address[] private _participants;
/// @dev Track who has entered
mapping(address => bool) private _hasEntered;
/// @dev Total prize pool (encrypted)
euint64 private _prizePool;
/// @dev Encrypted winner index
euint64 private _encryptedWinnerIndex;
/// @dev Revealed winner index
uint256 private _revealedWinnerIndex;
/// @dev Lottery state flags
bool private _isDrawn;
bool private _isRevealed;
bool private _isClaimed;
/// @dev Lottery end time
uint256 private _endTime;
// ============ Constructor ============
constructor(
IERC7984 _paymentToken,
uint64 _ticketPrice,
uint256 durationSeconds
) Ownable(msg.sender) {
paymentToken = _paymentToken;
ticketPrice = _ticketPrice;
_endTime = block.timestamp + durationSeconds;
}
// ============ Entry Functions ============
/// @notice Enter the lottery by paying the ticket price in ERC7984 tokens
/// @param encryptedAmount Encrypted ticket payment (should equal ticketPrice)
/// @param inputProof Proof for the encrypted input
function enter(
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external nonReentrant {
if (_isDrawn) revert LotteryAlreadyDrawn();
if (block.timestamp > _endTime) revert LotteryAlreadyDrawn();
if (_hasEntered[msg.sender]) revert AlreadyEntered();
// Verify caller is operator for the token
if (!paymentToken.isOperator(msg.sender, address(this))) {
revert NotOperator();
}
euint64 amount = FHE.fromExternal(encryptedAmount, inputProof);
// Transfer ticket payment to this contract
FHE.allowTransient(amount, address(paymentToken));
euint64 transferred = paymentToken.confidentialTransferFrom(
msg.sender,
address(this),
amount
);
// Add to prize pool
_prizePool = FHE.add(_prizePool, transferred);
FHE.allowThis(_prizePool);
// Register participant
_participants.push(msg.sender);
_hasEntered[msg.sender] = true;
emit LotteryEntered(msg.sender, _participants.length - 1);
}
// ============ Draw Functions ============
/// @notice Draw the winner using encrypted randomness
function draw() external onlyOwner {
if (_isDrawn) revert LotteryAlreadyDrawn();
if (_participants.length < 2) revert NotEnoughParticipants();
// Generate encrypted random winner index
euint64 randomValue = FHE.randEuint64();
uint64 participantCount = uint64(_participants.length);
_encryptedWinnerIndex = FHE.rem(randomValue, participantCount);
FHE.allowThis(_encryptedWinnerIndex);
_isDrawn = true;
emit WinnerDrawn(_participants.length);
}
/// @notice Request winner index to be decrypted
function requestWinnerReveal() external {
if (!_isDrawn) revert LotteryNotDrawn();
if (_isRevealed) revert WinnerNotRevealed();
FHE.makePubliclyDecryptable(_encryptedWinnerIndex);
emit WinnerReadyForReveal();
}
/// @notice Finalize winner reveal with decryption proof
function finalizeWinnerReveal(
uint256 winnerIndex,
bytes calldata decryptionProof
) external {
if (!_isDrawn) revert LotteryNotDrawn();
if (_isRevealed) revert WinnerNotRevealed();
require(winnerIndex < _participants.length, "Invalid index");
// Verify decryption proof
bytes32[] memory cts = new bytes32[](1);
cts[0] = euint64.unwrap(_encryptedWinnerIndex);
bytes memory cleartexts = abi.encode(winnerIndex);
FHE.checkSignatures(cts, cleartexts, decryptionProof);
_revealedWinnerIndex = winnerIndex;
_isRevealed = true;
emit WinnerRevealed(winnerIndex, _participants[winnerIndex]);
}
// ============ Claim Functions ============
/// @notice Claim the prize if you are the winner
function claimPrize() external nonReentrant {
if (!_isRevealed) revert WinnerNotRevealed();
if (_isClaimed) revert PrizeAlreadyClaimed();
if (_participants[_revealedWinnerIndex] != msg.sender) revert NotWinner();
_isClaimed = true;
// Transfer prize pool to winner
FHE.allowTransient(_prizePool, address(paymentToken));
paymentToken.confidentialTransfer(msg.sender, _prizePool);
emit PrizeClaimed(msg.sender);
}
// ============ View Functions ============
function getParticipantCount() external view returns (uint256) {
return _participants.length;
}
function getTicketPrice() external view returns (uint64) {
return ticketPrice;
}
function getEndTime() external view returns (uint256) {
return _endTime;
}
function isDrawn() external view returns (bool) {
return _isDrawn;
}
function isRevealed() external view returns (bool) {
return _isRevealed;
}
function isClaimed() external view returns (bool) {
return _isClaimed;
}
function getWinner() external view returns (address) {
if (!_isRevealed) revert WinnerNotRevealed();
return _participants[_revealedWinnerIndex];
}
function hasEntered(address participant) external view returns (bool) {
return _hasEntered[participant];
}
function getParticipant(uint256 index) external view returns (address) {
require(index < _participants.length, "Index out of bounds");
return _participants[index];
}
function getEncryptedPrizePool() external view returns (euint64) {
return _prizePool;
}
}
Code Explanation
Enter
Enter the lottery by paying the ticket price in ERC7984 tokens. The payment amount is encrypted, so other participants can't see your stake.
Lines 30-50
Draw
Draw the winner using encrypted random number generation. The winner index is computed without revealing intermediate values.
Lines 55-75
Claim
Winner claims the prize pool. The prize amount transfer is confidential.
Lines 80-95
FHE Operations Used
FHE.confidentialTransferFrom()FHE.FHE.randEuint64()FHE.fromExternal()
FHE Types Used
euint64euint32
Tags
lottery random ERC7984 gaming prize OpenZeppelin
Related Examples
Prerequisites
Before this example, you should understand:
Next Steps
After this example, check out:
Generated with Lab-Z
Last updated
