l1-lottery-contracts
Version:
This repo contains smart contracts and related scripts for ZkNoid L1 Lottery proposed [here](https://forums.minaprotocol.com/t/zknoid-l1-lottery/6269)
423 lines • 18.9 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { Field, SmartContract, state, State, method, UInt32, AccountUpdate, PublicKey, Struct, Reducer, UInt64, Permissions, VerificationKey, } from 'o1js';
import { Ticket } from './Structs/Ticket.js';
import { BLOCK_PER_ROUND, COMMISSION, PRECISION, TICKET_PRICE, mockWinningCombination, treasury, } from './constants.js';
import { NumberPacked, convertToUInt64 } from './util.js';
import { MerkleMap20, MerkleMap20Witness } from './Structs/CustomMerkleMap.js';
import { ActionList, LotteryAction, TicketReduceProof, } from './Proofs/TicketReduceProof.js';
import { RandomManager } from './Random/RandomManager.js';
export const mockResult = NumberPacked.pack(mockWinningCombination.map((v) => UInt32.from(v)));
export const generateNumbersSeed = (seed) => {
let bits = seed.toBits();
const numbers = [...Array(6)].map((_, i) => {
let res64 = UInt64.from(0);
res64.value = Field.fromBits(bits.slice(42 * i, 42 * (i + 1)));
res64 = res64.mod(9).add(1);
let res = UInt32.from(0);
res.value = res64.value;
return res;
});
return numbers;
};
const emptyMap20Root = new MerkleMap20().getRoot();
export class BuyTicketEvent extends Struct({
ticket: Ticket,
}) {
}
export class ProduceResultEvent extends Struct({
result: Field,
totalScore: UInt64,
bank: Field,
}) {
}
export class GetRewardEvent extends Struct({
ticket: Ticket,
ticketId: Field,
}) {
}
export class RefundEvent extends Struct({
ticketId: Field,
ticket: Ticket,
}) {
}
export class ReduceEvent extends Struct({}) {
}
export class PLottery extends SmartContract {
constructor() {
super(...arguments);
this.reducer = Reducer({ actionType: LotteryAction });
this.events = {
'buy-ticket': BuyTicketEvent,
'produce-result': ProduceResultEvent,
'get-reward': GetRewardEvent,
'get-refund': RefundEvent,
reduce: ReduceEvent,
};
// Do not change order of storage, as it would affect deployment via factory
// !!!!First slot for outer initializer
this.randomManager = State();
// Stores block of deploy
this.startSlot = State();
// Stores merkle map with all tickets, that user have bought
this.ticketRoot = State();
// Stores nullifier tree root for tickets, so one ticket can't be used twice
this.ticketNullifier = State();
// Stores merkle map with total bank for each round.
this.bank = State();
// Stores merkle map with wining combination for each rounds
this.result = State();
this.totalScore = State();
}
init() {
super.init();
/// !!!! This contracts is deployed from factory. No init call there
this.ticketRoot.set(emptyMap20Root);
this.ticketNullifier.set(emptyMap20Root);
this.startSlot.set(this.network.globalSlotSinceGenesis.getAndRequireEquals());
this.account.permissions.set({
...Permissions.default(),
setVerificationKey: Permissions.VerificationKey.impossibleDuringCurrentVersion(),
});
}
/**
* @notice Set verification key for account
* @dev verification key can be updated only if Mina hardfork happen. It allows zkApp to be live after Mina hardfork
* @param vk Verification key
*/
async updateVerificationKey(vk) {
this.account.verificationKey.set(vk);
}
/**
* @notice Allows a user to buy a lottery ticket for a specific round.
* @dev No ticket merkle tree update happens here. Only action is dispatched.
*
* @param ticket The lottery ticket being purchased.
*
* @require The ticket must be valid as per the ticket validity check.
* @require The specified round must be the current lottery round.
*
* @event buy-ticket Emitted when a ticket is successfully purchased.
*/
async buyTicket(ticket) {
// Ticket validity check
ticket.check().assertTrue();
ticket.amount.assertGreaterThan(UInt64.from(0), 'Ticket amount should be positive');
// Round check
this.checkCurrentRound();
// Take ticket price from user
let senderUpdate = AccountUpdate.createSigned(this.sender.getAndRequireSignatureV2());
senderUpdate.send({ to: this, amount: TICKET_PRICE.mul(ticket.amount) });
// Dispatch action and emit event
this.reducer.dispatch(new LotteryAction({
ticket,
}));
this.emitEvent('buy-ticket', new BuyTicketEvent({
ticket,
}));
}
/**
* @notice Reduce tickets that lies as actions.
* @dev This function verifies the proof and ensures that the contract's state matches the state described in the proof.
* It then updates the tickets merkle tree, populating it with new tickets.
*
* @param reduceProof The proof that validates the ticket reduction process and contains the new contract state.
*
* @require The proof must be valid and successfully verified.
* @require The processed action list in the proof must be empty, indicating that all actions have been processed.
* @require The contract's last processed state must match the initial state in the proof.
* @require The contract's action state must match the final state in the proof.
* @require The contract's last processed ticket ID must match the initial ticket ID in the proof.
*
* @event reduce Emitted when the tickets are successfully reduced and the contract state is updated.
*/
async reduceTicketsAndProduceResult(reduceProof) {
this.result
.getAndRequireEquals()
.assertEquals(Field(0), 'Already produced');
// Only after round is passed
this.checkRoundPass(UInt32.from(1));
// Get random value
const RM = new RandomManager(this.randomManager.getAndRequireEquals());
const rmValue = RM.result.getAndRequireEquals();
rmValue.assertGreaterThan(Field(0), 'Random value was not generated yet');
this.approve(RM.self);
let winningNumbers = generateNumbersSeed(rmValue);
let winningNumbersPacked = NumberPacked.pack(winningNumbers);
this.checkReduceProof(reduceProof, winningNumbersPacked);
const bankValue = reduceProof.publicOutput.newBank;
this.send({
to: treasury,
amount: convertToUInt64(bankValue.mul(COMMISSION).div(PRECISION)),
});
const newBankValue = bankValue.mul(PRECISION - COMMISSION).div(PRECISION);
// Update onchain values
this.ticketRoot.set(reduceProof.publicOutput.newTicketRoot);
this.bank.set(newBankValue);
this.totalScore.set(reduceProof.publicOutput.totalScore);
this.result.set(winningNumbersPacked);
// Emit event
this.emitEvent('produce-result', new ProduceResultEvent({
result: winningNumbersPacked,
bank: newBankValue,
totalScore: reduceProof.publicOutput.totalScore,
}));
}
// If random manager can't produce value
async emergencyReduceTickets(reduceProof) {
// Allow only after 2 round pass
this.checkRoundPass(UInt32.from(2));
// And no result produce
this.result.getAndRequireEquals().assertEquals(Field(0));
this.checkReduceProof(reduceProof, Field(0));
this.ticketRoot.set(reduceProof.publicOutput.newTicketRoot);
}
checkReduceProof(reduceProof, winningNumbersPacked) {
// Check proof validity
reduceProof.verify();
// Check that all actions was processed.
reduceProof.publicOutput.processedActionList.assertEquals(ActionList.emptyHash, 'Proof is not complete. Call cutActions first');
// Check random value
reduceProof.publicOutput.winningNumbersPacked.assertEquals(winningNumbersPacked, 'Wrong winning combination used in proof');
// If emergency reduce was previously call then check that ticketRoot is equal to newTicketRoot
const currentRoot = this.ticketRoot.getAndRequireEquals();
currentRoot
.equals(new MerkleMap20().getRoot())
.or(currentRoot.equals(reduceProof.publicOutput.newTicketRoot))
.assertTrue('Wrong ticket root');
// Check that actionState is equal to actionState on proof
this.account.actionState
.getAndRequireEquals()
.assertEquals(reduceProof.publicOutput.finalState);
}
// Update refund natspec
/**
* @notice Processes a refund for a lottery ticket if the result for the round was not generated within 2 days.
* @dev This function ensures that the ticket owner is the one requesting the refund, verifies the ticket's validity
* in the Merkle maps, checks that the result for the round is zero, and processes the refund after verifying
* and updating the necessary states.
*
* @param ticket The lottery ticket for which the refund is being requested.
* @param ticketWitness Witness of the ticket in the ticketMap tree.
*
* @require The sender must be the owner of the ticket.
* @require The ticket must exist in the Merkle map as verified by the round and ticket witnesses.
* @require The result for the round must be zero to be eligible for a refund.
* @require The refund can only be processed after approximately two days since the round finished.
*
* @event get-refund Emitted when a refund is successfully processed and the ticket price is returned to the user.
*/
async refund(ticket, ticketWitness) {
// Check that owner trying to claim
ticket.owner.assertEquals(this.sender.getAndRequireSignatureV2());
const result = this.result.getAndRequireEquals();
result.assertEquals(Field(0), 'Result for this round is not zero');
// Check ticket in merkle map and set ticket to zero
const [ticketRoot, ticketId] = ticketWitness.computeRootAndKeyV2(ticket.hash());
this.ticketRoot
.getAndRequireEquals()
.assertEquals(ticketRoot, 'Wrong ticket witness');
const [newTicketRoot] = ticketWitness.computeRootAndKeyV2(Field(0));
this.ticketRoot.set(newTicketRoot);
// Can call refund after ~ 2 days after round finished
this.checkRoundPass(UInt32.from(2));
// Check and update bank witness
const totalTicketPrice = ticket.amount.mul(TICKET_PRICE);
const bankValue = this.bank.getAndRequireEquals();
const newBankValue = bankValue.sub(totalTicketPrice.value);
this.bank.set(newBankValue);
// Send ticket price back to user
this.send({
to: ticket.owner,
amount: totalTicketPrice,
});
this.emitEvent('get-refund', new RefundEvent({
ticketId,
ticket,
}));
}
/**
* @notice Claims the reward for a winning lottery ticket.
* @dev This function calculate ticket score, totalScore is obtained from DistributionProof,
* and then sends appropriate potion of bank to ticket owner. Finally it nullify the ticket.
*
* @param ticket The lottery ticket for which the reward is being claimed.
* @param ticketWitness Witness of the ticket in the ticketMap tree.
* @param nullifierWitness The Merkle proof witness for the nullifier tree.
*
* @require The sender must be the owner of the ticket.
* @require The distribution proof must be valid and match the round's ticket root and winning numbers.
* @require The ticket must exist in the Merkle map as verified by the round and ticket witnesses.
* @require The actions for the round must be reduced before claiming the reward.
*
* @event get-reward Emitted when the reward is successfully claimed and transferred to the ticket owner.
*/
async getReward(ticket, ticketWitness, nullifierWitness) {
// Check ticket in tree
const [ticketRoot, ticketId] = ticketWitness.computeRootAndKeyV2(ticket.hash());
this.ticketRoot
.getAndRequireEquals()
.assertEquals(ticketRoot, 'Wrong ticket witness');
const winningNumbers = this.result.getAndRequireEquals();
winningNumbers.assertGreaterThan(Field(0), 'Winning number is not generated yet');
// Compute score using winning ticket
const score = ticket.getScore(NumberPacked.unpack(winningNumbers));
const totalScore = this.totalScore.getAndRequireEquals();
const bank = this.bank.getAndRequireEquals();
const payAmount = convertToUInt64(bank).mul(score).div(totalScore);
this.send({
to: ticket.owner,
amount: payAmount,
});
// Add ticket to nullifier
this.checkAndUpdateNullifier(nullifierWitness, ticketId, Field(0), Field.from(1));
this.emitEvent('get-reward', new GetRewardEvent({
ticket,
ticketId,
}));
}
/**
* @notice Check that execution is happening within provided round
*
*
* @require globalSlotSinceGenesis to be within range of round
*/
checkCurrentRound() {
const startSlot = this.startSlot.getAndRequireEquals();
this.network.globalSlotSinceGenesis.requireBetween(startSlot, startSlot.add(BLOCK_PER_ROUND).sub(1));
}
/**
* @notice Check that execution is happening after provided round
*
* @param amount Amounts of rounds to pass to check
*
* @require globalSlotSinceGenesis to be greater then last slot of given number
*/
checkRoundPass(amount) {
const startSlot = this.startSlot.getAndRequireEquals();
this.network.globalSlotSinceGenesis.requireBetween(startSlot.add(amount.mul(BLOCK_PER_ROUND)), UInt32.MAXINT());
}
/**
* @notice Check validity of merkle map witness for nullifier tree and then updates tree with new value.
*
* @param witness Merkle map witness for nullifier tree.
* @param key Round number, that will be compared with <witness> key.
* @param curValue Value of nullifier to be checked.
* @param newValue New value that should be store in tree.
*
* @returns key of <witness>
*/
checkAndUpdateNullifier(witness, key, curValue, newValue) {
return this.checkAndUpdateMap(this.ticketNullifier, witness, key, curValue, newValue);
}
/**
* @notice General method that allows to check and update onchain merkle trees roots
*
* @param state On-chain state, that should be updated.
* @param witness Merkle map witness.
* @param key Key that will be compared with <witness> key.
* @param curValue Value to be checked.
* @param newValue New value that should be store in tree.
*
* @returns key of <witness>
*/
checkAndUpdateMap(state, witness, key, curValue, newValue) {
let checkRes = this.checkMap(state, witness, key, curValue);
const [newRoot] = witness.computeRootAndKeyV2(newValue);
state.set(newRoot);
return checkRes;
}
/**
* @notice General method that allows to check onchain merkle trees roots
*
* @param state On-chain state, that should be updated.
* @param witness Merkle map witness.
* @param key Key that will be compared with <witness> key.
* @param curValue Value to be checked.
*
* @returns key of <witness>
*/
checkMap(state, witness, key, curValue) {
const curRoot = state.getAndRequireEquals();
const [prevRoot, witnessKey] = witness.computeRootAndKeyV2(curValue);
curRoot.assertEquals(prevRoot, 'Wrong witness');
witnessKey.assertEquals(key, 'Wrong key');
return {
key: witnessKey,
};
}
}
__decorate([
state(PublicKey),
__metadata("design:type", Object)
], PLottery.prototype, "randomManager", void 0);
__decorate([
state(UInt32),
__metadata("design:type", Object)
], PLottery.prototype, "startSlot", void 0);
__decorate([
state(Field),
__metadata("design:type", Object)
], PLottery.prototype, "ticketRoot", void 0);
__decorate([
state(Field),
__metadata("design:type", Object)
], PLottery.prototype, "ticketNullifier", void 0);
__decorate([
state(Field),
__metadata("design:type", Object)
], PLottery.prototype, "bank", void 0);
__decorate([
state(Field),
__metadata("design:type", Object)
], PLottery.prototype, "result", void 0);
__decorate([
state(UInt64),
__metadata("design:type", Object)
], PLottery.prototype, "totalScore", void 0);
__decorate([
method,
__metadata("design:type", Function),
__metadata("design:paramtypes", [VerificationKey]),
__metadata("design:returntype", Promise)
], PLottery.prototype, "updateVerificationKey", null);
__decorate([
method,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Ticket]),
__metadata("design:returntype", Promise)
], PLottery.prototype, "buyTicket", null);
__decorate([
method,
__metadata("design:type", Function),
__metadata("design:paramtypes", [TicketReduceProof]),
__metadata("design:returntype", Promise)
], PLottery.prototype, "reduceTicketsAndProduceResult", null);
__decorate([
method,
__metadata("design:type", Function),
__metadata("design:paramtypes", [TicketReduceProof]),
__metadata("design:returntype", Promise)
], PLottery.prototype, "emergencyReduceTickets", null);
__decorate([
method,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Ticket, MerkleMap20Witness]),
__metadata("design:returntype", Promise)
], PLottery.prototype, "refund", null);
__decorate([
method,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Ticket,
MerkleMap20Witness,
MerkleMap20Witness]),
__metadata("design:returntype", Promise)
], PLottery.prototype, "getReward", null);
//# sourceMappingURL=PLottery.js.map