Skip to content

Commit

Permalink
Merge pull request #17 from bobanetwork/lottery
Browse files Browse the repository at this point in the history
lottery contract, tests and demo script
  • Loading branch information
kitounliu committed Jul 1, 2024
2 parents d3a0085 + 16b356f commit 0caefef
Show file tree
Hide file tree
Showing 8 changed files with 352 additions and 11 deletions.
36 changes: 31 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# zkDVRF
# zkRand

zkDVRF is a t-out-of-n threshold scheme that runs among a group of n distributed members. The protocol consists of two components:
zkRand is a t-out-of-n threshold scheme that runs among a group of n distributed members. The protocol consists of two components:
a snark-based non-interactive distributed key generation (NI-DKG) and randomness generation based on threshold bls-signatures.

### To build:
Expand Down Expand Up @@ -191,7 +191,7 @@ which can be used to obtain "evals.json" by converting all the big integers into

## Deploy

To deploy the Zkdvrf contracts on-chain-
To deploy the zkRand contracts on-chain-

1. Set up your .env (use .env.example for reference)

Expand Down Expand Up @@ -263,15 +263,15 @@ DEGREE=18

### Step-2: Deploy Contracts

1. To deploy the zkdvrf contracts, run-
1. To deploy the zkRand contracts, run-

```
yarn deploy
```

2. Populate your demo-config.json file using-

a) your zkdvrf deployed address
a) your zkdvrf.sol deployed address
b) five sample addresses, and their private keys from ganache pre-generated accounts

### Step-3: NIDKG
Expand Down Expand Up @@ -309,3 +309,29 @@ If you have exited the admin script, but have already been through the NIDKG pro
yarn admin:restart
```

### Continuing with lottery demo
1. Deploy the lottery contracts

```
yarn lottery:deploy
```

2. Populate your demo-config.json file using-

a) your lottery.sol deployed address
b) private keys for lottery admin and three players from ganache pre-generated accounts

3. Run the lottery admin to start the lottery
```
yarn lottery:admin
```
The lottery will set a target random for picking up a winner.
The round number for target random is set to be 3 in the script.

4. Run the players to place bets
```
yarn lottery:play
```
Before zkdvrf.sol starts producing the target random, players can enter the lottery by depositing a certain amount of ethers.

5. Continuing the above Step-4 for generating random until the round number hits 3 which will trigger the lottery admin to pick and pay a winner.
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");
// 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;
}
}
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

0 comments on commit 0caefef

Please sign in to comment.