diff --git a/entropy-sdk-solidity/examples/crypto-roulette-lottery/DEPLOYMENT.md b/entropy-sdk-solidity/examples/crypto-roulette-lottery/DEPLOYMENT.md new file mode 100644 index 0000000..7dd643a --- /dev/null +++ b/entropy-sdk-solidity/examples/crypto-roulette-lottery/DEPLOYMENT.md @@ -0,0 +1,420 @@ +# 🚀 Deployment Guide + +Complete guide for deploying Crypto Roulette & Daily Lottery contracts. + +--- + +## Prerequisites + +### Required Tools + +1. **Foundry** - Ethereum development toolkit + ```bash + curl -L https://foundry.paradigm.xyz | bash + foundryup + ``` + +2. **Git** - Version control + ```bash + git --version + ``` + +3. **Node.js** (optional, for frontend) + ```bash + node --version # Should be >= v20.18.3 + ``` + +### Required Accounts + +1. **Deployer Wallet** with testnet ETH +2. **RPC URL** for target network +3. **Etherscan API Key** (for verification) + +--- + +## Network Configuration + +### Optimism Sepolia (Current Deployment) + +| Parameter | Value | +|-----------|-------| +| **Chain ID** | 11155420 | +| **RPC URL** | https://sepolia.optimism.io | +| **Explorer** | https://sepolia-optimism.etherscan.io | +| **Pyth Entropy** | `0x4821932D0CDd71225A6d914706A621e0389D7061` | +| **Currency** | ETH | + +### Get Testnet ETH + +1. Visit [Optimism Sepolia Faucet](https://www.optimism.io/faucet) +2. Enter your deployer address +3. Request test ETH + +--- + +## Step-by-Step Deployment + +### 1. Set Up Project + +```bash +# Clone the repository +git clone https://github.com/YOUR_USERNAME/pyth-examples.git +cd pyth-examples/entropy-sdk-solidity/examples/crypto-roulette-lottery + +# Install dependencies (if using Foundry project) +forge install +``` + +### 2. Configure Environment + +Create a `.env` file: + +```bash +# Network Configuration +RPC_URL=https://sepolia.optimism.io +CHAIN_ID=11155420 + +# Etherscan (for verification) +ETHERSCAN_API_KEY=your_etherscan_api_key_here + +# Deployment Parameters +ENTROPY_ADDRESS=0x4821932D0CDd71225A6d914706A621e0389D7061 +TICKET_PRICE=1000000000000000 # 0.001 ETH in wei +``` + +### 3. Set Up Deployer Account + +**Option A: Using Foundry Keystore (Recommended)** + +```bash +# Create encrypted keystore +cast wallet import deployer --interactive + +# You'll be prompted for: +# 1. Your private key +# 2. A password to encrypt it + +# Your keystore is now saved securely +``` + +**Option B: Using Private Key (Less Secure)** + +```bash +# Add to .env file +PRIVATE_KEY=your_private_key_here +``` + +### 4. Verify Pyth Entropy Address + +Confirm Pyth Entropy is deployed on your target network: + +```bash +# Check if contract exists +cast code 0x4821932D0CDd71225A6d914706A621e0389D7061 \ + --rpc-url https://sepolia.optimism.io + +# Should return bytecode if deployed +``` + +**Pyth Entropy Addresses by Network:** + +| Network | Address | +|---------|---------| +| Optimism Sepolia | `0x4821932D0CDd71225A6d914706A621e0389D7061` | +| Optimism Mainnet | `0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF` | +| Ethereum Sepolia | `0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c` | + +For other networks, check [Pyth Documentation](https://docs.pyth.network/entropy/contract-addresses). + +### 5. Deploy Contracts + +**Using the Deployment Script:** + +```bash +# Deploy to Optimism Sepolia +forge script script/DeployCryptoRoulette.s.sol \ + --rpc-url $RPC_URL \ + --account deployer \ + --broadcast \ + --verify + +# You'll be prompted for your keystore password +``` + +**Manual Deployment (Alternative):** + +```bash +# 1. Deploy DailyLottery +forge create contracts/DailyLottery.sol:DailyLottery \ + --rpc-url $RPC_URL \ + --account deployer \ + --constructor-args $ENTROPY_ADDRESS 0x0000000000000000000000000000000000000000 + +# Note the deployed address: LOTTERY_ADDRESS + +# 2. Deploy CryptoRoulette +forge create contracts/CryptoRoulette.sol:CryptoRoulette \ + --rpc-url $RPC_URL \ + --account deployer \ + --constructor-args $ENTROPY_ADDRESS $LOTTERY_ADDRESS $TICKET_PRICE + +# Note the deployed address: ROULETTE_ADDRESS + +# 3. Link contracts +cast send $LOTTERY_ADDRESS \ + "setRouletteContract(address)" $ROULETTE_ADDRESS \ + --rpc-url $RPC_URL \ + --account deployer +``` + +### 6. Verify Contracts on Etherscan + +**CryptoRoulette:** + +```bash +forge verify-contract $ROULETTE_ADDRESS \ + contracts/CryptoRoulette.sol:CryptoRoulette \ + --chain optimism-sepolia \ + --constructor-args $(cast abi-encode "constructor(address,address,uint256)" \ + $ENTROPY_ADDRESS \ + $LOTTERY_ADDRESS \ + $TICKET_PRICE) \ + --etherscan-api-key $ETHERSCAN_API_KEY +``` + +**DailyLottery:** + +```bash +forge verify-contract $LOTTERY_ADDRESS \ + contracts/DailyLottery.sol:DailyLottery \ + --chain optimism-sepolia \ + --constructor-args $(cast abi-encode "constructor(address,address)" \ + $ENTROPY_ADDRESS \ + $ROULETTE_ADDRESS) \ + --etherscan-api-key $ETHERSCAN_API_KEY +``` + +--- + +## Post-Deployment Verification + +### 1. Check Contract State + +```bash +# Get current day +cast call $ROULETTE_ADDRESS "getCurrentDay()(uint256)" \ + --rpc-url $RPC_URL + +# Get ticket price +cast call $ROULETTE_ADDRESS "ticketPrice()(uint256)" \ + --rpc-url $RPC_URL + +# Get total spin cost (entropy fee + ticket) +cast call $ROULETTE_ADDRESS "getTotalSpinCost()(uint256)" \ + --rpc-url $RPC_URL + +# Get lottery contract address +cast call $ROULETTE_ADDRESS "lotteryContract()(address)" \ + --rpc-url $RPC_URL + +# Get roulette contract address in lottery +cast call $LOTTERY_ADDRESS "rouletteContract()(address)" \ + --rpc-url $RPC_URL +``` + +### 2. Verify Pyth Entropy Fee + +```bash +# Get current Pyth Entropy fee +cast call $ENTROPY_ADDRESS "getFeeV2()(uint128)" \ + --rpc-url $RPC_URL + +# Should return fee in wei (typically ~0.0001 ETH = 100000000000000) +``` + +### 3. Test Functionality + +**Test a Roulette Spin:** + +```bash +# Get total cost +TOTAL_COST=$(cast call $ROULETTE_ADDRESS "getTotalSpinCost()(uint256)" --rpc-url $RPC_URL) + +# Spin roulette (guess = 0 for BTC) +cast send $ROULETTE_ADDRESS \ + "spinRoulette(uint8)" 0 \ + --value $TOTAL_COST \ + --rpc-url $RPC_URL \ + --account deployer + +# Wait ~10-30 seconds for Pyth callback +# Check events on Etherscan to see result +``` + +--- + +## Deployment Costs + +Approximate gas costs on Optimism Sepolia: + +| Action | Gas Used | Cost (at 0.001 Gwei) | +|--------|----------|----------------------| +| Deploy DailyLottery | ~2,500,000 | ~$0.005 | +| Deploy CryptoRoulette | ~3,000,000 | ~$0.006 | +| setRouletteContract | ~50,000 | ~$0.0001 | +| **Total Deployment** | **~5,550,000** | **~$0.011** | + +Plus: +- Verification: Free +- Test spin: ~150,000 gas + Entropy fee + +--- + +## Configuration Options + +### Adjust Ticket Price + +To change the ticket price after deployment: + +**Note:** Ticket price is immutable. To change it, you must redeploy. + +When redeploying, modify `DeployCryptoRoulette.s.sol`: + +```solidity +uint256 ticketPrice = 0.002 ether; // Change this value +``` + +### Change Network + +To deploy on a different network: + +1. **Update Entropy Address** in deployment script +2. **Update RPC URL** in environment +3. **Get testnet/mainnet currency** +4. **Update verification chain** + +Example for Ethereum Sepolia: + +```bash +# .env +RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY +ENTROPY_ADDRESS=0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c + +# Deployment +forge script script/DeployCryptoRoulette.s.sol \ + --rpc-url $RPC_URL \ + --account deployer \ + --broadcast + +# Verification +forge verify-contract $ADDRESS \ + contracts/CryptoRoulette.sol:CryptoRoulette \ + --chain sepolia \ + --constructor-args $ARGS +``` + +--- + +## Troubleshooting + +### Issue: "Insufficient funds for gas" + +**Solution:** Ensure deployer account has enough ETH for gas + deployment + +```bash +# Check balance +cast balance $YOUR_ADDRESS --rpc-url $RPC_URL + +# Get ETH from faucet if needed +``` + +### Issue: "Invalid entropy address" + +**Solution:** Verify Pyth Entropy is deployed on your network + +```bash +# Check if contract exists +cast code $ENTROPY_ADDRESS --rpc-url $RPC_URL + +# If returns 0x, Pyth Entropy is not deployed on this network +``` + +### Issue: "Verification failed" + +**Solution:** Ensure constructor args match exactly + +```bash +# Get constructor args from deployment logs +# Encode them properly with cast abi-encode + +cast abi-encode "constructor(address,address,uint256)" \ + 0x4821... \ + 0x5149... \ + 1000000000000000 +``` + +### Issue: "Pyth callback not arriving" + +**Possible causes:** +1. Pyth providers not active on testnet +2. Insufficient entropy fee sent +3. Network congestion + +**Solution:** +```bash +# Check if entropy fee was sent correctly +# View transaction on explorer +# Wait up to 60 seconds for callback + +# If still failing, check Pyth status: +# https://status.pyth.network +``` + +--- + +## Production Deployment Checklist + +Before deploying to mainnet: + +- [ ] **Audit contracts** (get professional audit) +- [ ] **Test extensively** on testnet +- [ ] **Verify Pyth Entropy** address for mainnet +- [ ] **Set appropriate** ticket price +- [ ] **Fund deployer** account with sufficient ETH +- [ ] **Backup private key** securely +- [ ] **Prepare emergency** procedures +- [ ] **Set up monitoring** for events +- [ ] **Document contract** addresses +- [ ] **Verify contracts** on Etherscan +- [ ] **Test with small** amounts first + +--- + +## Deployed Contracts (Optimism Sepolia) + +Current production deployment: + +- **CryptoRoulette**: [`0x19aab2239911164c9051ccaed184102a10d7121f`](https://sepolia-optimism.etherscan.io/address/0x19aab2239911164c9051ccaed184102a10d7121f) +- **DailyLottery**: [`0x5149cc9f6c3a4b60cfa84125161e96b0cf677eb4`](https://sepolia-optimism.etherscan.io/address/0x5149cc9f6c3a4b60cfa84125161e96b0cf677eb4) +- **Pyth Entropy**: [`0x4821932D0CDd71225A6d914706A621e0389D7061`](https://sepolia-optimism.etherscan.io/address/0x4821932D0CDd71225A6d914706A621e0389D7061) + +Deployed: November 23, 2024 +Block: 36038675 +Ticket Price: 0.001 ETH + +--- + +## Support + +For deployment issues: + +- **Pyth Entropy Docs**: https://docs.pyth.network/entropy +- **Foundry Book**: https://book.getfoundry.sh +- **Optimism Docs**: https://docs.optimism.io + +--- + +## License + +MIT License - See main README for details + diff --git a/entropy-sdk-solidity/examples/crypto-roulette-lottery/README.md b/entropy-sdk-solidity/examples/crypto-roulette-lottery/README.md new file mode 100644 index 0000000..057a8f2 --- /dev/null +++ b/entropy-sdk-solidity/examples/crypto-roulette-lottery/README.md @@ -0,0 +1,564 @@ +# 🎰 Crypto Roulette & Daily Lottery + +> **Pyth Entropy Pool Prize Submission - ETH Global Buenos Aires 2024** + +**Verifiable On-Chain Randomness Powered by Pyth Entropy V2** + +![Solidity](https://img.shields.io/badge/Solidity-0.8.13-363636?style=flat&logo=solidity) +![Optimism Sepolia](https://img.shields.io/badge/Network-Optimism%20Sepolia-red?style=flat) +![Foundry](https://img.shields.io/badge/Built%20with-Foundry-orange?style=flat) + +--- + +## 🎯 Pyth Entropy Pool Prize Submission + +**Project:** Crypto Roulette & Daily Lottery +**Network:** Optimism Sepolia (Chain ID: 11155420) +**Status:** ✅ Deployed, Verified, and Fully Functional + +**Deployed Contracts:** +- **CryptoRoulette**: [`0x19aab2239911164c9051ccaed184102a10d7121f`](https://sepolia-optimism.etherscan.io/address/0x19aab2239911164c9051ccaed184102a10d7121f) +- **DailyLottery**: [`0x5149cc9f6c3a4b60cfa84125161e96b0cf677eb4`](https://sepolia-optimism.etherscan.io/address/0x5149cc9f6c3a4b60cfa84125161e96b0cf677eb4) +- **Pyth Entropy**: [`0x4821932D0CDd71225A6d914706A621e0389D7061`](https://sepolia-optimism.etherscan.io/address/0x4821932D0CDd71225A6d914706A621e0389D7061) + +**Innovation Highlight:** This project demonstrates the versatility of Pyth Entropy V2 by implementing **TWO distinct randomness consumption patterns** in a single interconnected gaming ecosystem: high-frequency roulette spins and periodic lottery draws. + +--- + +## 📖 Project Overview + +**Crypto Roulette & Daily Lottery** is an innovative decentralized gaming platform that combines two games powered entirely by Pyth Entropy V2 for verifiable on-chain randomness. + +### The Games + +1. **🎲 Crypto Roulette**: Players guess which cryptocurrency (BTC, ETH, SOL, AVAX, or DOGE) will be randomly selected + - 20% win probability (1 in 5 chance) + - Winners automatically qualify for the daily lottery + - All ticket prices fund the daily lottery pool + +2. **🎁 Daily Lottery**: Exclusive lottery where only roulette winners can participate + - Winner-takes-all daily prize pool + - Random winner selected from whitelist of roulette winners + - Owner-triggered draws at end of each day + +### Why This Is Innovative + +Unlike traditional single-game implementations, this project creates an **interconnected gaming economy**: +- Two different smart contracts sharing randomness infrastructure +- Two distinct use cases for Pyth Entropy (continuous + periodic) +- Sustainable game loop: losers fund the pool, winners compete for prizes +- Real-world gaming mechanics powered by verifiable randomness + +--- + +## 🔮 Pyth Entropy V2 Integration + +### Why Pyth Entropy? + +We chose Pyth Entropy V2 for its: +- **Verifiable Randomness**: Cryptographically secure and unpredictable +- **On-Chain Native**: Numbers generated and verified entirely on-chain +- **Permissionless**: Anyone can request random numbers +- **Low Latency**: Fast callback mechanism (~5-30 seconds) +- **Battle-Tested**: Audited and used by major DeFi protocols + +### Implementation Pattern: Two-Step Randomness + +Both contracts follow Pyth's secure two-step pattern: + +#### Pattern Flow: +``` +1. Request Phase: Contract calls entropy.requestV2() → Emits event +2. Callback Phase: Pyth calls entropyCallback() with verified random number +``` + +This prevents: +- ❌ Front-running attacks +- ❌ Result manipulation +- ❌ Transaction reversion exploits +- ❌ Block hash prediction + +--- + +## 💡 Innovation: Dual Randomness Patterns + +### Pattern 1: High-Frequency Randomness (CryptoRoulette) + +**Use Case:** Instant gameplay with continuous player interactions + +**Implementation:** + +```solidity +// Player initiates spin +function spinRoulette(Asset _guess) external payable { + uint128 entropyFee = entropy.getFeeV2(); + require(msg.value >= entropyFee + ticketPrice, "Insufficient payment"); + + // Request random number from Pyth Entropy + uint64 sequenceNumber = entropy.requestV2{ value: entropyFee }(); + + // Store spin request + spins[sequenceNumber] = SpinRequest({ + player: msg.sender, + guessedAsset: _guess, + day: currentDay, + fulfilled: false, + resultAsset: Asset.BTC, + won: false + }); + + emit SpinRequested(msg.sender, sequenceNumber, _guess, currentDay); +} + +// Pyth Entropy calls back with random result +function entropyCallback( + uint64 sequenceNumber, + address, + bytes32 randomNumber +) internal override { + SpinRequest storage spin = spins[sequenceNumber]; + require(!spin.fulfilled, "Already fulfilled"); + + // Use random number to determine outcome (0-4 for 5 assets) + uint256 randomIndex = uint256(randomNumber) % 5; + Asset resultAsset = Asset(randomIndex); + + bool won = (resultAsset == spin.guessedAsset); + + spin.resultAsset = resultAsset; + spin.won = won; + spin.fulfilled = true; + + // Winner joins lottery whitelist + if (won) { + lotteryContract.addToWhitelist(spin.player, spin.day); + } + + // Ticket price always goes to lottery pool + lotteryContract.addToPool{ value: ticketPrice }(spin.day); + + emit SpinCompleted(sequenceNumber, spin.player, resultAsset, won); +} +``` + +**Key Features:** +- Continuous, player-initiated randomness +- Each spin is independent +- Real-time gameplay experience +- Demonstrates Pyth Entropy for gaming + +--- + +### Pattern 2: Periodic Randomness (DailyLottery) + +**Use Case:** Scheduled, high-stakes winner selection + +**Implementation:** + +```solidity +// Owner triggers daily draw +function startDailyDraw(uint256 day) external payable onlyOwner { + require(!dayCompleted[day], "Day already completed"); + require(dailyWhitelist[day].length > 0, "Whitelist empty"); + require(dailyPool[day] > 0, "Pool empty"); + + // Request random number for winner selection + uint128 entropyFee = entropy.getFeeV2(); + require(msg.value >= entropyFee, "Insufficient entropy fee"); + + uint64 sequenceNumber = entropy.requestV2{ value: entropyFee }(); + + draws[sequenceNumber] = DrawRequest({ + day: day, + fulfilled: false, + winner: address(0), + prize: dailyPool[day] + }); + + emit DrawRequested( + day, + sequenceNumber, + dailyWhitelist[day].length, + dailyPool[day] + ); +} + +// Pyth Entropy calls back to select winner +function entropyCallback( + uint64 sequenceNumber, + address, + bytes32 randomNumber +) internal override { + DrawRequest storage draw = draws[sequenceNumber]; + require(!draw.fulfilled, "Already fulfilled"); + + address[] storage whitelist = dailyWhitelist[draw.day]; + + // Use random number to pick winner from whitelist + uint256 winnerIndex = uint256(randomNumber) % whitelist.length; + address winner = whitelist[winnerIndex]; + + // Transfer prize to winner + (bool success, ) = winner.call{ value: draw.prize }(""); + require(success, "Transfer failed"); + + draw.winner = winner; + draw.fulfilled = true; + dayCompleted[draw.day] = true; + + emit WinnerSelected(draw.day, winner, draw.prize); +} +``` + +**Key Features:** +- Periodic, admin-triggered randomness +- High-stakes winner selection +- Fair distribution from whitelist +- Demonstrates Pyth Entropy for lotteries + +--- + +## 🏗️ Architecture + +### System Flow + +``` +┌─────────────────────────────────────────────────────────────┐ +│ PLAYER │ +└────────────┬────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 1. PLAY ROULETTE │ +│ • Select crypto asset (BTC/ETH/SOL/AVAX/DOGE) │ +│ • Pay: Entropy Fee + Ticket Price │ +└────────────┬────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ CRYPTOROULETTE CONTRACT │ +│ • Calls: entropy.requestV2() │ +│ • Stores: SpinRequest with player guess │ +└────────────┬────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ PYTH ENTROPY V2 │ +│ • Generates secure random number │ +│ • Calls back: entropyCallback() │ +└────────────┬────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 2. ROULETTE RESULT DETERMINED │ +│ • Random % 5 = Winning asset │ +│ • If WIN: Add to lottery whitelist │ +│ • Always: Add ticket to lottery pool │ +└────────────┬────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ DAILYLOTTERY CONTRACT │ +│ • Accumulates prize pool from tickets │ +│ • Maintains whitelist of roulette winners │ +└────────────┬────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 3. DAILY DRAW (Owner Triggered) │ +│ • Owner calls: startDailyDraw() │ +│ • Calls: entropy.requestV2() │ +└────────────┬────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ PYTH ENTROPY V2 │ +│ • Generates secure random number │ +│ • Calls back: entropyCallback() │ +└────────────┬────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 4. LOTTERY WINNER SELECTED │ +│ • Random % whitelist.length = Winner index │ +│ • Transfer entire pool to winner │ +│ • Mark day as completed │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 📜 Smart Contracts + +### CryptoRoulette.sol + +**Purpose:** Manages roulette game with 5 crypto assets + +**Key Functions:** +- `spinRoulette(Asset _guess)`: Player initiates spin +- `entropyCallback()`: Receives random result from Pyth +- `getEntropyFee()`: Returns current Pyth fee +- `getTotalSpinCost()`: Returns total cost (entropy + ticket) + +**Events:** +- `SpinRequested(address player, uint64 sequenceNumber, Asset guess, uint256 day)` +- `SpinCompleted(uint64 sequenceNumber, address player, Asset result, bool won)` + +### DailyLottery.sol + +**Purpose:** Manages daily lottery with whitelist-based entry + +**Key Functions:** +- `addToWhitelist(address player, uint256 day)`: Adds roulette winner (roulette-only) +- `addToPool(uint256 day)`: Adds funds to prize pool (roulette-only) +- `startDailyDraw(uint256 day)`: Initiates random winner selection (owner-only) +- `entropyCallback()`: Receives random result and selects winner +- `emergencyWithdraw(uint256 day)`: Emergency fund recovery (owner-only) + +**Events:** +- `PlayerWhitelisted(address player, uint256 day)` +- `PoolIncreased(uint256 day, uint256 amount, uint256 newTotal)` +- `DrawRequested(uint256 day, uint64 sequenceNumber, uint256 whitelistSize, uint256 poolAmount)` +- `WinnerSelected(uint256 day, address winner, uint256 prize)` + +### IDailyLottery.sol + +**Purpose:** Interface for cross-contract communication + +--- + +## 🚀 Deployment + +### Prerequisites + +- Foundry installed +- Optimism Sepolia ETH for deployment +- Keystore account configured + +### Deploy Script + +The `DeployCryptoRoulette.s.sol` script deploys both contracts and links them: + +```solidity +function run() external ScaffoldEthDeployerRunner { + // Pyth Entropy on Optimism Sepolia + address entropyAddress = 0x4821932D0CDd71225A6d914706A621e0389D7061; + uint256 ticketPrice = 0.001 ether; + + // Deploy DailyLottery + DailyLottery lottery = new DailyLottery(entropyAddress, address(0)); + + // Deploy CryptoRoulette + CryptoRoulette roulette = new CryptoRoulette( + entropyAddress, + address(lottery), + ticketPrice + ); + + // Link contracts + lottery.setRouletteContract(address(roulette)); +} +``` + +### Deployment Commands + +```bash +# Deploy to Optimism Sepolia +forge script script/DeployCryptoRoulette.s.sol \ + --rpc-url https://sepolia.optimism.io \ + --account deployer \ + --broadcast \ + --verify + +# Verify contracts +forge verify-contract \ + src/CryptoRoulette.sol:CryptoRoulette \ + --chain optimism-sepolia \ + --constructor-args $(cast abi-encode "constructor(address,address,uint256)" \ + 0x4821932D0CDd71225A6d914706A621e0389D7061 \ + \ + 1000000000000000) +``` + +--- + +## 🛠️ Tech Stack + +- **Solidity**: v0.8.13 +- **Pyth Entropy SDK**: V2 +- **Foundry**: Development & deployment framework +- **Scaffold-ETH 2**: Full-stack dApp framework +- **Next.js**: React framework for frontend +- **RainbowKit**: Wallet connection +- **Wagmi**: Ethereum React hooks +- **Optimism Sepolia**: L2 testnet deployment + +--- + +## ✨ Key Features + +### 🎯 Provably Fair Randomness +Every outcome is determined by Pyth Entropy's verifiable random numbers. No one can predict or manipulate results. + +### 🔗 Interconnected Game Economy +Roulette and lottery work together: ticket prices fund the pool, only winners can win the lottery. + +### 🔍 Transparent On-Chain Results +All spins, results, and lottery draws are recorded with events. Anyone can verify fairness. + +### 💰 Winner-Takes-All Mechanism +Entire daily pool goes to one lucky winner, creating exciting high-stakes gameplay. + +### 🛡️ Security First +- Owner-controlled lottery draws +- Emergency withdrawal functions +- Reentrancy protection +- Day completion tracking + +--- + +## 🎮 How It Works + +### For Players + +1. **Connect wallet** to the dApp +2. **Play roulette**: Choose an asset and spin +3. **Wait for result**: Pyth Entropy determines outcome (~5-30 seconds) +4. **If you win**: Automatically added to today's lottery +5. **Daily draw**: Owner triggers at end of day +6. **Winner selected**: Pyth Entropy randomly picks one winner +7. **Prize transferred**: Winner receives entire pool automatically + +### For Operators + +1. **Monitor daily pool**: Check accumulated prizes +2. **Trigger draw**: Call `startDailyDraw(day)` at end of day +3. **Pay entropy fee**: Include Pyth's fee with transaction +4. **Winner selected**: Pyth Entropy handles selection +5. **Funds distributed**: Winner receives prize automatically + +--- + +## 📊 Innovation Highlights + +### 🌟 Dual Randomness Patterns + +Most examples show **one** use case for Pyth Entropy. We demonstrate **two distinct patterns**: +1. High-frequency, player-initiated (roulette) +2. Periodic, admin-triggered (lottery) + +This showcases Pyth Entropy's versatility across different gaming mechanics. + +### 🎰 Interconnected Economy + +Unlike standalone games, we've created a **sustainable gaming ecosystem**: +- Roulette losers contribute to lottery pool +- Only skilled/lucky players compete for big prizes +- Creates sustained engagement and excitement + +### 🏆 Production-Ready + +This isn't just a proof-of-concept: +- ✅ Deployed and verified on Optimism Sepolia +- ✅ Full frontend with Scaffold-ETH 2 +- ✅ Comprehensive event logging +- ✅ Emergency functions for edge cases +- ✅ Real-world game mechanics + +--- + +## 🔒 Security Considerations + +### Randomness Security +- Pyth Entropy V2 provides cryptographically secure randomness +- Two-step pattern prevents manipulation +- Results cannot be predicted before commitment + +### Access Control +- Owner-only: `startDailyDraw`, `emergencyWithdraw`, `setRouletteContract` +- Contract-only: `addToWhitelist`, `addToPool` (only roulette can call) +- No user funds controllable by owner + +### State Management +- Day completion tracking prevents double draws +- Sequence number mapping prevents replay attacks +- Fulfilled flags prevent double processing + +### Emergency Functions +- `emergencyWithdraw`: Allows recovery of unclaimed prizes +- Only callable for completed days +- Prevents permanent fund locking + +--- + +## 📁 Repository Structure + +``` +crypto-roulette-lottery/ +├── README.md (this file) +├── DEPLOYMENT.md (deployment guide) +├── contracts/ +│ ├── CryptoRoulette.sol (roulette game contract) +│ ├── DailyLottery.sol (lottery contract) +│ └── interfaces/ +│ └── IDailyLottery.sol (interface) +└── script/ + └── DeployCryptoRoulette.s.sol (deployment script) +``` + +--- + +## 🔗 Links + +### Deployed Contracts +- [CryptoRoulette on Etherscan](https://sepolia-optimism.etherscan.io/address/0x19aab2239911164c9051ccaed184102a10d7121f) +- [DailyLottery on Etherscan](https://sepolia-optimism.etherscan.io/address/0x5149cc9f6c3a4b60cfa84125161e96b0cf677eb4) + +### Documentation +- [Pyth Entropy Documentation](https://docs.pyth.network/entropy) +- [Scaffold-ETH 2 Docs](https://docs.scaffoldeth.io) +- [Foundry Book](https://book.getfoundry.sh) + +### Project Links +- **Full Repository**: [Add your repo URL] +- **Live Demo**: [Add your demo URL] +- **Video Demo**: [Add if available] + +--- + +## 🏆 Built For + +**ETH Global Buenos Aires Hackathon 2024** +- Track: DeFi / Gaming +- Prize: Pyth Entropy Pool Prize ($5,000) + +This project qualifies for the Pyth Entropy Pool Prize by: +- ✅ Using Pyth Entropy to generate random numbers on-chain +- ✅ Consuming random numbers in smart contracts +- ✅ Demonstrating innovative use of Pyth Entropy V2 +- ✅ Production-ready deployment on Optimism Sepolia +- ✅ Comprehensive documentation and code + +--- + +## 📄 License + +MIT License - See LICENSE file for details + +--- + +## 🙏 Acknowledgments + +- **Pyth Network** for providing industry-leading on-chain randomness +- **ETH Global** for hosting an incredible hackathon +- **Optimism** for providing a fast, low-cost L2 environment +- **Scaffold-ETH 2** team for the amazing dApp framework + +--- + +
+ +**⚡ Powered by Pyth Entropy V2 | Built with ❤️ for ETH Global Buenos Aires ⚡** + +
+ diff --git a/entropy-sdk-solidity/examples/crypto-roulette-lottery/contracts/CryptoRoulette.sol b/entropy-sdk-solidity/examples/crypto-roulette-lottery/contracts/CryptoRoulette.sol new file mode 100644 index 0000000..05b8366 --- /dev/null +++ b/entropy-sdk-solidity/examples/crypto-roulette-lottery/contracts/CryptoRoulette.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "@pythnetwork/entropy-sdk-solidity/IEntropyV2.sol"; +import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol"; +import "./interfaces/IDailyLottery.sol"; + +/** + * @title CryptoRoulette + * @notice A crypto roulette game that uses Pyth Entropy for verifiable randomness + * @dev Players spin the roulette by guessing one of 5 crypto assets + */ +contract CryptoRoulette is IEntropyConsumer { + + // ============ Enums ============ + + enum Asset { BTC, ETH, SOL, AVAX, DOGE } + + // ============ Structs ============ + + struct SpinRequest { + address player; + Asset guessedAsset; + uint256 day; + bool fulfilled; + Asset resultAsset; + bool won; + } + + // ============ State Variables ============ + + IEntropyV2 public immutable entropy; + IDailyLottery public immutable lotteryContract; + + address public owner; + uint256 public ticketPrice; + uint256 public currentDay; + + // Mapping from Entropy sequence number to spin request + mapping(uint64 => SpinRequest) public spins; + + // ============ Events ============ + + event SpinRequested( + address indexed player, + uint64 indexed sequenceNumber, + Asset guess, + uint256 day + ); + + event SpinCompleted( + uint64 indexed sequenceNumber, + Asset result, + bool won, + address indexed player + ); + + event DayAdvanced(uint256 newDay); + event TicketPriceUpdated(uint256 newPrice); + + // ============ Errors ============ + + error NotEnoughFees(); + error Unauthorized(); + error TransferFailed(); + + // ============ Modifiers ============ + + modifier onlyOwner() { + if (msg.sender != owner) revert Unauthorized(); + _; + } + + // ============ Constructor ============ + + /** + * @notice Initializes the CryptoRoulette contract + * @param _entropy Address of the Entropy contract + * @param _lottery Address of the DailyLottery contract + * @param _ticketPrice Price of each ticket (goes to lottery pool) + */ + constructor( + address _entropy, + address _lottery, + uint256 _ticketPrice + ) { + entropy = IEntropyV2(_entropy); + lotteryContract = IDailyLottery(_lottery); + ticketPrice = _ticketPrice; + owner = msg.sender; + currentDay = 1; // Start at day 1 + } + + // ============ External Functions ============ + + /** + * @notice Spin the roulette with a guess + * @param guess The asset the player is guessing will be selected + */ + function spinRoulette(Asset guess) external payable { + // Get the required Entropy fee + uint128 entropyFee = entropy.getFeeV2(); + + // Validate payment covers both entropy fee and ticket price + if (msg.value < entropyFee + ticketPrice) revert NotEnoughFees(); + + // Request random number from Entropy + uint64 sequenceNumber = entropy.requestV2{ value: entropyFee }(); + + // Store the spin request + spins[sequenceNumber] = SpinRequest({ + player: msg.sender, + guessedAsset: guess, + day: currentDay, + fulfilled: false, + resultAsset: Asset.BTC, // Placeholder, will be set in callback + won: false + }); + + emit SpinRequested(msg.sender, sequenceNumber, guess, currentDay); + } + + /** + * @notice Advances to the next day + * @dev Only callable by owner, typically at the end of each day + */ + function advanceDay() external onlyOwner { + currentDay++; + emit DayAdvanced(currentDay); + } + + /** + * @notice Updates the ticket price + * @param newPrice The new ticket price + */ + function setTicketPrice(uint256 newPrice) external onlyOwner { + ticketPrice = newPrice; + emit TicketPriceUpdated(newPrice); + } + + /** + * @notice Withdraws accumulated entropy fees + * @dev Withdraws any ETH balance beyond what's needed for pending tickets + */ + function withdrawFees() external onlyOwner { + uint256 balance = address(this).balance; + (bool success, ) = owner.call{value: balance}(""); + if (!success) revert TransferFailed(); + } + + /** + * @notice Transfers ownership to a new address + * @param newOwner The new owner address + */ + function transferOwnership(address newOwner) external onlyOwner { + owner = newOwner; + } + + // ============ View Functions ============ + + /** + * @notice Returns the Entropy contract address + * @return Address of the Entropy contract + */ + function getEntropy() internal view override returns (address) { + return address(entropy); + } + + /** + * @notice Gets details of a specific spin + * @param sequenceNumber The sequence number of the spin + * @return The SpinRequest struct + */ + function getSpinDetails(uint64 sequenceNumber) external view returns (SpinRequest memory) { + return spins[sequenceNumber]; + } + + /** + * @notice Gets the current day number + * @return The current day + */ + function getCurrentDay() external view returns (uint256) { + return currentDay; + } + + /** + * @notice Calculates the total cost to spin (entropy fee + ticket price) + * @return The total cost in wei + */ + function getTotalSpinCost() external view returns (uint256) { + return uint256(entropy.getFeeV2()) + ticketPrice; + } + + // ============ Internal Functions ============ + + /** + * @notice Callback function called by Entropy with the random number + * @param sequenceNumber The sequence number of the request + * @param randomNumber The random number provided by Entropy + */ + function entropyCallback( + uint64 sequenceNumber, + address, // provider address, not used + bytes32 randomNumber + ) internal override { + SpinRequest storage spin = spins[sequenceNumber]; + + // Determine the winning asset (0-4 maps to BTC, ETH, SOL, AVAX, DOGE) + uint256 randomIndex = uint256(randomNumber) % 5; + Asset resultAsset = Asset(randomIndex); + + // Check if player won + bool won = (resultAsset == spin.guessedAsset); + + // Update the spin request + spin.fulfilled = true; + spin.resultAsset = resultAsset; + spin.won = won; + + // ALWAYS add ticket price to the pool (regardless of win/loss) + lotteryContract.addToPool{value: ticketPrice}(spin.day); + + // ONLY add to whitelist if player won + if (won) { + lotteryContract.addToWhitelist(spin.player, spin.day); + } + + emit SpinCompleted(sequenceNumber, resultAsset, won, spin.player); + } + + // ============ Receive Function ============ + + receive() external payable {} +} + diff --git a/entropy-sdk-solidity/examples/crypto-roulette-lottery/contracts/DailyLottery.sol b/entropy-sdk-solidity/examples/crypto-roulette-lottery/contracts/DailyLottery.sol new file mode 100644 index 0000000..dfa4be1 --- /dev/null +++ b/entropy-sdk-solidity/examples/crypto-roulette-lottery/contracts/DailyLottery.sol @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "@pythnetwork/entropy-sdk-solidity/IEntropyV2.sol"; +import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol"; + +/** + * @title DailyLottery + * @notice Manages daily lottery draws using Pyth Entropy for random winner selection + * @dev Only whitelisted players (those who won the roulette) can win the lottery + */ +contract DailyLottery is IEntropyConsumer { + + // ============ Structs ============ + + struct DrawRequest { + uint256 day; + bool fulfilled; + address winner; + uint256 prize; + } + + // ============ State Variables ============ + + IEntropyV2 public immutable entropy; + address public rouletteContract; + address public owner; + bool private rouletteSet; + + // Mappings for daily lottery data + mapping(uint256 => address[]) public dailyWhitelist; + mapping(uint256 => uint256) public dailyPool; + mapping(uint64 => DrawRequest) public draws; + mapping(uint256 => bool) public dayCompleted; + + // ============ Events ============ + + event PlayerWhitelisted(address indexed player, uint256 indexed day); + + event PoolIncreased( + uint256 indexed day, + uint256 amount, + uint256 newTotal + ); + + event DrawRequested( + uint256 indexed day, + uint64 indexed sequenceNumber, + uint256 whitelistSize, + uint256 poolAmount + ); + + event WinnerSelected( + uint256 indexed day, + address indexed winner, + uint256 prize + ); + + // ============ Errors ============ + + error Unauthorized(); + error PoolEmpty(); + error WhitelistEmpty(); + error DayAlreadyCompleted(); + error TransferFailed(); + + // ============ Modifiers ============ + + modifier onlyOwner() { + if (msg.sender != owner) revert Unauthorized(); + _; + } + + modifier onlyRoulette() { + if (msg.sender != rouletteContract) revert Unauthorized(); + _; + } + + // ============ Constructor ============ + + /** + * @notice Initializes the DailyLottery contract + * @param _entropy Address of the Entropy contract + * @param _rouletteContract Address of the CryptoRoulette contract + */ + constructor(address _entropy, address _rouletteContract) { + entropy = IEntropyV2(_entropy); + rouletteContract = _rouletteContract; + owner = msg.sender; + } + + // ============ External Functions ============ + + /** + * @notice Adds a player to the whitelist for a specific day + * @dev Only callable by the roulette contract + * @param player Address of the player who won the roulette + * @param day The day number for which to add the player + */ + function addToWhitelist(address player, uint256 day) external onlyRoulette { + // Add player to whitelist (duplicates allowed intentionally) + dailyWhitelist[day].push(player); + + emit PlayerWhitelisted(player, day); + } + + /** + * @notice Adds funds to the daily pool + * @dev Only callable by the roulette contract + * @param day The day number for which to add to the pool + */ + function addToPool(uint256 day) external payable onlyRoulette { + dailyPool[day] += msg.value; + + emit PoolIncreased(day, msg.value, dailyPool[day]); + } + + /** + * @notice Starts the daily lottery draw for a specific day + * @dev Only callable by owner, requests random number from Entropy + * @param day The day number for which to start the draw + */ + function startDailyDraw(uint256 day) external payable onlyOwner { + // Validations + if (dayCompleted[day]) revert DayAlreadyCompleted(); + if (dailyPool[day] == 0) revert PoolEmpty(); + if (dailyWhitelist[day].length == 0) revert WhitelistEmpty(); + + // Get the required Entropy fee + uint128 entropyFee = entropy.getFeeV2(); + if (msg.value < entropyFee) revert Unauthorized(); // Reusing error for simplicity + + // Request random number from Entropy + uint64 sequenceNumber = entropy.requestV2{ value: entropyFee }(); + + // Store the draw request + draws[sequenceNumber] = DrawRequest({ + day: day, + fulfilled: false, + winner: address(0), + prize: dailyPool[day] + }); + + emit DrawRequested( + day, + sequenceNumber, + dailyWhitelist[day].length, + dailyPool[day] + ); + } + + /** + * @notice Emergency withdrawal function in case of issues + * @dev Only callable by owner + * @param day The day for which to withdraw the pool + */ + function emergencyWithdraw(uint256 day) external onlyOwner { + if (dayCompleted[day]) revert DayAlreadyCompleted(); + + uint256 amount = dailyPool[day]; + dailyPool[day] = 0; + + (bool success, ) = owner.call{value: amount}(""); + if (!success) revert TransferFailed(); + } + + /** + * @notice Sets the roulette contract address (can only be called once) + * @param _roulette The address of the CryptoRoulette contract + */ + function setRouletteContract(address _roulette) external onlyOwner { + require(!rouletteSet, "Already configured"); + require(_roulette != address(0), "Invalid address"); + rouletteContract = _roulette; + rouletteSet = true; + } + + /** + * @notice Transfers ownership to a new address + * @param newOwner The new owner address + */ + function transferOwnership(address newOwner) external onlyOwner { + owner = newOwner; + } + + // ============ View Functions ============ + + /** + * @notice Returns the Entropy contract address + * @return Address of the Entropy contract + */ + function getEntropy() internal view override returns (address) { + return address(entropy); + } + + /** + * @notice Gets the whitelist for a specific day + * @param day The day number + * @return Array of whitelisted addresses (may contain duplicates) + */ + function getWhitelist(uint256 day) external view returns (address[] memory) { + return dailyWhitelist[day]; + } + + /** + * @notice Gets the pool amount for a specific day + * @param day The day number + * @return The pool amount in wei + */ + function getPoolAmount(uint256 day) external view returns (uint256) { + return dailyPool[day]; + } + + /** + * @notice Gets the whitelist size for a specific day + * @param day The day number + * @return The number of entries in the whitelist + */ + function getWhitelistSize(uint256 day) external view returns (uint256) { + return dailyWhitelist[day].length; + } + + /** + * @notice Gets details of a specific draw + * @param sequenceNumber The sequence number of the draw + * @return The DrawRequest struct + */ + function getDrawDetails(uint64 sequenceNumber) external view returns (DrawRequest memory) { + return draws[sequenceNumber]; + } + + /** + * @notice Checks if a day's lottery has been completed + * @param day The day number + * @return True if completed, false otherwise + */ + function isDayCompleted(uint256 day) external view returns (bool) { + return dayCompleted[day]; + } + + // ============ Internal Functions ============ + + /** + * @notice Callback function called by Entropy with the random number + * @dev Selects the winner and transfers the prize + * @param sequenceNumber The sequence number of the request + * @param randomNumber The random number provided by Entropy + */ + function entropyCallback( + uint64 sequenceNumber, + address, // provider address, not used + bytes32 randomNumber + ) internal override { + DrawRequest storage draw = draws[sequenceNumber]; + + // Ensure draw hasn't been fulfilled already + if (draw.fulfilled) return; + + uint256 day = draw.day; + address[] memory whitelist = dailyWhitelist[day]; + + // Select winner using random number + uint256 winnerIndex = uint256(randomNumber) % whitelist.length; + address winner = whitelist[winnerIndex]; + + // Get prize amount + uint256 prize = dailyPool[day]; + + // Update state + draw.fulfilled = true; + draw.winner = winner; + dayCompleted[day] = true; + dailyPool[day] = 0; // Clear the pool + + // Transfer prize to winner + (bool success, ) = winner.call{value: prize}(""); + if (!success) revert TransferFailed(); + + emit WinnerSelected(day, winner, prize); + } + + // ============ Receive Function ============ + + receive() external payable {} +} + diff --git a/entropy-sdk-solidity/examples/crypto-roulette-lottery/contracts/interfaces/IDailyLottery.sol b/entropy-sdk-solidity/examples/crypto-roulette-lottery/contracts/interfaces/IDailyLottery.sol new file mode 100644 index 0000000..9558c8b --- /dev/null +++ b/entropy-sdk-solidity/examples/crypto-roulette-lottery/contracts/interfaces/IDailyLottery.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +/** + * @title IDailyLottery + * @notice Interface for communication between CryptoRoulette and DailyLottery contracts + */ +interface IDailyLottery { + /** + * @notice Adds a player to the whitelist for a specific day + * @param player Address of the player who won the roulette + * @param day The day number for which to add the player + */ + function addToWhitelist(address player, uint256 day) external; + + /** + * @notice Adds funds to the daily pool + * @param day The day number for which to add to the pool + */ + function addToPool(uint256 day) external payable; +} + diff --git a/entropy-sdk-solidity/examples/crypto-roulette-lottery/script/DeployCryptoRoulette.s.sol b/entropy-sdk-solidity/examples/crypto-roulette-lottery/script/DeployCryptoRoulette.s.sol new file mode 100644 index 0000000..983f257 --- /dev/null +++ b/entropy-sdk-solidity/examples/crypto-roulette-lottery/script/DeployCryptoRoulette.s.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./DeployHelpers.s.sol"; +import "../contracts/DailyLottery.sol"; +import "../contracts/CryptoRoulette.sol"; + +/** + * @notice Deploy script for CryptoRoulette and DailyLottery contracts + * @dev Deploys both contracts and links them together + * Example: + * yarn deploy --file DeployCryptoRoulette.s.sol # local anvil chain + * yarn deploy --file DeployCryptoRoulette.s.sol --network sepolia # live network (requires keystore) + */ +contract DeployCryptoRoulette is ScaffoldETHDeploy { + /** + * @dev Deployer setup based on `ETH_KEYSTORE_ACCOUNT` in `.env` + * + * Note: For local testing, we use a placeholder entropy address. + * For testnet/mainnet deployment, update the entropy address below: + * - Sepolia: Check Pyth Entropy documentation for Sepolia address + * - Other networks: Check https://docs.pyth.network/entropy + */ + function run() external ScaffoldEthDeployerRunner { + // Placeholder Entropy address for local testing + // TODO: Update this address for testnet/mainnet deployment + address entropyAddress = address(0x4821932D0CDd71225A6d914706A621e0389D7061); + + // Initial ticket price (0.001 ETH) + uint256 ticketPrice = 0.001 ether; + + console.log("Deploying DailyLottery with entropy:", entropyAddress); + + // Deploy DailyLottery first (roulette contract will be set later) + DailyLottery lottery = new DailyLottery( + entropyAddress, + address(0) // Placeholder, will be set after CryptoRoulette deployment + ); + + console.log("DailyLottery deployed at:", address(lottery)); + console.log("Deploying CryptoRoulette..."); + + // Deploy CryptoRoulette with lottery address + CryptoRoulette roulette = new CryptoRoulette( + entropyAddress, + address(lottery), + ticketPrice + ); + + console.log("CryptoRoulette deployed at:", address(roulette)); + console.log("Setting roulette contract in DailyLottery..."); + + // Set the roulette contract in DailyLottery + lottery.setRouletteContract(address(roulette)); + + console.log("Deployment complete!"); + console.log("- DailyLottery:", address(lottery)); + console.log("- CryptoRoulette:", address(roulette)); + console.log("- Ticket Price:", ticketPrice); + console.log("- Entropy:", entropyAddress); + } +} +