Skip to content
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

Merged
merged 3 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions contracts/lottery.sol
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");
Copy link
Contributor

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?

// 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");
Copy link
Contributor

Choose a reason for hiding this comment

The 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method isn't necessary - lets lean on calling players() instead

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
}
7 changes: 7 additions & 0 deletions demo-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,12 @@
"0x742e271d909d026b2b4dcc0384ec0b8df8f674f0773c354b57c24858419e89d3",
"0xeeb82181766c7d0fe45d2cb5b3399b1da17a1a432938ec8c4d73daca85eedaea",
"0x88b68d7e8d96d8465cfe3dc4abf707e21aa49139189a784e2d50cc9ada9076c3"
],
"lotteryAddress": "0x019254975A0e82C44e16CCEd295e332C1F6774f2",
"lotteryAdminKey": "0xc9224470c28153019fb65b231370e8c946b65b9d012e46e57d6f9f3f7827e5cd",
"lotteryPlayerKeys": [
"0xa8a4ba3252cdf977c2d16a67f8eefae5d26e88637a23b6c82b4dbb2364eb7fb5",
"0x7fa41fbee14d382f42df346fe92cbde2fcb3bbd30fbfffb505c5466a62bc5d77",
"0x3fc3e099ad4811fa4b8c7f2c2d796337de84b5e8ec34817015d7f4c1ac92efef"
]
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@
"register": "npx hardhat run scripts/register.ts",
"nidkg": "npx hardhat run scripts/nidkg.ts",
"member": "yarn register && yarn nidkg",
"random": "npx hardhat run scripts/random.ts"
"random": "npx hardhat run scripts/random.ts",
"lottery:deploy": "npx hardhat run scripts/lottery/deploy.ts",
"lottery:admin": "npx hardhat run scripts/lottery/admin.ts",
"lottery:play": "npx hardhat run scripts/lottery/play.ts"
},
"repository": {
"type": "git",
Expand Down
76 changes: 76 additions & 0 deletions scripts/lottery/admin.ts
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;
});
26 changes: 26 additions & 0 deletions scripts/lottery/deploy.ts
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;
});
46 changes: 46 additions & 0 deletions scripts/lottery/play.ts
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;
});
Loading
Loading