|
| 1 | +# Lottery with Pyth Entropy |
| 2 | + |
| 3 | +This example demonstrates how to build a provably fair lottery application using Pyth Entropy for verifiable randomness. The application showcases a complete implementation where each lottery ticket draws its own random number from Entropy, and a winner is selected based on which ticket's random number is closest to a final target number. |
| 4 | + |
| 5 | +## What This Example Does |
| 6 | + |
| 7 | +The Lottery example implements a complete lottery system with three distinct phases: |
| 8 | + |
| 9 | +### Phase 1: Ticket Sales (ACTIVE) |
| 10 | +Users can purchase lottery tickets during the active phase. Each ticket purchase triggers a request to Pyth Entropy for a unique random number. The process works as follows: |
| 11 | +- User pays the ticket price plus the Entropy fee |
| 12 | +- Contract requests a random number from Pyth Entropy using `IEntropyV2.requestV2()` |
| 13 | +- Each ticket is assigned a sequence number that uniquely identifies its random number request |
| 14 | +- The ticket price is added to the prize pool |
| 15 | + |
| 16 | +### Phase 2: Drawing (DRAWING) |
| 17 | +When the lottery owner decides to end the lottery: |
| 18 | +- The owner calls `endLottery()` which triggers one final random number request |
| 19 | +- This final number serves as the "target" for determining the winner |
| 20 | +- The contract status changes to DRAWING while waiting for the final random number |
| 21 | + |
| 22 | +### Phase 3: Winner Selection (ENDED) |
| 23 | +Once the final target random number is revealed through Entropy's callback: |
| 24 | +- The contract automatically calculates which ticket's random number is closest to the target |
| 25 | +- The winner is determined based on the absolute difference: `|ticket_random - target_random|` |
| 26 | +- The winner can then claim the entire prize pool |
| 27 | + |
| 28 | +### Key Features |
| 29 | + |
| 30 | +**True Randomness Per Ticket**: Unlike simple lottery systems that draw one random number at the end, this implementation ensures each ticket gets its own verifiable random number from Pyth Entropy. This provides transparency and fairness, as all random numbers are generated independently and can be verified on-chain. |
| 31 | + |
| 32 | +**Entropy V2 Integration**: The contract uses the latest `IEntropyV2` interface from Pyth, utilizing the simplified `requestV2()` method that leverages in-contract PRNG for user contribution. |
| 33 | + |
| 34 | +**Automatic Winner Selection**: The winner is automatically determined when the final random number callback is received, eliminating the need for additional transactions. |
| 35 | + |
| 36 | +**Complete Frontend**: A Next.js application with Web3 wallet integration allows users to buy tickets, view their tickets with real-time random number updates, and claim prizes. |
| 37 | + |
| 38 | +## Project Structure |
| 39 | + |
| 40 | +``` |
| 41 | +entropy/lottery/ |
| 42 | +├── contract/ # Smart contracts built with Hardhat |
| 43 | +│ ├── contracts/ |
| 44 | +│ │ └── Lottery.sol # Main lottery contract with Entropy integration |
| 45 | +│ ├── ignition/ |
| 46 | +│ │ └── modules/ |
| 47 | +│ │ └── Lottery.ts # Deployment configuration |
| 48 | +│ ├── package.json |
| 49 | +│ └── hardhat.config.ts |
| 50 | +│ |
| 51 | +├── app/ # Next.js frontend application |
| 52 | +│ ├── app/ |
| 53 | +│ │ ├── (home)/ |
| 54 | +│ │ │ ├── page.tsx # Main lottery interface |
| 55 | +│ │ │ └── components/ # Lottery-specific components |
| 56 | +│ │ │ ├── buy-ticket.tsx # Ticket purchase component |
| 57 | +│ │ │ ├── my-tickets.tsx # User tickets display |
| 58 | +│ │ │ └── lottery-status.tsx # Lottery status and winner info |
| 59 | +│ │ ├── layout.tsx # App layout with providers |
| 60 | +│ │ └── globals.css # Global styles |
| 61 | +│ ├── components/ # Reusable UI components |
| 62 | +│ ├── contracts/ # Generated contract ABIs and types |
| 63 | +│ ├── providers/ # Wagmi and React Query providers |
| 64 | +│ ├── config.ts # Chain configuration |
| 65 | +│ └── package.json |
| 66 | +│ |
| 67 | +└── README.md # This file |
| 68 | +``` |
| 69 | + |
| 70 | +## Prerequisites |
| 71 | + |
| 72 | +Before running this example, ensure you have: |
| 73 | + |
| 74 | +- **Node.js** (v18 or later) |
| 75 | +- **npm** or **bun** package manager |
| 76 | +- A Web3 wallet (e.g., MetaMask) with testnet funds |
| 77 | +- Access to a testnet where Pyth Entropy is deployed (e.g., Blast Sepolia, Optimism Sepolia, Arbitrum Sepolia) |
| 78 | + |
| 79 | +## Running the Example |
| 80 | + |
| 81 | +### Step 1: Deploy the Smart Contract |
| 82 | + |
| 83 | +Navigate to the contract directory and install dependencies: |
| 84 | + |
| 85 | +```bash |
| 86 | +cd contract |
| 87 | +npm install |
| 88 | +``` |
| 89 | + |
| 90 | +Create a `.env` file in the contract directory with your wallet private key: |
| 91 | + |
| 92 | +```env |
| 93 | +WALLET_KEY=your_private_key_here |
| 94 | +BLAST_SCAN_API_KEY=your_api_key_here # Optional, for verification |
| 95 | +``` |
| 96 | + |
| 97 | +Deploy the contract to Blast Sepolia testnet: |
| 98 | + |
| 99 | +```bash |
| 100 | +npx hardhat ignition deploy ignition/modules/Lottery.ts --network blast-sepolia --verify |
| 101 | +``` |
| 102 | + |
| 103 | +The deployment module is configured with: |
| 104 | +- **Entropy Contract**: `0x98046Bd286715D3B0BC227Dd7a956b83D8978603` (Blast Sepolia) |
| 105 | +- **Entropy Provider**: `0x6CC14824Ea2918f5De5C2f75A9Da968ad4BD6344` |
| 106 | +- **Ticket Price**: 0.001 ETH |
| 107 | + |
| 108 | +After deployment, note the deployed contract address. You'll need to update this in the frontend. |
| 109 | + |
| 110 | +### Step 2: Configure the Frontend |
| 111 | + |
| 112 | +Navigate to the app directory and install dependencies: |
| 113 | + |
| 114 | +```bash |
| 115 | +cd ../app |
| 116 | +npm install |
| 117 | +``` |
| 118 | + |
| 119 | +Update the contract address in `contracts/addresses.ts` with your deployed contract address: |
| 120 | + |
| 121 | +```typescript |
| 122 | +export const lotteryAddress = "YOUR_DEPLOYED_CONTRACT_ADDRESS" as const; |
| 123 | +``` |
| 124 | + |
| 125 | +Generate the contract types for TypeScript: |
| 126 | + |
| 127 | +```bash |
| 128 | +npx wagmi generate |
| 129 | +``` |
| 130 | + |
| 131 | +### Step 3: Run the Frontend |
| 132 | + |
| 133 | +Start the development server: |
| 134 | + |
| 135 | +```bash |
| 136 | +npm run dev |
| 137 | +``` |
| 138 | + |
| 139 | +The application will be available at http://localhost:3000. |
| 140 | + |
| 141 | +### Step 4: Interact with the Lottery |
| 142 | + |
| 143 | +1. **Connect Wallet**: Click the wallet button to connect your Web3 wallet (make sure you're on the correct network) |
| 144 | + |
| 145 | +2. **Buy Tickets**: In the "Buy Tickets" tab, purchase one or more lottery tickets. Each purchase will: |
| 146 | + - Charge you the ticket price (0.001 ETH) plus the Entropy fee |
| 147 | + - Request a random number from Pyth Entropy |
| 148 | + - Show your ticket with a "Waiting for random number..." status |
| 149 | + |
| 150 | +3. **View Your Tickets**: Switch to the "My Tickets" tab to see all your purchased tickets. Once the Entropy callback completes (usually within a few seconds), you'll see the random number assigned to each ticket. |
| 151 | + |
| 152 | +4. **End the Lottery** (Owner Only): If you're the contract owner, you can end the lottery after tickets have been sold. This triggers the final random number draw to determine the winner. |
| 153 | + |
| 154 | +5. **Claim Prize**: If you're the winner, a "Claim Your Prize!" button will appear in the Lottery Status card. Click it to transfer the entire prize pool to your wallet. |
| 155 | + |
| 156 | +## How It Works: Technical Details |
| 157 | + |
| 158 | +### Smart Contract Architecture |
| 159 | + |
| 160 | +The `Lottery.sol` contract implements the `IEntropyConsumer` interface to receive callbacks from Pyth Entropy: |
| 161 | + |
| 162 | +```solidity |
| 163 | +contract Lottery is IEntropyConsumer, Ownable, ReentrancyGuard { |
| 164 | + // Lottery states: ACTIVE -> DRAWING -> ENDED |
| 165 | + enum LotteryStatus { ACTIVE, DRAWING, ENDED } |
| 166 | + |
| 167 | + // Each ticket stores its buyer, sequence number, and random number |
| 168 | + struct Ticket { |
| 169 | + address buyer; |
| 170 | + uint64 sequenceNumber; |
| 171 | + bytes32 randomNumber; |
| 172 | + bool fulfilled; |
| 173 | + } |
| 174 | + |
| 175 | + // ... |
| 176 | +} |
| 177 | +``` |
| 178 | + |
| 179 | +### Buying a Ticket |
| 180 | + |
| 181 | +When a user buys a ticket, the contract: |
| 182 | + |
| 183 | +1. Validates the lottery is in ACTIVE status |
| 184 | +2. Checks sufficient payment (ticket price + entropy fee) |
| 185 | +3. Calls `entropy.requestV2()` to request a random number |
| 186 | +4. Stores the ticket with its sequence number |
| 187 | +5. Adds the ticket price to the prize pool |
| 188 | + |
| 189 | +```solidity |
| 190 | +function buyTicket() external payable nonReentrant { |
| 191 | + // ... validation ... |
| 192 | + |
| 193 | + uint64 sequenceNumber = entropy.requestV2{value: entropyFee}(); |
| 194 | + |
| 195 | + tickets[ticketId] = Ticket({ |
| 196 | + buyer: msg.sender, |
| 197 | + sequenceNumber: sequenceNumber, |
| 198 | + randomNumber: bytes32(0), |
| 199 | + fulfilled: false |
| 200 | + }); |
| 201 | + |
| 202 | + // ... |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +### Entropy Callback |
| 207 | + |
| 208 | +The Pyth Entropy protocol calls back the contract with random numbers: |
| 209 | + |
| 210 | +```solidity |
| 211 | +function entropyCallback( |
| 212 | + uint64 sequenceNumber, |
| 213 | + address, |
| 214 | + bytes32 randomNumber |
| 215 | +) internal override { |
| 216 | + if (sequenceNumber == winningSequenceNumber) { |
| 217 | + // This is the final winning target number |
| 218 | + winningTargetNumber = randomNumber; |
| 219 | + |
| 220 | + // Find the ticket closest to the target |
| 221 | + for (uint256 i = 0; i < ticketCount; i++) { |
| 222 | + if (tickets[i].fulfilled) { |
| 223 | + uint256 difference = _calculateDifference( |
| 224 | + tickets[i].randomNumber, |
| 225 | + winningTargetNumber |
| 226 | + ); |
| 227 | + // Track the closest ticket... |
| 228 | + } |
| 229 | + } |
| 230 | + |
| 231 | + status = LotteryStatus.ENDED; |
| 232 | + } else { |
| 233 | + // This is a ticket's random number |
| 234 | + uint256 ticketId = sequenceToTicketId[sequenceNumber]; |
| 235 | + tickets[ticketId].randomNumber = randomNumber; |
| 236 | + tickets[ticketId].fulfilled = true; |
| 237 | + } |
| 238 | +} |
| 239 | +``` |
| 240 | + |
| 241 | +### Winner Determination |
| 242 | + |
| 243 | +The winner is determined by calculating which ticket's random number has the smallest absolute difference from the target: |
| 244 | + |
| 245 | +```solidity |
| 246 | +function _calculateDifference(bytes32 a, bytes32 b) private pure returns (uint256) { |
| 247 | + uint256 aValue = uint256(a); |
| 248 | + uint256 bValue = uint256(b); |
| 249 | + return aValue > bValue ? aValue - bValue : bValue - aValue; |
| 250 | +} |
| 251 | +``` |
| 252 | + |
| 253 | +### Frontend Implementation |
| 254 | + |
| 255 | +The frontend uses Wagmi v2 for Web3 interactions and React Query for state management: |
| 256 | + |
| 257 | +- **BuyTicket Component**: Handles ticket purchases and displays the cost |
| 258 | +- **MyTickets Component**: Lists user's tickets with real-time status updates |
| 259 | +- **LotteryStatus Component**: Shows prize pool, ticket count, and winner information |
| 260 | + |
| 261 | +The app automatically listens for blockchain events to update the UI when random numbers are revealed or the lottery ends. |
| 262 | + |
| 263 | +## Supported Networks |
| 264 | + |
| 265 | +This example is configured for multiple testnets: |
| 266 | + |
| 267 | +- **Blast Sepolia** (default in deployment) |
| 268 | +- **Optimism Sepolia** |
| 269 | +- **Arbitrum Sepolia** |
| 270 | + |
| 271 | +To deploy to a different network: |
| 272 | + |
| 273 | +1. Find the Entropy contract and provider addresses for your target network at https://docs.pyth.network/entropy |
| 274 | +2. Update `ignition/modules/Lottery.ts` with the correct addresses |
| 275 | +3. Ensure the network is configured in `hardhat.config.ts` and `app/config.ts` |
| 276 | +4. Deploy using `--network <network-name>` |
| 277 | + |
| 278 | +## Security Considerations |
| 279 | + |
| 280 | +**Randomness Source**: This implementation uses `IEntropyV2.requestV2()` without a user-provided random number. This method uses in-contract PRNG for the user contribution, which means you trust the validator to honestly generate random numbers. For applications requiring stronger guarantees against validator collusion, consider using the variant that accepts a `userRandomNumber` parameter. |
| 281 | + |
| 282 | +**Reentrancy Protection**: The contract uses OpenZeppelin's `ReentrancyGuard` to prevent reentrancy attacks on critical functions like `buyTicket()` and `claimPrize()`. |
| 283 | + |
| 284 | +**Access Control**: Only the contract owner can end the lottery using the `endLottery()` function, preventing premature lottery closures. |
| 285 | + |
| 286 | +## Development Notes |
| 287 | + |
| 288 | +### Technology Stack |
| 289 | + |
| 290 | +**Smart Contracts**: |
| 291 | +- Solidity ^0.8.24 |
| 292 | +- Hardhat for development and deployment |
| 293 | +- OpenZeppelin contracts for security |
| 294 | +- Pyth Entropy SDK v2.0.0 for randomness |
| 295 | + |
| 296 | +**Frontend**: |
| 297 | +- Next.js 14 with App Router |
| 298 | +- React 18 |
| 299 | +- Wagmi v2 for Ethereum interactions |
| 300 | +- Viem for contract interactions |
| 301 | +- TanStack React Query for state management |
| 302 | +- Tailwind CSS with shadcn/ui components |
| 303 | + |
| 304 | +### Testing |
| 305 | + |
| 306 | +To test the contract compilation: |
| 307 | + |
| 308 | +```bash |
| 309 | +cd contract |
| 310 | +npx hardhat compile |
| 311 | +``` |
| 312 | + |
| 313 | +To build the frontend: |
| 314 | + |
| 315 | +```bash |
| 316 | +cd app |
| 317 | +npm run build |
| 318 | +``` |
| 319 | + |
| 320 | +### Customization |
| 321 | + |
| 322 | +You can customize the lottery parameters by modifying `ignition/modules/Lottery.ts`: |
| 323 | + |
| 324 | +```typescript |
| 325 | +const TicketPrice = parseEther("0.001"); // Change ticket price |
| 326 | +``` |
| 327 | + |
| 328 | +You can also modify the contract to add features like: |
| 329 | +- Multiple lottery rounds |
| 330 | +- Automatic lottery duration limits |
| 331 | +- Multiple winners or prize distribution |
| 332 | +- Minimum ticket requirements |
| 333 | + |
| 334 | +## Gas Costs |
| 335 | + |
| 336 | +Approximate gas costs on testnets: |
| 337 | + |
| 338 | +- **Deploy Contract**: ~2,000,000 gas |
| 339 | +- **Buy Ticket**: ~150,000 gas (includes Entropy fee) |
| 340 | +- **End Lottery**: ~100,000 gas (includes Entropy fee) |
| 341 | +- **Claim Prize**: ~50,000 gas |
| 342 | + |
| 343 | +Note: Gas costs vary by network and include the Entropy provider fee for randomness generation. |
| 344 | + |
| 345 | +## Troubleshooting |
| 346 | + |
| 347 | +### Common Issues |
| 348 | + |
| 349 | +**"Lottery not active" error**: The lottery has already ended or is in drawing phase. Wait for a new round. |
| 350 | + |
| 351 | +**"Insufficient fee" error**: Make sure you're sending enough ETH to cover both the ticket price and the Entropy fee. Use `getTotalCost()` to check the required amount. |
| 352 | + |
| 353 | +**Random number not appearing**: Entropy callbacks typically complete within a few seconds, but may take longer during network congestion. Check the blockchain explorer for the callback transaction. |
| 354 | + |
| 355 | +**Wallet not connecting**: Ensure your wallet is on the correct network (e.g., Blast Sepolia). The app will prompt you to switch networks if needed. |
| 356 | + |
| 357 | +## Additional Resources |
| 358 | + |
| 359 | +- **Pyth Entropy Documentation**: https://docs.pyth.network/entropy |
| 360 | +- **IEntropyV2 Interface**: https://github.com/pyth-network/pyth-crosschain/blob/main/target_chains/ethereum/entropy_sdk/solidity/IEntropyV2.sol |
| 361 | +- **Pyth Network**: https://pyth.network |
| 362 | +- **Source Repository**: https://github.com/pyth-network/pyth-examples |
| 363 | + |
| 364 | +## License |
| 365 | + |
| 366 | +This example is provided under the MIT License. |
0 commit comments