Lottery with ERC7984
Overview
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
Draw
Claim
FHE Operations Used
FHE Types Used
Tags
Related Examples
Prerequisites
Next Steps
Last updated
