This repo contains smart contracts and related scripts for ZkNoid L1 Lottery proposed here
Pre-Round Phase
- Before a round begins, the only action permitted is committing a value for randomness.
Active Round
- A round is considered active if the current chain slot falls within the
[roundStartBlock, roundEndBlock]
range.
During the Round
- Users are allowed to
buyTickets
.
Post-Round Actions
- Once the round concludes, the following steps must be completed before users can claim rewards for their tickets:
-
Ticket Reduction:
ticketReduce
must be invoked on thePLottery
contract. This function will reduce all tickets from the target round and at least one from the subsequent round.
-
Randomness Reveal:
reveal
should be called on theRandomManager
for the target round. This will generate the seed required for winning ticket generation.
-
Result Production:
produceResult
must be executed on thePLottery
contract to determine the winning ticket for the round.
-
Reward Collection
- After the above steps are completed, users can call
getReward
to claim their rewards for the tickets.
Fallback for Result Generation
- If the result is not produced within the next two rounds, users will be eligible to request a refund for their tickets.
@state(Field) ticketRoot = State<Field>();
@state(Field) ticketNullifier = State<Field>();
@state(Field) bankRoot = State<Field>();
@state(Field) roundResultRoot = State<Field>();
@state(UInt32) startBlock = State<UInt32>();
@state(Field) lastProcessedState = State<Field>();
@state(Field) lastReduceInRound = State<Field>();
@state(Field) lastProcessedTicketId = State<Field>();
We are using all 8 avaliable slots:
- ticketRoot - to store merkle tree root for ticket
- ticketNullifier - to store merkle tree root for nullifer, so one ticket could be redeemed only once
- bankRoot - to store bank merkle tree root for each round
- roundResultRoot - to store winning combination for each round
- startBlock - stores slot of deployment
- lastProcessedState - stores last processed actions state
- lastReduceInRound - stores round in wich last actions reduce was called. We can garantee, that in this case all tickets from previous rounds was processed by reducer
- lastProcessedTicketId - id of last processed ticket by reducer
For ticket purchase buyTicket method is used
@method async buyTicket(ticket: Ticket, round: Field) {
...
}
It do not update ticket merkle tree, but add action to actionList.
this.reducer.dispatch(
new LotteryAction({
ticket,
round,
})
);
Also it fires event:
this.emitEvent(
'buy-ticket',
new BuyTicketEvent({
ticket,
round: round,
})
);
As mentioned earlier buyTicket do not update ticket merkle tree, but add action to actionList. We can't use it directly, so we need to convert actions list to merkle tree. We do it using ZkProgramm:
export const TicketReduceProgram = ZkProgram({
name: 'ticket-reduce-program',
publicInput: TicketReduceProofPublicInput,
publicOutput: TicketReduceProofPublicOutput,
methods: {
init: {
...
},
addTicket: {
...
},
cutActions: {
...
},
},
});
For random generation we have separate contract. It utilize ZKOn zk oracle and commit-reveal technique.
First we commit our hidden value:
@method async commit(
commitValue: CommitValue,
commitWitness: MerkleMapWitness
) {
...
}
Then send request for ZkOn oracle, with request that lies on IPFS(cid can be found here). It sends request on quantum-random.com to get random number.
@method async callZkon() {
...
const requestId = await coordinator.sendRequest(
this.address,
hashPart1,
hashPart2
);
...
}
After we receive random number from ZKOn, we mix it with out hidden number, and store result on merkle tree.
@method async reveal(
commitValue: CommitValue,
commitWitness: MerkleMapWitness,
resultWitness: MerkleMapWitness
) {
...
const resultValue = Poseidon.hash([commitValue.value, curRandomValue]);
// Update result
const [newResultRoot] = resultWitness.computeRootAndKey(resultValue);
this.resultRoot.set(newResultRoot);
...
}
Later we will use this number to generate winning combination
@method async produceResult(
resultWiness: MerkleMap20Witness,
result: Field,
bankValue: Field,
bankWitness: MerkleMap20Witness,
rmWitness: MerkleMapWitness,
rmValue: Field
) {
...
this.checkRandomResultValue(rmWitness, rmValue, round);
let winningNumbers = generateNumbersSeed(rmValue);
...
}
public checkRandomResultValue(
roundResultWitness: MerkleMapWitness,
roundResulValue: Field,
round: Field
) {
...
const rm = new RandomManager(randomManagerAddress);
const resultRoot = rm.resultRoot.getAndRequireEquals();
...
}
Bank is distrubuted among all players fairly, according to amount of rightly guessed number. To do so, after winning numbers generation we match score for it ticket:
- 0 numbers guessed - 0
- 1 number guessed - 90
- 2 numbers guessed - 324
- 3 numbers guessed - 2187
- 4 numbers guessed - 26244
- 5 numbers guessed - 590490
- 6 numbers guessed - 31886460
Then we compute total score for round in a provable way using proof.
Each ticket can get roundBank * ticketScore / totalScore tokens.
To get winning for ticket getReward function exist. It will compute score for ticket, portion of bank, which it owns, transfer to to user, and update nullifier merkle map, so this ticket can't be used in future.
@method async getReward(
ticket: Ticket,
roundWitness: MerkleMap20Witness,
roundTicketWitness: MerkleMap20Witness,
dp: DistributionProof,
winningNumbers: Field,
resutWitness: MerkleMap20Witness,
bankValue: Field,
bankWitness: MerkleMap20Witness,
nullifierWitness: MerkleMapWitness
) {
}
Install npm modules:
npm install
Prepare mina-fungible-token module:
npm run token_prepare
npm run build
npm run test
npm run coverage
To deploy you first need to set evironments variables:
DEPLOYER_KEY = ""
RANDOM_MANAGER_OWNER_ADDRESS = ""
Then you can deploy it using
npm run deploy
It will generate private and public keys for RandomManager and Lottery. Deploy them on corresponding addresses and will store addresses on deploy/addresses and store keys on keys/auto