-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #17 from bobanetwork/lottery
lottery contract, tests and demo script
- Loading branch information
Showing
8 changed files
with
352 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
import {zkdvrf} from "./zkdvrf.sol"; | ||
|
||
import "@openzeppelin/contracts/utils/Strings.sol"; | ||
import '@openzeppelin/contracts/access/Ownable.sol'; | ||
|
||
contract Lottery is Ownable { | ||
using Strings for uint256; | ||
|
||
address payable[] public players; | ||
mapping(address => bool) public hasEntered; | ||
uint256 public minBet; | ||
|
||
address public zkdvrfAddr; | ||
uint256 public randRoundNum; | ||
bytes32 public randValue; | ||
|
||
enum Status { | ||
Setup, | ||
Open, | ||
Close | ||
} | ||
|
||
Status public contractPhase; | ||
|
||
event BetOpen(uint256 randRoundNumber, uint256 minBetAmount); | ||
|
||
constructor(address zkdvrfAddress) Ownable(msg.sender) { | ||
zkdvrfAddr = zkdvrfAddress; | ||
} | ||
|
||
function setup(uint256 randRoundNumber, uint256 minBetAmount) public onlyOwner { | ||
require(contractPhase == Status.Setup, "Setup has already been completed"); | ||
randRoundNum = randRoundNumber; | ||
minBet = minBetAmount; | ||
|
||
contractPhase = Status.Open; | ||
emit BetOpen(randRoundNumber, minBetAmount); | ||
} | ||
|
||
// check if random has been produced or is being produced | ||
function roundReached() public returns (bool) { | ||
uint256 latestRoundNum = zkdvrf(zkdvrfAddr).currentRoundNum(); | ||
return randRoundNum <= latestRoundNum; | ||
} | ||
|
||
function enter() public payable { | ||
require(contractPhase == Status.Open, "Not open yet"); | ||
// Once the random generation starts or has completed, players are no longer allowed to enter | ||
require(!roundReached(), "Too late. Random has been produced or is being produced"); | ||
require(!hasEntered[msg.sender], "You have already entered the lottery"); | ||
require(msg.value >= minBet, "Must provide enough bet"); | ||
|
||
players.push(payable(msg.sender)); | ||
hasEntered[msg.sender] = true; | ||
} | ||
|
||
// Fisher-Yates Shuffle | ||
function shuffle() private { | ||
require(randValue != 0x00, "Random not ready yet"); | ||
|
||
for (uint i = 0; i < players.length; i++) { | ||
bytes32 randomBytes = keccak256(abi.encodePacked(randValue, i)); | ||
uint256 random = uint256(randomBytes); | ||
|
||
uint j = random % (i + 1); | ||
(players[i], players[j]) = (players[j], players[i]); | ||
} | ||
} | ||
|
||
function pickWinner() public onlyOwner { | ||
require(contractPhase == Status.Open, "Not open"); | ||
require(players.length > 0, "No players"); | ||
// read random from zkdvrf contract | ||
randValue = zkdvrf(zkdvrfAddr).getRandomAtRound(randRoundNum).value; | ||
shuffle(); // Shuffle the players array | ||
// The winner is the first player in the shuffled array | ||
// The permutation is randomly generated so we can also take more winners if needed | ||
(bool success, ) = players[0].call{value: address(this).balance}(""); | ||
require(success, "Transfer failed."); | ||
|
||
contractPhase = Status.Close; | ||
} | ||
|
||
|
||
function getPlayers() public view returns (address payable[] memory) { | ||
return players; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import hre, {artifacts, ethers} from "hardhat"; | ||
import {Contract, ContractFactory, providers, utils, Wallet} from "ethers"; | ||
import {readJsonFromFile} from "../utils"; | ||
|
||
const config = readJsonFromFile("demo-config.json") | ||
const zkdvrfAddress = config.zkdvrfAddress | ||
const lotteryAddress = config.lotteryAddress | ||
const adminKey = config.lotteryAdminKey | ||
|
||
async function main() { | ||
const netprovider = new providers.JsonRpcProvider(process.env.RPC_URL) | ||
const adminWallet = new Wallet(adminKey, netprovider) | ||
|
||
const Lottery = await ethers.getContractFactory('Lottery') | ||
const contractABI = Lottery.interface.format(); | ||
const contract = new ethers.Contract(lotteryAddress, contractABI, netprovider).connect(adminWallet) | ||
|
||
const randRoundNumber = 3 | ||
const minBet = ethers.utils.parseEther("5"); | ||
const res = await contract.setup(randRoundNumber, minBet) | ||
const receipt = await netprovider.getTransactionReceipt(res.hash); | ||
// Check if the transaction was successful | ||
if (receipt.status === 1) { | ||
console.log(`Transaction setup(..) successful!`); | ||
} else { | ||
console.error(`Transaction setup(..) failed!`); | ||
} | ||
console.log("Bet starts") | ||
console.log("Waiting for random in round ", randRoundNumber) | ||
|
||
const Zkdvrf = await ethers.getContractFactory('zkdvrf') | ||
const zkContractABI = Zkdvrf.interface.format(); | ||
const zkContract = new ethers.Contract(zkdvrfAddress, zkContractABI, netprovider) | ||
|
||
// This will run when the event is emitted | ||
const eventName = `RandomReady` | ||
zkContract.on(eventName, async (roundNum, input, event) => { | ||
console.log("event", eventName, roundNum, input); | ||
// Proceed to the next step here | ||
if (roundNum == randRoundNumber) { | ||
// the random number is ready | ||
const res = await contract.pickWinner() | ||
// Check if the transaction was successful | ||
const receipt = await netprovider.getTransactionReceipt(res.hash); | ||
if (receipt.status === 1) { | ||
console.log("Transaction pickWinner() successful!"); | ||
} else { | ||
console.error("Transaction pickWinner() failed!"); | ||
} | ||
|
||
const status = await contract.contractPhase() | ||
console.log("Lottery contract status:", status) | ||
|
||
const players = await contract.getPlayers() | ||
console.log("Players:", players) | ||
|
||
// query users balance | ||
for (let i = 0; i < players.length; i++) { | ||
netprovider.getBalance(players[i]).then((balance) => { | ||
// Convert Wei to Ether | ||
let etherString = ethers.utils.formatEther(balance); | ||
console.log(players[i], " balance: " + etherString); | ||
}).catch((err) => { | ||
console.error(err); | ||
}); | ||
} | ||
} | ||
}); | ||
|
||
} | ||
|
||
|
||
main().catch((error) => { | ||
console.error(error); | ||
process.exitCode = 1; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import hre, {artifacts, ethers} from "hardhat"; | ||
import {Contract, ContractFactory, providers, utils, Wallet} from "ethers"; | ||
import {readJsonFromFile} from "../utils"; | ||
|
||
const config = readJsonFromFile("demo-config.json") | ||
const zkdvrfAddress = config.zkdvrfAddress | ||
const adminKey = config.lotteryAdminKey | ||
|
||
async function main() { | ||
const netprovider = new providers.JsonRpcProvider(process.env.RPC_URL) | ||
const deployerWallet = new Wallet(adminKey, netprovider) | ||
|
||
const Lottery = await ethers.getContractFactory('Lottery') | ||
const lottery = await Lottery.connect(deployerWallet).deploy(zkdvrfAddress) | ||
await lottery.deployed() | ||
|
||
console.log("Lottery contract deployed at", lottery.address) | ||
} | ||
|
||
main().then(() => { | ||
process.exit(0); | ||
}) | ||
.catch((error) => { | ||
console.error(error); | ||
process.exitCode = 1; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import hre, {artifacts, ethers} from "hardhat"; | ||
import {providers, Wallet} from "ethers"; | ||
import {readJsonFromFile} from "../utils"; | ||
|
||
const config = readJsonFromFile("demo-config.json") | ||
const lotteryAddress = config.lotteryAddress | ||
const playerKeys = config.lotteryPlayerKeys | ||
|
||
|
||
async function main() { | ||
const netprovider = new providers.JsonRpcProvider(process.env.RPC_URL) | ||
|
||
const Lottery = await ethers.getContractFactory('Lottery') | ||
const contractABI = Lottery.interface.format(); | ||
const contract = new ethers.Contract(lotteryAddress, contractABI, netprovider); | ||
|
||
// This will run when the event is emitted | ||
const eventName = `BetOpen` | ||
contract.on(eventName, async (randRoundNum, minBet, event) => { | ||
console.log("event", eventName, randRoundNum, minBet); | ||
// Proceed to the next step here | ||
|
||
for (let i = 0; i < playerKeys.length; i++) { | ||
const userWallet = new Wallet(playerKeys[i], netprovider) | ||
const userAddress = userWallet.address | ||
const userContract = contract.connect(userWallet) | ||
|
||
try { | ||
let tx = await userContract.enter({ | ||
value: minBet, | ||
from: userAddress, | ||
}); | ||
console.log(tx); | ||
} catch (err) { | ||
console.error(err); | ||
} | ||
} | ||
|
||
process.exit(0); | ||
}); | ||
} | ||
|
||
main().catch((error) => { | ||
console.error(error); | ||
process.exitCode = 1; | ||
}); |
Oops, something went wrong.