-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
lottery contract, tests and demo script #17
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lets add in a require(contractPhase == open) to avoid the option to re-call this method again? |
||
// 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this method isn't necessary - lets lean on calling players() instead There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. players() can only be called on each element. It is not convenient for reading the whole list. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the ideal way is for the script to do the heavy lifting - and reducing the contract size hence. But since this is a demo feel free to use your preference. |
||
return players; | ||
} | ||
} |
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; | ||
}); |
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; | ||
}); |
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; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just checking - do you want to have one entry per wallet condition here?