UNPKG

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