Smart Contract
FGameLottery
A.d2abb5dbf5e08666.FGameLottery
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# FGameLottery
5
6This contract is a lottery game contract. It allows users to buy tickets and participate in the lottery.
7The lottery is drawn every epoch. The lottery result is generated randomly and verified with the participants' tickets.
8
9*/
10import Burner from 0xf233dcee88fe0abe
11// Fixes Imports
12import Fixes from 0xd2abb5dbf5e08666
13import FixesHeartbeat from 0xd2abb5dbf5e08666
14import FRC20FTShared from 0xd2abb5dbf5e08666
15import FRC20Indexer from 0xd2abb5dbf5e08666
16import FRC20AccountsPool from 0xd2abb5dbf5e08666
17import FRC20Staking from 0xd2abb5dbf5e08666
18
19access(all) contract FGameLottery {
20
21 access(all) entitlement Admin
22
23 /* --- Events --- */
24 /// Event emitted when the contract is initialized
25 access(all) event ContractInitialized()
26
27 /// Event emitted when a new lottery ticket is created
28 access(all) event TicketAdded(
29 poolAddr: Address,
30 lotteryId: UInt64,
31 address: Address,
32 ticketId: UInt64,
33 numbers: [UInt8;6]
34 )
35 /// Event emitted when a ticket's powerup is updated
36 access(all) event TicketPowerupChanged(
37 poolAddr: Address,
38 lotteryId: UInt64,
39 address: Address,
40 ticketId: UInt64,
41 powerup: UFix64
42 )
43 /// Event emitted when a ticket status is updated
44 access(all) event TicketStatusChanged(
45 poolAddr: Address,
46 lotteryId: UInt64,
47 address: Address,
48 ticketId: UInt64,
49 fromStatus: UInt8,
50 toStatus: UInt8,
51 )
52 /// Event emitted when tickets is purchased
53 access(all) event TicketPurchased(
54 poolAddr: Address,
55 lotteryId: UInt64,
56 address: Address,
57 ticketIds: [UInt64],
58 costTick: String,
59 costAmount: UFix64
60 )
61 /// Event emitted when a ticket is verified
62 access(all) event TicketVerified(
63 poolAddr: Address,
64 lotteryId: UInt64,
65 address: Address,
66 ticketId: UInt64,
67 numbers: [UInt8;6],
68 status: UInt8,
69 prizeRank: UInt8
70 )
71 /// Event emitted when a ticket is disbursed
72 access(all) event TicketPrizeDisbursed(
73 poolAddr: Address,
74 lotteryId: UInt64,
75 address: Address,
76 ticketId: UInt64,
77 prizeAmount: UFix64
78 )
79 /// Event emitted when a new lottery is started
80 access(all) event LotteryStarted(
81 poolAddr: Address,
82 lotteryId: UInt64,
83 startTime: UFix64
84 )
85 /// Event emitted when a lottery is drawn
86 access(all) event LotteryDrawn(
87 poolAddr: Address,
88 lotteryId: UInt64,
89 numbers: [UInt8;6],
90 participantAmount: UInt64,
91 totalBought: UFix64,
92 jackpotAmount: UFix64
93 )
94 /// Event emitted when a lottery is verified with non-jackpot updated
95 access(all) event LotteryParticipantsVerified(
96 poolAddr: Address,
97 lotteryId: UInt64,
98 participants: [Address],
99 winners: UInt64,
100 nonJackpotTotal: UFix64
101 )
102 /// Event emitted when a lottery is verified with jackpot updated
103 access(all) event LotteryJackpotWinnerUpdated(
104 poolAddr: Address,
105 lotteryId: UInt64,
106 winner: Address
107 )
108 /// Event emitted when a lottery is verified with jackpot updated
109 access(all) event LotteryJackpotFinialized(
110 poolAddr: Address,
111 lotteryId: UInt64,
112 jackpotAmount: UFix64,
113 nonJackpotTotal: UFix64,
114 nonJackpotDowngradeRatio: UFix64
115 )
116 /// Event emitted when a lottery is disbursing prizes
117 access(all) event LotteryPrizesDisbursed(
118 poolAddr: Address,
119 lotteryId: UInt64,
120 )
121 /// Event emitted when a lottery jackpot is donated
122 access(all) event LotteryJackpotDonated(
123 poolAddr: Address,
124 donationAmount: UFix64
125 )
126
127 /* --- Variable, Enums and Structs --- */
128
129 access(all) let userCollectionStoragePath: StoragePath
130 access(all) let userCollectionPublicPath: PublicPath
131 access(all) let lotteryPoolStoragePath: StoragePath
132 access(all) let lotteryPoolPublicPath: PublicPath
133
134 access(all) var MAX_WHITE_NUMBER: UInt8
135 access(all) var MAX_RED_NUMBER: UInt8
136
137 /* --- Interfaces & Resources --- */
138
139 /// Struct for the ticket number
140 /// The ticket number is a combination of 5 white numbers and 1 red number
141 /// The white numbers are between 1 and MAX_WHITE_NUMBER
142 /// The red number is between 1 and MAX_RED_NUMBER
143 ///
144 access(all) struct TicketNumber {
145 // White numbers
146 access(all) let white: [UInt8;5]
147 // Red number
148 access(all) let red: UInt8
149
150 view init(white: [UInt8;5], red: UInt8) {
151 // Check if the white numbers are valid
152 for number in white {
153 assert(
154 number >= 1 && number <= FGameLottery.MAX_WHITE_NUMBER,
155 message: "White numbers must be between 1 and MAX_WHITE_NUMBER"
156 )
157 }
158 // Check if the red number is valid
159 assert(
160 red >= 1 && red <= FGameLottery.MAX_RED_NUMBER,
161 message: "Red number must be between 1 and MAX_RED_NUMBER"
162 )
163 self.white = white
164 self.red = red
165 }
166
167 /// Get the ticket numbers
168 access(all)
169 view fun getNumbers(): [UInt8;6] {
170 return [
171 self.white[0],
172 self.white[1],
173 self.white[2],
174 self.white[3],
175 self.white[4],
176 self.red
177 ]
178 }
179 }
180
181 /// Create a new random ticket number
182 ///
183 access(contract)
184 fun createRandomTicketNumber(): TicketNumber {
185 // Generate random numbers for the ticket
186 var whiteNumbers: [UInt8;5] = [0, 0, 0, 0, 0]
187 // generate the random white numbers, the numbers are between 1 and MAX_WHITE_NUMBER
188 var i = 0
189 while i < 5 {
190 let rndUInt8 = revertibleRandom<UInt8>(modulo: UInt8.max)
191 let newNum = rndUInt8 % FGameLottery.MAX_WHITE_NUMBER + 1
192 // we need to check if the number is already in the array
193 if !whiteNumbers.contains(newNum) {
194 whiteNumbers[i] = newNum
195 i = i + 1
196 }
197 }
198 // sort the white numbers in ascending order
199 // there is no sort method for array, so we use fast sort algorithm to sort the array
200 i = 0
201 while i < 4 {
202 var j = i + 1
203 while j < 5 {
204 if whiteNumbers[i] > whiteNumbers[j] {
205 let temp = whiteNumbers[i]
206 whiteNumbers[i] = whiteNumbers[j]
207 whiteNumbers[j] = temp
208 }
209 j = j + 1
210 }
211 i = i + 1
212 }
213 // generate the random red number, the number is between 1 and MAX_RED_NUMBER
214 let rndUInt8 = revertibleRandom<UInt8>(modulo: UInt8.max)
215 var redNumber: UInt8 = rndUInt8 % FGameLottery.MAX_RED_NUMBER + 1
216 return TicketNumber(white: whiteNumbers, red: redNumber)
217 }
218
219 /// Enum for the ticket status
220 ///
221 access(all) enum TicketStatus: UInt8 {
222 access(all) case ACTIVE
223 access(all) case LOSE
224 access(all) case WIN
225 access(all) case WIN_DISBURSED
226 }
227
228 /// Enum for the prize rank
229 ///
230 access(all) enum PrizeRank: UInt8 {
231 access(all) case JACKPOT // 100% Jackpot Pool + Math.min(50%, Remaining) of Current Pool
232 access(all) case SECOND // 50000x Ticket Price
233 access(all) case THIRD // 5000x Ticket Price
234 access(all) case FOURTH // 25x Ticket Price
235 access(all) case FIFTH // 4x Ticket Price
236 access(all) case SIXTH // 2x Ticket Price
237 }
238
239 /// Ticket entry resource interface
240 ///
241 access(all) resource interface TicketEntryPublic {
242 // variables
243 access(all) let pool: Address
244 access(all) let lotteryId: UInt64
245 access(all) let numbers: TicketNumber
246 access(all) let boughtAt: UFix64
247 // view functions
248 access(all) view fun getStatus(): TicketStatus
249 access(all) view fun getTicketId(): UInt64
250 access(all) view fun getTicketOwner(): Address
251 access(all) view fun getNumbers(): [UInt8;6]
252 access(all) view fun getPowerup(): UFix64
253 access(all) view fun getWinPrizeRank(): PrizeRank?
254 access(all) view fun getEstimatedPrizeAmount(): UFix64?
255 // borrow methods
256 access(all) view fun borrowLottery(): &Lottery
257 // write methods - only the contract can call these methods
258 access(contract) fun onPrizeVerify()
259 access(contract) fun onPrizeDisburse(_ prizeAmount: UFix64)
260 }
261
262 /// Resource for the ticket entry
263 ///
264 access(all) resource TicketEntry: TicketEntryPublic {
265 /// Lottery Pool Address for the ticket
266 access(all) let pool: Address
267 /// Lottery ID for the ticket
268 access(all) let lotteryId: UInt64
269 /// Ticket numbers
270 access(all) let numbers: TicketNumber
271 /// Ticket bought at
272 access(all) let boughtAt: UFix64
273 /// Ticket powerup, default is 1, you can increase the powerup to increase the winning amount
274 access(self) var powerup: UFix64
275 /// Ticket status
276 access(self) var status: TicketStatus
277 /// Winner prize rank
278 access(self) var winPrizeRank: PrizeRank?
279
280 init(
281 pool: Address,
282 lotteryId: UInt64,
283 powerup: UFix64?,
284 numbers: TicketNumber?,
285 ) {
286 pre {
287 powerup == nil || powerup! >= 1.0: "Powerup must be greater than 0"
288 powerup == nil || powerup! <= 10.0: "Powerup must be less than or equal to 10"
289 FGameLottery.borrowLotteryPool(pool) != nil: "Lottery pool not found"
290 }
291
292 self.pool = pool
293 self.lotteryId = lotteryId
294 self.boughtAt = getCurrentBlock().timestamp
295 // Create a new random ticket number
296 self.numbers = numbers ?? FGameLottery.createRandomTicketNumber()
297 // Set the default powerup to 1
298 self.powerup = powerup ?? 1.0
299 // Set the default status to ACTIVE
300 self.status = TicketStatus.ACTIVE
301 // Set the default win prize rank to nil
302 self.winPrizeRank = nil
303
304 // ensure lottery is active
305 let lotteryRef = self.borrowLottery()
306 assert(
307 lotteryRef.getStatus() == LotteryStatus.ACTIVE,
308 message: "Lottery is not active"
309 )
310 }
311
312 /// Get the ticket ID
313 ///
314 access(all)
315 view fun getTicketId(): UInt64 {
316 return self.uuid
317 }
318
319 /// Get the ticket owner
320 ///
321 access(all)
322 view fun getTicketOwner(): Address {
323 return self.owner?.address ?? panic("Ticket owner is missing")
324 }
325
326 /// Get the ticket numbers
327 ///
328 access(all)
329 view fun getNumbers(): [UInt8;6] {
330 return self.numbers.getNumbers()
331 }
332
333 /// Get the ticket powerup
334 ///
335 access(all)
336 view fun getPowerup(): UFix64 {
337 return self.powerup
338 }
339
340 /// Get the ticket status
341 ///
342 access(all)
343 view fun getStatus(): TicketStatus {
344 return self.status
345 }
346
347 /// Get the winner prize rank
348 ///
349 access(all)
350 view fun getWinPrizeRank(): PrizeRank? {
351 return self.winPrizeRank
352 }
353
354 /// Get the estimated prize amount
355 ///
356 access(all)
357 view fun getEstimatedPrizeAmount(): UFix64? {
358 let prizeRank = self.getWinPrizeRank()
359 if prizeRank == nil {
360 return nil
361 }
362
363 let lotteryRef = self.borrowLottery()
364 let drawnResult = lotteryRef.getResult()
365 if drawnResult == nil {
366 return nil
367 }
368 let pool = FGameLottery.borrowLotteryPool(self.pool)
369 ?? panic("Lottery pool not found")
370 let ticketOwner = self.getTicketOwner()
371 let feeRatio = pool.getServiceFee()
372 // Get the prize amount
373 if prizeRank! == PrizeRank.JACKPOT {
374 // Disburse the jackpot prize
375 let winners = drawnResult!.jackpotWinners
376 if winners == nil || winners!.length == 0 || !winners!.contains(ticketOwner) {
377 return nil
378 }
379 let jackpotAmount = drawnResult!.jackpotAmount
380 let jackpotWinnerAmt = winners?.length!
381 let basicPrize = jackpotAmount / UFix64(jackpotWinnerAmt)
382 // 16% prize is service fee
383 return basicPrize * (1.0 - feeRatio)
384 } else {
385 // Get the base prize amount
386 let basePrize = pool.getWinnerPrizeByRank(prizeRank!)
387
388 // Disburse the prize amount
389 let prizeAmountWithPowerup = basePrize * self.getPowerup()
390 let prizeDowngradeRatio = drawnResult!.nonJackpotDowngradeRatio
391 let basicPrize = prizeAmountWithPowerup * prizeDowngradeRatio
392 if prizeRank!.rawValue <= PrizeRank.THIRD.rawValue {
393 // 16% prize is service fee
394 return basicPrize * (1.0 - feeRatio)
395 } else {
396 return basicPrize
397 }
398 }
399 }
400
401 /// Borrow the lottery
402 ///
403 access(all)
404 view fun borrowLottery(): &Lottery {
405 let lotteryPool = self._borrowLotteryPool()
406 return lotteryPool.borrowLottery(self.lotteryId) ?? panic("Lottery not found")
407 }
408
409 /** Update Ticket Data */
410
411 access(contract)
412 fun setPowerup(powerup: UFix64) {
413 pre {
414 powerup >= 1.0: "Powerup must be greater than 0"
415 powerup <= 10.0: "Powerup must be less than or equal to 10"
416 powerup > self.powerup: "New powerup must be greater than the current powerup"
417 }
418 self.powerup = powerup
419
420 emit TicketPowerupChanged(
421 poolAddr: self.pool,
422 lotteryId: self.lotteryId,
423 address: self.getTicketOwner(),
424 ticketId: self.getTicketId(),
425 powerup: powerup
426 )
427 }
428
429 access(contract)
430 fun onPrizeVerify() {
431 if self.status != TicketStatus.ACTIVE {
432 return
433 }
434 // Verify the ticket numbers with the lottery result
435 let lotteryRef = self.borrowLottery()
436 let lotteryResult = lotteryRef.getResult()
437 if lotteryResult == nil {
438 return
439 }
440 // --- check the ticket numbers and set the status ---
441 let resultNumbers = lotteryResult!.numbers
442 // get all the matched white numbers
443 let matchedWhiteNumbers: [UInt8] = []
444 // Check the white numbers
445 for number in resultNumbers.white {
446 if self.numbers.white.contains(number) {
447 matchedWhiteNumbers.append(number)
448 }
449 }
450 // check if the red number is matched
451 let isRedMatched = self.numbers.red == resultNumbers.red
452
453 // Set the ticket status based on the matched numbers
454 if matchedWhiteNumbers.length == 5 && isRedMatched {
455 // Jackpot: 5 white numbers and 1 red number are matched
456 self._setStatus(toStatus: TicketStatus.WIN)
457 self.winPrizeRank = PrizeRank.JACKPOT
458 } else if matchedWhiteNumbers.length == 5 {
459 // Second: 5 white numbers are matched
460 self._setStatus(toStatus: TicketStatus.WIN)
461 self.winPrizeRank = PrizeRank.SECOND
462 } else if matchedWhiteNumbers.length == 4 && isRedMatched {
463 // Third: 4 white numbers and 1 red number are matched
464 self._setStatus(toStatus: TicketStatus.WIN)
465 self.winPrizeRank = PrizeRank.THIRD
466 } else if matchedWhiteNumbers.length == 4 || (matchedWhiteNumbers.length == 3 && isRedMatched) {
467 // Fourth: 4 white numbers are matched or 3 white numbers and 1 red number are matched
468 self._setStatus(toStatus: TicketStatus.WIN)
469 self.winPrizeRank = PrizeRank.FOURTH
470 } else if matchedWhiteNumbers.length == 3 || (matchedWhiteNumbers.length == 2 && isRedMatched) {
471 // Fifth: 3 white numbers or 2 white numbers and 1 red number are matched
472 self._setStatus(toStatus: TicketStatus.WIN)
473 self.winPrizeRank = PrizeRank.FIFTH
474 } else if isRedMatched {
475 // Sixth: at least 1 red number is matched
476 self._setStatus(toStatus: TicketStatus.WIN)
477 self.winPrizeRank = PrizeRank.SIXTH
478 } else {
479 // Lose: no number is matched
480 self._setStatus(toStatus: TicketStatus.LOSE)
481 }
482
483 // emit event
484 emit TicketVerified(
485 poolAddr: self.pool,
486 lotteryId: self.lotteryId,
487 address: self.getTicketOwner(),
488 ticketId: self.getTicketId(),
489 numbers: self.getNumbers(),
490 status: self.status.rawValue,
491 prizeRank: self.winPrizeRank?.rawValue ?? 0
492 )
493 }
494
495 access(contract)
496 fun onPrizeDisburse(_ prizeAmount: UFix64) {
497 // Only the ticket with WIN status and the prize rank can be disbursed
498 if self.status != TicketStatus.WIN {
499 return
500 }
501 if self.winPrizeRank == nil {
502 return
503 }
504
505 // Set the ticket status to WIN_DISBURSED
506 self._setStatus(toStatus: TicketStatus.WIN_DISBURSED)
507
508 // emit event
509 emit TicketPrizeDisbursed(
510 poolAddr: self.pool,
511 lotteryId: self.lotteryId,
512 address: self.getTicketOwner(),
513 ticketId: self.getTicketId(),
514 prizeAmount: prizeAmount
515 )
516 }
517
518 /** --- Internal Methods --- */
519
520 access(self)
521 view fun _borrowLotteryPool(): &LotteryPool {
522 return FGameLottery.borrowLotteryPool(self.pool)
523 ?? panic("Lottery pool not found")
524 }
525
526 access(self)
527 fun _setStatus(toStatus: TicketStatus) {
528 let oldStatus = self.status
529 self.status = toStatus
530
531 emit TicketStatusChanged(
532 poolAddr: self.pool,
533 lotteryId: self.lotteryId,
534 address: self.getTicketOwner(),
535 ticketId: self.getTicketId(),
536 fromStatus: oldStatus.rawValue,
537 toStatus: toStatus.rawValue
538 )
539 }
540 }
541
542 /// User's ticket collection resource interface
543 ///
544 access(all) resource interface TicketCollectionPublic {
545 // --- read methods ---
546 access(all)
547 view fun getIDs(): [UInt64]
548
549 access(all)
550 fun forEachID(_ f: fun (UInt64): Bool): Void
551
552 access(all)
553 fun slicedIDs(_ page: Int, _ size: Int): [UInt64]
554
555 access(all)
556 view fun getTicketAmount(): Int
557
558 access(all)
559 view fun borrowTicket(ticketId: UInt64): &TicketEntry?
560
561 // --- write methods ---
562 access(contract)
563 fun addTicket(_ ticket: @TicketEntry)
564 }
565
566 /// User's ticket collection
567 ///
568 access(all) resource TicketCollection: TicketCollectionPublic {
569 access(self)
570 let tickets: @{UInt64: TicketEntry}
571 access(self)
572 let dscSortedIDs: [UInt64]
573
574 init() {
575 self.tickets <- {}
576 self.dscSortedIDs = []
577 }
578
579 /** ---- Public Methods ---- */
580
581 /// Get the ticket IDs
582 ///
583 access(all)
584 view fun getIDs(): [UInt64] {
585 return self.dscSortedIDs
586 }
587
588 /// Allows a given function to iterate through the list
589 /// of owned NFT IDs in a collection without first
590 /// having to load the entire list into memory
591 access(all)
592 fun forEachID(_ f: fun (UInt64): Bool): Void {
593 self.tickets.forEachKey(f)
594 }
595
596 /// Get the sliced ticket IDs
597 ///
598 access(all)
599 fun slicedIDs(_ page: Int, _ size: Int): [UInt64] {
600 var startAt = page * size
601 let max = self.dscSortedIDs.length
602 if startAt >= max {
603 return []
604 }
605 var upTo = startAt + size
606 if upTo > max {
607 upTo = max
608 }
609 return self.dscSortedIDs.slice(from: startAt, upTo: upTo)
610 }
611
612 /// Get the ticket amount
613 ///
614 access(all)
615 view fun getTicketAmount(): Int {
616 return self.dscSortedIDs.length
617 }
618
619 /// Borrow a ticket from the collection
620 ///
621 access(all)
622 view fun borrowTicket(ticketId: UInt64): &TicketEntry? {
623 return &self.tickets[ticketId]
624 }
625
626 /** ---- Private Methods ---- */
627
628 /// Add a new ticket to the collection
629 ///
630 access(contract)
631 fun addTicket(_ ticket: @TicketEntry) {
632 pre {
633 self.owner != nil: "Only the collection with an owner can add a ticket"
634 self.tickets[ticket.getTicketId()] == nil: "Ticket already exists"
635 }
636 // Basic information
637 let ticketId = ticket.getTicketId()
638
639 self.tickets[ticketId] <-! ticket
640 // Add the ticket ID to the sorted array in descending order (newest first)
641 self.dscSortedIDs.insert(at: 0, ticketId)
642
643 let ref = self.borrowTicketRef(ticketId: ticketId)
644
645 emit TicketAdded(
646 poolAddr: ref.pool,
647 lotteryId: ref.lotteryId,
648 address: self.owner!.address,
649 ticketId: ticketId,
650 numbers: ref.getNumbers()
651 )
652 }
653
654 /** --- Internal Methods --- */
655
656 /// Borrow a ticket reference
657 ///
658 access(self)
659 view fun borrowTicketRef(ticketId: UInt64): &TicketEntry {
660 return &self.tickets[ticketId] as &TicketEntry? ?? panic("Ticket not found")
661 }
662 }
663
664 /// Ticket identifier struct
665 ///
666 access(all) struct TicketIdentifier {
667 access(all) let address: Address
668 access(all) let ticketId: UInt64
669
670 init(_ address: Address, _ ticketId: UInt64) {
671 self.address = address
672 self.ticketId = ticketId
673 }
674
675 /// Borrow the ticket entry
676 ///
677 access(all)
678 view fun borrowTicket(): &TicketEntry? {
679 let userCol = FGameLottery.getUserTicketCollection(self.address)
680 if let colRef = userCol.borrow() {
681 return colRef.borrowTicket(ticketId: self.ticketId)
682 }
683 return nil
684 }
685 }
686
687 /// Enum for the lottery status
688 ///
689 access(all) enum LotteryStatus: UInt8 {
690 access(all) case ACTIVE
691 access(all) case READY_TO_DRAW
692 access(all) case DRAWN
693 access(all) case DRAWN_AND_VERIFIED
694 }
695
696 /// Struct for the lottery result
697 ///
698 access(all) struct LotteryResult {
699 access(all) let numbers: TicketNumber
700 access(all) let totalBought: UFix64
701 access(all) var verifyingProgress: UFix64
702 access(all) var disbursingProgress: UFix64
703 access(all) let winners: {Address: UInt64}
704 access(all) var nonJackpotTotal: UFix64
705 access(all) var nonJackpotDowngradeRatio: UFix64
706 access(all) let nonJackpotWinners: {UInt8: UInt64}
707 access(all) var jackpotAmount: UFix64
708 access(all) var jackpotWinners: [Address]?
709
710 view init(
711 numbers: TicketNumber,
712 totalBought: UFix64,
713 jackpotAmount: UFix64,
714 ) {
715 self.numbers = numbers
716 self.totalBought = totalBought
717 self.winners = {}
718 self.jackpotAmount = jackpotAmount
719 self.jackpotWinners = nil
720 self.verifyingProgress = 0.0
721 self.disbursingProgress = 0.0
722 self.nonJackpotTotal = 0.0
723 self.nonJackpotDowngradeRatio = 1.0
724 self.nonJackpotWinners = {}
725 }
726
727 /// Add a winner
728 ///
729 access(contract)
730 fun addWinner(_ address: Address) {
731 self.winners[address] = (self.winners[address] ?? 0) + 1
732 }
733
734 /// Increment the non-jackpot total
735 ///
736 access(contract)
737 fun incrementNonJackpotTotal(_ rank: PrizeRank, _ amount: UFix64) {
738 self.nonJackpotWinners[rank.rawValue] = (self.nonJackpotWinners[rank.rawValue] ?? 0) + 1
739 self.nonJackpotTotal = self.nonJackpotTotal + amount
740 }
741
742 /// Set the non-jackpot downgraded ratio
743 ///
744 access(contract)
745 fun setNonJackpotDowngradeRatio(_ ratio: UFix64) {
746 if ratio >= 0.0 && ratio <= 1.0 {
747 self.nonJackpotDowngradeRatio = ratio
748 }
749 }
750
751 /// Set the jackpot info
752 ///
753 access(contract)
754 fun updateJackpot(_ amount: UFix64) {
755 self.jackpotAmount = amount
756 }
757
758 /// Add a jackpot winner
759 ///
760 access(contract)
761 fun addJackpotWinner(_ address: Address) {
762 if self.jackpotWinners == nil {
763 self.jackpotWinners = [address]
764 } else {
765 self.jackpotWinners?.append(address)!
766 }
767 }
768
769 /// Set the distribution progress
770 ///
771 access(contract)
772 fun setVerifyingProgress(_ progress: UFix64) {
773 if progress >= 0.0 && progress <= 1.0 {
774 self.verifyingProgress = progress
775 }
776 }
777
778 access(contract)
779 fun setDisbursingProgress(_ progress: UFix64) {
780 if progress >= 0.0 && progress <= 1.0 {
781 self.disbursingProgress = progress
782 }
783 }
784 }
785
786 /// Struct for the lottery info
787 ///
788 access(all) struct LotteryBasicInfo {
789 access(all) let epochIndex: UInt64
790 access(all) let epochStartAt: UFix64
791 access(all) let currentPool: UFix64
792 access(all) let participantsAmount: UInt64
793 access(all) let status: LotteryStatus
794 access(all) let disbursing: Bool
795
796 view init(
797 epochIndex: UInt64,
798 epochStartAt: UFix64,
799 status: LotteryStatus,
800 disbursing: Bool,
801 currentPool: UFix64,
802 participantsAmt: UInt64
803 ) {
804 self.epochIndex = epochIndex
805 self.epochStartAt = epochStartAt
806 self.status = status
807 self.disbursing = disbursing
808 self.currentPool = currentPool
809 self.participantsAmount = participantsAmt
810 }
811 }
812
813 /// Lottery public resource interface
814 ///
815 access(all) resource interface LotteryPublic {
816 /// Lottery info - public view
817 access(all)
818 view fun getInfo(): LotteryBasicInfo
819 /// Lottery status
820 access(all)
821 view fun getStatus(): LotteryStatus
822 /// Lotter result
823 access(all)
824 view fun getResult(): LotteryResult?
825 /// Return the participant addresses
826 access(all)
827 view fun getParticipants(): [Address]
828 /// Return the participant amount
829 access(all)
830 view fun getParticipantAmount(): UInt64
831 /// Get the total bought balance
832 access(all)
833 view fun getCurrentLotteryBalance(): UFix64
834 /// Check if the lottery is disbursing prizes
835 access(all)
836 view fun isDisbursing(): Bool
837 }
838
839 /// Lottery resource
840 ///
841 access(all) resource Lottery: LotteryPublic {
842 /// Lottery epoch index
843 access(all)
844 let epochIndex: UInt64
845 /// Lottery epoch start time
846 access(all)
847 let epochStartAt: UFix64
848 /// Lottery total bought
849 access(self)
850 let current: @FRC20FTShared.Change
851 /// Participants tickets: [Address: [TicketID]]
852 access(self)
853 let participants: {Address: [UInt64]}
854 /// Lottery final status
855 access(self)
856 var drawnResult: LotteryResult?
857 /// Lottery draw checker queue
858 access(self)
859 var checkingQueue: [Address]?
860 /// Lottery winners
861 access(self)
862 let disbursingQueque: [TicketIdentifier]
863
864 init(
865 epochIndex: UInt64,
866 jackpotPoolRef: &FRC20FTShared.Change,
867 ) {
868 self.epochIndex = epochIndex
869 self.epochStartAt = getCurrentBlock().timestamp
870 self.participants = {}
871 self.disbursingQueque = []
872 self.drawnResult = nil
873 self.checkingQueue = nil
874 // Set the current pool
875 let tick = jackpotPoolRef.getOriginalTick()
876 self.current <- FRC20FTShared.createEmptyChange(tick: tick, from: jackpotPoolRef.from)
877 }
878
879 /** ---- Public Methods ---- */
880
881 /// Lottery info - public view
882 ///
883 access(all)
884 view fun getInfo(): LotteryBasicInfo {
885 return LotteryBasicInfo(
886 epochIndex: self.epochIndex,
887 epochStartAt: self.epochStartAt,
888 status: self.getStatus(),
889 disbursing: self.isDisbursing(),
890 currentPool: self.getCurrentLotteryBalance(),
891 participantsAmt: self.getParticipantAmount()
892 )
893 }
894
895 /// Get the lottery status
896 ///
897 access(all)
898 view fun getStatus(): LotteryStatus {
899 let now = getCurrentBlock().timestamp
900 let poolRef = self.borrowLotteryPool()
901 let interval = poolRef.getEpochInterval()
902 let epochCloseTime = self.epochStartAt + interval
903 if now < epochCloseTime {
904 return LotteryStatus.ACTIVE
905 } else if self.drawnResult == nil {
906 return LotteryStatus.READY_TO_DRAW
907 } else if self.checkingQueue == nil || self.checkingQueue!.length > 0 {
908 return LotteryStatus.DRAWN
909 } else {
910 return LotteryStatus.DRAWN_AND_VERIFIED
911 }
912 }
913
914 /// Lotter result
915 ///
916 access(all)
917 view fun getResult(): LotteryResult? {
918 return self.drawnResult
919 }
920
921 /// Return the participant addresses
922 ///
923 access(all)
924 view fun getParticipants(): [Address] {
925 return self.participants.keys
926 }
927
928 /// Return the participant amount
929 ///
930 access(all)
931 view fun getParticipantAmount(): UInt64 {
932 return UInt64(self.participants.length)
933 }
934
935 /// Get the total bought balance
936 ///
937 access(all)
938 view fun getCurrentLotteryBalance(): UFix64 {
939 return self.current.getBalance()
940 }
941
942 /// Check if the lottery is disbursing prizes
943 ///
944 access(all)
945 view fun isDisbursing(): Bool {
946 let status = self.getStatus()
947 return status == LotteryStatus.DRAWN_AND_VERIFIED && self.disbursingQueque.length > 0
948 }
949
950 /** ---- Contract level Methods ----- */
951
952 /// Create a new ticket and add it to user's collection
953 ///
954 access(contract)
955 fun buyNewTicket(
956 payment: @FRC20FTShared.Change,
957 recipient: Capability<&TicketCollection>,
958 powerup: UFix64? // default is 1.0, you can increase the powerup to increase the winning amount
959 ): UInt64 {
960 pre {
961 self.getStatus() == LotteryStatus.ACTIVE: "The lottery is not active"
962 }
963 post {
964 self.getCurrentLotteryBalance() == before(self.getCurrentLotteryBalance()) + before(payment.getBalance()):
965 "The payment is not deposited to the lottery"
966 }
967
968 // deposit the payment to the total bought
969 let ref = self.borrowCurrentLotteryChange()
970 ref.forceMerge(from: <- payment)
971
972 // Create a new ticket
973 let collection = recipient.borrow() ?? panic("Recipient not found")
974 let ticket <- create TicketEntry(
975 pool: self.borrowLotteryPool().getAddress(),
976 lotteryId: self.epochIndex,
977 powerup: powerup,
978 numbers: nil
979 )
980 let ticketId = ticket.getTicketId()
981 // Add the ticket to the collection
982 collection.addTicket(<- ticket)
983
984 // Add the ticket to the participants
985 if self.participants[recipient.address] == nil {
986 self.participants[recipient.address] = [ticketId]
987 } else {
988 self.participants[recipient.address]?.append(ticketId)!
989 }
990
991 return ticketId
992 }
993
994 /// Generate random number to draw the lottery
995 ///
996 access(contract)
997 fun drawLottery() {
998 // Only execute the method if the lottery is ready to draw
999 if self.getStatus() != LotteryStatus.READY_TO_DRAW {
1000 return
1001 }
1002 // borrow the lottery pool
1003 let pool = self.borrowLotteryPool()
1004
1005 // Generate the random numbers
1006 let lotteryNumbers = FGameLottery.createRandomTicketNumber()
1007 let jackpotAmount = pool.getJackpotPoolBalance()
1008 let participantAmount = self.getParticipantAmount()
1009 let totalBought = self.current.getBalance()
1010
1011 self.drawnResult = LotteryResult(
1012 numbers: lotteryNumbers,
1013 totalBought: totalBought,
1014 jackpotAmount: jackpotAmount,
1015 )
1016 // set the verifying progress to 1.0 if there is no participant
1017 if participantAmount == 0 {
1018 self.drawnResult?.setVerifyingProgress(1.0)!
1019 self.drawnResult?.setDisbursingProgress(1.0)!
1020 }
1021 self.checkingQueue = self.participants.keys
1022
1023 // emit event
1024 emit LotteryDrawn(
1025 poolAddr: pool.getAddress(),
1026 lotteryId: self.epochIndex,
1027 numbers: lotteryNumbers.getNumbers(),
1028 participantAmount: participantAmount,
1029 totalBought: totalBought,
1030 jackpotAmount: jackpotAmount,
1031 )
1032 }
1033
1034 /// Claim the winning amount
1035 ///
1036 access(contract)
1037 fun verifyParticipantsTickets(_ maxEntries: Int) {
1038 // This method is used to verify the participants' tickets
1039 // only execute the method if the lottery is drawn and the checking queue is not empty
1040 if self.getStatus() != LotteryStatus.DRAWN {
1041 return
1042 }
1043 if self.checkingQueue == nil || self.checkingQueue!.length == 0 {
1044 return
1045 }
1046 if self.drawnResult == nil {
1047 return
1048 }
1049
1050 // borrow the lottery pool
1051 let pool = self.borrowLotteryPool()
1052
1053 // variables
1054 let participants: [Address] = []
1055 var winnersCnt: UInt64 = 0
1056 var nonJackpotAmount = 0.0
1057 // get the checking addresses
1058 var i = 0
1059 while i < maxEntries && self.checkingQueue!.length > 0 {
1060 // remove the first address from the queue
1061 let addr = self.checkingQueue!.removeFirst()
1062 participants.append(addr)
1063 // user collection
1064 let userColCap = FGameLottery.getUserTicketCollection(addr)
1065 var checkedEntries = 1
1066 if let userColRef = userColCap.borrow() {
1067 // retrieve the participant tickets
1068 if let ticketsRef = &self.participants[addr] as auth(Mutate) &[UInt64]? {
1069 while ticketsRef.length > 0 && checkedEntries < maxEntries {
1070 let ticketId = ticketsRef.removeFirst()
1071 if let ticketRef = userColRef.borrowTicket(ticketId: ticketId) {
1072 // verify the ticket
1073 ticketRef.onPrizeVerify()
1074 // if the ticket is a winner, update prize amount by rank
1075 if let prizeRank = ticketRef.getWinPrizeRank() {
1076 // update the winner count
1077 winnersCnt = winnersCnt + 1
1078 // add the winner to the result
1079 self.drawnResult?.addWinner(addr)!
1080 // add the ticket to the disbursing queue
1081 self.disbursingQueque.append(TicketIdentifier(addr, ticketId))
1082 // update result data
1083 if prizeRank == PrizeRank.JACKPOT {
1084 self.drawnResult?.addJackpotWinner(addr)!
1085 // emit event
1086 emit LotteryJackpotWinnerUpdated(
1087 poolAddr: pool.getAddress(),
1088 lotteryId: self.epochIndex,
1089 winner: addr
1090 )
1091 } else {
1092 let basePrize = pool.getWinnerPrizeByRank(prizeRank)
1093 let powerup = ticketRef.getPowerup()
1094 let prize = basePrize * powerup
1095 nonJackpotAmount = nonJackpotAmount + prize
1096 self.drawnResult?.incrementNonJackpotTotal(prizeRank, prize)!
1097 }
1098 } // end if prizeRank
1099 } // end if ticketRef
1100 // one entry is checked
1101 checkedEntries = checkedEntries + 1
1102 }
1103 // if there are remaining tickets, add addr back to the queue
1104 if ticketsRef.length > 0 {
1105 self.checkingQueue!.append(addr)
1106 }
1107 }
1108 } // end if userColRef
1109 i = i + checkedEntries
1110 }
1111
1112 // update the distribution progress
1113 let totalParticipants = self.getParticipantAmount()
1114 let remainingUnChecked = UInt64(self.checkingQueue!.length)
1115 let progress = UFix64(totalParticipants - remainingUnChecked) / UFix64(totalParticipants)
1116 self.drawnResult?.setVerifyingProgress(progress)!
1117
1118 // emit event
1119 emit LotteryParticipantsVerified(
1120 poolAddr: pool.getAddress(),
1121 lotteryId: self.epochIndex,
1122 participants: participants,
1123 winners: winnersCnt,
1124 nonJackpotTotal: nonJackpotAmount
1125 )
1126
1127 // if the checking queue is empty, finalize the jackpot
1128 if remainingUnChecked == 0 {
1129 let nonJackpotTotal = self.drawnResult?.nonJackpotTotal ?? 0.0
1130 let totalBought = self.drawnResult?.totalBought ?? 0.0
1131
1132 let jackpotRef = pool.borrowJackpotPool()
1133 let oldJackpotAmount = jackpotRef.getBalance()
1134 // min new jackpot amount is 50% of the total bought
1135 let minNewJackpotAmount = totalBought * 0.5
1136
1137 var ratio = 1.0
1138 var newJackpotAmount = 0.0
1139 if minNewJackpotAmount >= nonJackpotTotal ||
1140 (totalBought >= nonJackpotTotal && (oldJackpotAmount + totalBought).saturatingSubtract(nonJackpotTotal) >= minNewJackpotAmount) {
1141 // no need to downgrade the non-jackpot prize
1142 let restAmount = totalBought - nonJackpotTotal
1143 newJackpotAmount = oldJackpotAmount + restAmount
1144 // finalize the jackpot
1145 self.drawnResult?.updateJackpot(newJackpotAmount)!
1146
1147 // deposit the new added amount to the jackpot pool
1148 jackpotRef.forceMerge(from: <- self.current.withdrawAsChange(amount: restAmount))
1149 // all remaining non-jackpot prize will be added to the jackpot
1150 } else if totalBought < nonJackpotTotal && (oldJackpotAmount + totalBought).saturatingSubtract(nonJackpotTotal) >= minNewJackpotAmount {
1151 // withdraw from the jackpot pool to cover the non-jackpot prize
1152 let requiredAmount = nonJackpotTotal - totalBought
1153 let newJackpotAmount = oldJackpotAmount - requiredAmount
1154 // finalize the jackpot
1155 self.drawnResult?.updateJackpot(newJackpotAmount)!
1156
1157 // withdraw the required amount from the jackpot pool
1158 let change <- jackpotRef.withdrawAsChange(amount: requiredAmount)
1159
1160 // deposit the required amount to the total bought
1161 let currRef = self.borrowCurrentLotteryChange()
1162 currRef.forceMerge(from: <- change)
1163 } else {
1164 // ensure the jackpot amount is at least 50% of the total bought
1165 if minNewJackpotAmount > oldJackpotAmount {
1166 let jackpotRequired = minNewJackpotAmount - oldJackpotAmount
1167 // withdraw the required amount from the current pool to ensure the jackpot
1168 let change <- self.current.withdrawAsChange(amount: jackpotRequired)
1169 // deposit the required amount to the jackpot pool
1170 jackpotRef.forceMerge(from: <- change)
1171 } else if minNewJackpotAmount < oldJackpotAmount {
1172 let jackpotRest = oldJackpotAmount - minNewJackpotAmount
1173 // withdraw the rest amount from the jackpot pool
1174 let change <- jackpotRef.withdrawAsChange(amount: jackpotRest)
1175 // deposit the rest amount to the current pool
1176 let currRef = self.borrowCurrentLotteryChange()
1177 currRef.forceMerge(from: <- change)
1178 }
1179 newJackpotAmount = minNewJackpotAmount
1180 // finalize the jackpot, the new jackpot amount is the min new jackpot amount
1181 self.drawnResult?.updateJackpot(minNewJackpotAmount)!
1182
1183 // calculate the non-jackpot downgrade ratio
1184 if nonJackpotTotal > 0.0 {
1185 let currentPoolBalance = self.current.getBalance()
1186 ratio = currentPoolBalance / nonJackpotTotal
1187 // finalize the non-jackpot prize
1188 self.drawnResult?.setNonJackpotDowngradeRatio(ratio)!
1189 }
1190 }
1191
1192 // emit event
1193 emit LotteryJackpotFinialized(
1194 poolAddr: pool.getAddress(),
1195 lotteryId: self.epochIndex,
1196 jackpotAmount: newJackpotAmount,
1197 nonJackpotTotal: nonJackpotTotal,
1198 nonJackpotDowngradeRatio: ratio
1199 )
1200 }
1201 }
1202
1203 /// Disburse the prizes to the winners
1204 ///
1205 access(contract)
1206 fun disbursePrizes(_ maxEntries: Int) {
1207 // This method is used to disburse the prizes to the winners
1208 if !self.isDisbursing() {
1209 return
1210 }
1211
1212 // variables
1213 var i = 0
1214 while i < maxEntries && self.disbursingQueque.length > 0 {
1215 let ticketIdentifier = self.disbursingQueque.removeFirst()
1216 if let ticketRef = ticketIdentifier.borrowTicket() {
1217 self._disbursePrize(ticket: ticketRef)
1218 }
1219 i = i + 1
1220 }
1221
1222 // update the disbursing progress
1223 let totalWinners = self.drawnResult?.winners?.keys?.length ?? 0
1224 let remainingWinners = self.disbursingQueque.length
1225 if totalWinners > 0 {
1226 let progress = UFix64(totalWinners - remainingWinners) / UFix64(totalWinners)
1227 self.drawnResult?.setDisbursingProgress(progress)!
1228 }
1229
1230 // emit event
1231 if self.disbursingQueque.length == 0 {
1232 let pool = self.borrowLotteryPool()
1233 emit LotteryPrizesDisbursed(
1234 poolAddr: pool.getAddress(),
1235 lotteryId: self.epochIndex
1236 )
1237 }
1238 }
1239
1240 /** ---- Internal Methods ----- */
1241
1242 /// Disburse the prize to the winners
1243 ///
1244 access(self)
1245 fun _disbursePrize(ticket: &TicketEntry) {
1246 // Only status is DRAWN_AND_VERIFIED and the ticket status is WIN can withdraw the prize
1247 if self.getStatus() != LotteryStatus.DRAWN_AND_VERIFIED {
1248 return
1249 }
1250 let ticketStatus = ticket.getStatus()
1251 if ticketStatus != TicketStatus.WIN {
1252 return
1253 }
1254 let prizeRank = ticket.getWinPrizeRank()
1255 if prizeRank == nil {
1256 return
1257 }
1258 // Borrow the FRC20 indexer
1259 let frc20Indexer = FRC20Indexer.getIndexer()
1260 // Borrow the lottery pool
1261 let pool = self.borrowLotteryPool()
1262
1263 // Disburse the prize to the ticket owner
1264 let ticketOwner = ticket.getTicketOwner()
1265
1266 // Initialize the reward change
1267 let rewardChange <- FRC20FTShared.createEmptyChange(
1268 tick: self.current.getOriginalTick(),
1269 from: ticketOwner
1270 )
1271 // Initialize the fee change
1272 let feeChange: @FRC20FTShared.Change <- FRC20FTShared.createEmptyChange(
1273 tick: self.current.getOriginalTick(),
1274 from: pool.getAddress()
1275 )
1276 // Get the fee ratio of the pool
1277 let feeRatio = pool.getServiceFee()
1278
1279 // Get the prize amount
1280 if prizeRank! == PrizeRank.JACKPOT {
1281 // Disburse the jackpot prize
1282 let jackpotPoolRef = pool.borrowJackpotPool()
1283 let winners = self.drawnResult?.jackpotWinners!
1284 if winners == nil || winners.length == 0 || !winners.contains(ticketOwner) {
1285 Burner.burn(<- rewardChange)
1286 Burner.burn(<- feeChange)
1287 return
1288 }
1289 let jackpotAmount = self.drawnResult?.jackpotAmount!
1290 let jackpotWinnerAmt = winners.length
1291 let withdrawAmount = jackpotAmount / UFix64(jackpotWinnerAmt)
1292 if jackpotPoolRef.getBalance() < withdrawAmount {
1293 Burner.burn(<- rewardChange)
1294 Burner.burn(<- feeChange)
1295 return
1296 }
1297 let prizeChange <- jackpotPoolRef.withdrawAsChange(amount: withdrawAmount)
1298
1299 // 16% (default) of prize will be charged as the service fee
1300 let serviceFee = prizeChange.getBalance() * feeRatio
1301 feeChange.forceMerge(from: <- prizeChange.withdrawAsChange(amount: serviceFee))
1302
1303 // Deposit the prize to the ticket owner
1304 rewardChange.forceMerge(from: <- prizeChange)
1305 } else {
1306 // Get the base prize amount
1307 let basePrize = pool.getWinnerPrizeByRank(prizeRank!)
1308
1309 let currentBalance = self.current.getBalance()
1310 // Disburse the prize amount
1311 let prizeAmountWithPowerup = basePrize * ticket.getPowerup()
1312 let prizeDowngradeRatio = self.drawnResult?.nonJackpotDowngradeRatio!
1313 // re-calculate the prize amount with the downgrade ratio
1314 var amountToDisburse = prizeAmountWithPowerup * prizeDowngradeRatio
1315 if prizeDowngradeRatio == 1.0 && currentBalance < amountToDisburse {
1316 // Fix the issue of no enough balance to disburse the prize
1317 var halfBought = self.drawnResult?.totalBought! * 0.5
1318 if halfBought > currentBalance {
1319 halfBought = currentBalance
1320 }
1321 var newPrizeDowngradeRatio = halfBought / self.drawnResult?.nonJackpotTotal!
1322 if newPrizeDowngradeRatio > 1.0 {
1323 newPrizeDowngradeRatio = 1.0
1324 }
1325 amountToDisburse = prizeAmountWithPowerup * newPrizeDowngradeRatio
1326 }
1327 let prizeChange <- self.current.withdrawAsChange(amount: amountToDisburse)
1328
1329 // if PrizeRank is 3rd or higher, 16% of prize will be charged as the service fee
1330 // if PrizeRank is lower than 3rd, no service fee will be charged
1331 if prizeRank!.rawValue <= PrizeRank.THIRD.rawValue {
1332 let serviceFee = prizeChange.getBalance() * feeRatio
1333 feeChange.forceMerge(from: <- prizeChange.withdrawAsChange(amount: serviceFee))
1334 }
1335
1336 // Deposit the prize to the ticket owner
1337 rewardChange.forceMerge(from: <- prizeChange)
1338 }
1339
1340 // deposit the fee to the service pools
1341 if feeChange.getBalance() > 0.0 {
1342 let feeTickName = feeChange.getOriginalTick()
1343 let totalFeeAmount = feeChange.getBalance()
1344 // Borrow the FRC20 accounts pool
1345 let acctsPool = FRC20AccountsPool.borrowAccountsPool()
1346 let globalSharedStore = FRC20FTShared.borrowGlobalStoreRef()
1347 let stakingFRC20Tick = FRC20FTShared.getPlatformStakingTickerName()
1348
1349 if feeTickName == "" {
1350 // this is $FLOW token
1351 let serviceFee = totalFeeAmount * 0.5
1352 // 50% of service fee will be deposited to the platform pool
1353 let serviceFeeVault <- feeChange.withdrawAsVault(amount: serviceFee)
1354 let frc20Indexer = FRC20Indexer.getIndexer()
1355 let platformFlowRecipient = frc20Indexer.borowPlatformTreasuryReceiver()
1356 platformFlowRecipient.deposit(from: <- serviceFeeVault)
1357 // 50% of service fee will be deposited to the shared pool
1358 let sharedFeeVault <- feeChange.extractAsVault()
1359 if let flowsStakingRecipient = acctsPool.borrowFRC20StakingFlowTokenReceiver(tick: stakingFRC20Tick) {
1360 flowsStakingRecipient.deposit(from: <- sharedFeeVault)
1361 } else {
1362 platformFlowRecipient.deposit(from: <- sharedFeeVault)
1363 }
1364 } else {
1365 // For non-Flow Token, all service fee will be deposited to the Staking shared pool
1366 if let stakingAddress = acctsPool.getFRC20StakingAddress(tick: stakingFRC20Tick) {
1367 if let stakingPool = FRC20Staking.borrowPool(stakingAddress) {
1368 if let rewardStrategy = stakingPool.borrowRewardStrategy(feeTickName) {
1369 // Donate the tokens
1370 rewardStrategy.addIncome(income: <- feeChange.withdrawAsChange(amount: totalFeeAmount))
1371 }
1372 }
1373 }
1374 if feeChange.getBalance() > 0.0 {
1375 // return to lottery pool address or pool's FT Vault
1376 // If no FT Receiver exists, the Fee Vault will be burned
1377 frc20Indexer.returnChange(change: <- feeChange.withdrawAsChange(amount: totalFeeAmount))
1378 }
1379 }
1380 }
1381 // zero balance destroy
1382 Burner.burn(<- feeChange)
1383
1384 // Get the prize amount
1385 let prizeAmount = rewardChange.getBalance()
1386 // deposit the prize to the ticket owner
1387 frc20Indexer.returnChange(change: <- rewardChange)
1388
1389 // Update the ticket status to WIN_DISBURSED and emit event
1390 ticket.onPrizeDisburse(prizeAmount)
1391 }
1392
1393 access(contract)
1394 view fun borrowCurrentLotteryChange(): auth(FRC20FTShared.Write) &FRC20FTShared.Change {
1395 return &self.current
1396 }
1397
1398 access(self)
1399 view fun borrowLotteryPool(): &LotteryPool {
1400 let ownerAddr = self.owner?.address ?? panic("Owner is missing")
1401 return FGameLottery.borrowLotteryPool(ownerAddr)
1402 ?? panic("Lottery pool not found")
1403 }
1404 }
1405
1406 access(all) resource interface LotteryPoolPublic {
1407 // --- read methods ---
1408 access(all)
1409 view fun getName(): String
1410 access(all)
1411 view fun getAddress(): Address
1412 access(all)
1413 view fun getCurrentEpochIndex(): UInt64
1414 access(all)
1415 view fun getEpochInterval(): UFix64
1416 access(all)
1417 view fun getLotteryToken(): String
1418 access(all)
1419 view fun getTicketPrice(): UFix64
1420 access(all)
1421 view fun getJackpotPoolBalance(): UFix64
1422 access(all)
1423 view fun getPoolTotalBalance(): UFix64
1424 access(all)
1425 view fun getServiceFee(): UFix64
1426 access(all)
1427 view fun isEpochAutoStart(): Bool
1428 access(all)
1429 view fun getWinnerPrizeByRank(_ rank: PrizeRank): UFix64
1430
1431 // --- read methods: default implement ---
1432
1433 /// Check if the current lottery is active
1434 access(all)
1435 view fun isCurrentLotteryActive(): Bool {
1436 let currentLotteryRef = self.borrowCurrentLottery()
1437 return currentLotteryRef != nil && currentLotteryRef!.getStatus() == LotteryStatus.ACTIVE
1438 }
1439
1440 /// Check if the current lottery is ready to draw
1441 access(all)
1442 view fun isCurrentLotteryReadyToDraw(): Bool {
1443 let currentLotteryRef = self.borrowCurrentLottery()
1444 let status = currentLotteryRef?.getStatus()
1445 return currentLotteryRef != nil && status == LotteryStatus.READY_TO_DRAW
1446 }
1447
1448 /// Check if the current lottery is finished
1449 access(all)
1450 view fun isCurrentLotteryFinished(): Bool {
1451 let currentLotteryRef = self.borrowCurrentLottery()
1452 let status = currentLotteryRef?.getStatus()
1453 return currentLotteryRef == nil || status == LotteryStatus.DRAWN || status == LotteryStatus.DRAWN_AND_VERIFIED
1454 }
1455
1456 // --- write methods ---
1457
1458 /// Buy lottery tickets
1459 access(all)
1460 fun buyTickets(
1461 payment: @FRC20FTShared.Change,
1462 amount: UInt64,
1463 powerup: UFix64?,
1464 recipient: Capability<&TicketCollection>,
1465 )
1466
1467 /// Donate to the jackpot pool
1468 access(all)
1469 fun donateToJackpot(
1470 payment: @FRC20FTShared.Change
1471 )
1472
1473 // --- borrow methods ---
1474
1475 access(all)
1476 view fun borrowLottery(_ epochIndex: UInt64): &Lottery?
1477 access(all)
1478 view fun borrowCurrentLottery(): &Lottery?
1479 }
1480
1481 access(all) resource interface LotteryPoolAdmin {
1482 // --- write methods ---
1483 access(Admin)
1484 fun startNewEpoch()
1485 }
1486
1487 /// Lottery pool resource
1488 ///
1489 access(all) resource LotteryPool: LotteryPoolPublic, LotteryPoolAdmin, FixesHeartbeat.IHeartbeatHook {
1490 /// Lottery pool constants
1491 access(all)
1492 let name: String
1493 access(self)
1494 let initEpochInterval: UFix64
1495 access(self)
1496 let initTicketPrice: UFix64
1497 // Lottery pool variables
1498 access(self)
1499 let jackpotPool: @FRC20FTShared.Change
1500 access(self)
1501 let lotteries: @{UInt64: Lottery}
1502 access(self)
1503 var currentEpochIndex: UInt64
1504 access(self)
1505 var finishedEpoches: [UInt64]
1506 access(self)
1507 var lastSealedEpochIndex: UInt64?
1508
1509 init(
1510 name: String,
1511 rewardTick: String,
1512 ticketPrice: UFix64,
1513 epochInterval: UFix64
1514 ) {
1515 pre {
1516 ticketPrice > 0.0: "Ticket price must be greater than 0"
1517 epochInterval > 0.0: "Epoch interval must be greater than 0"
1518 }
1519 self.name = name
1520 let accountAddr = FGameLottery.account.address
1521 self.jackpotPool <- FRC20FTShared.createEmptyChange(tick: rewardTick, from: accountAddr)
1522 self.initTicketPrice = ticketPrice
1523 self.initEpochInterval = epochInterval
1524 self.currentEpochIndex = 0
1525 self.finishedEpoches = []
1526 self.lastSealedEpochIndex = nil
1527 self.lotteries <- {}
1528 }
1529
1530 /** ---- Public Methods ---- */
1531
1532 access(all)
1533 view fun getName(): String {
1534 return self.name
1535 }
1536
1537 access(all)
1538 view fun getAddress(): Address {
1539 return self.owner?.address ?? panic("Owner is missing")
1540 }
1541
1542 access(all)
1543 view fun getCurrentEpochIndex(): UInt64 {
1544 return self.currentEpochIndex
1545 }
1546
1547 access(all)
1548 view fun getEpochInterval(): UFix64 {
1549 let store = self.borrowConfigStore()
1550 let interval = store.getByEnum(FRC20FTShared.ConfigType.GameLotteryEpochInterval) as! UFix64?
1551 return interval ?? self.initEpochInterval
1552 }
1553
1554 access(all)
1555 view fun getTicketPrice(): UFix64 {
1556 let store = self.borrowConfigStore()
1557 let price = store.getByEnum(FRC20FTShared.ConfigType.GameLotteryTicketPrice) as! UFix64?
1558 return price ?? self.initTicketPrice
1559 }
1560
1561 access(all)
1562 view fun getServiceFee(): UFix64 {
1563 let store = self.borrowConfigStore()
1564 let fee = store.getByEnum(FRC20FTShared.ConfigType.GameLotteryServiceFee) as! UFix64?
1565 return fee ?? 0.16
1566 }
1567
1568 access(all)
1569 view fun isEpochAutoStart(): Bool {
1570 let store = self.borrowConfigStore()
1571 let autoStart = store.getByEnum(FRC20FTShared.ConfigType.GameLotteryAutoStart) as! Bool?
1572 return autoStart ?? true
1573 }
1574
1575 access(all)
1576 view fun getLotteryToken(): String {
1577 return self.jackpotPool.getOriginalTick()
1578 }
1579
1580 /// Returns the current Jackpot pool balance
1581 ///
1582 access(all)
1583 view fun getJackpotPoolBalance(): UFix64 {
1584 return self.jackpotPool.getBalance()
1585 }
1586
1587 /// Returna the total balance of the pool
1588 ///
1589 access(all)
1590 view fun getPoolTotalBalance(): UFix64 {
1591 var totalBalance = self.jackpotPool.getBalance()
1592 let epochIndex = self.currentEpochIndex
1593 if let lotteryRef = self.borrowLottery(epochIndex) {
1594 totalBalance = totalBalance + lotteryRef.getCurrentLotteryBalance()
1595 }
1596 return totalBalance
1597 }
1598
1599 access(all)
1600 view fun getWinnerPrizeByRank(_ rank: PrizeRank): UFix64 {
1601 // Get the ticket price
1602 let ticketPrice = self.getTicketPrice()
1603 // Calculate the winner prize
1604 var prize: UFix64 = 0.0
1605 switch rank {
1606 case PrizeRank.JACKPOT:
1607 prize = self.jackpotPool.getBalance()
1608 break
1609 case PrizeRank.SECOND:
1610 prize = ticketPrice * 50000.0
1611 break
1612 case PrizeRank.THIRD:
1613 prize = ticketPrice * 5000.0
1614 break
1615 case PrizeRank.FOURTH:
1616 prize = ticketPrice * 25.0
1617 break
1618 case PrizeRank.FIFTH:
1619 prize = ticketPrice * 4.0
1620 break
1621 case PrizeRank.SIXTH:
1622 prize = ticketPrice * 2.0
1623 break
1624 }
1625 return prize
1626 }
1627
1628 access(all)
1629 view fun borrowLottery(_ epochIndex: UInt64): &Lottery? {
1630 return &self.lotteries[epochIndex]
1631 }
1632
1633 access(all)
1634 view fun borrowCurrentLottery(): &Lottery? {
1635 return self.borrowLottery(self.currentEpochIndex)
1636 }
1637
1638 /** ---- Public Methods: write ----- */
1639
1640 /// Buy lottery tickets
1641 access(all)
1642 fun buyTickets(
1643 payment: @FRC20FTShared.Change,
1644 amount: UInt64,
1645 powerup: UFix64?,
1646 recipient: Capability<&TicketCollection>,
1647 ) {
1648 pre {
1649 amount > 0: "Amount must be greater than 0"
1650 payment.getOriginalTick() == self.jackpotPool.getOriginalTick()
1651 : "Invalid payment token, expected: ".concat(self.jackpotPool.getOriginalTick())
1652 .concat(", got: ").concat(payment.getOriginalTick())
1653 self.isCurrentLotteryActive(): "The current lottery is not active"
1654 }
1655
1656 // Ensure the payment is enough
1657 let price = self.getTicketPrice()
1658 let oneTicketCost = price * (powerup ?? 1.0)
1659 let totalCost = oneTicketCost * UFix64(amount)
1660 assert(
1661 payment.getBalance() == totalCost,
1662 message: "Payment balance should be equal to the total cost"
1663 )
1664
1665 // Get the current lottery
1666 let lotteryRef = self.borrowLottery(self.currentEpochIndex)
1667 ?? panic("Lottery not found")
1668
1669 // Create tickets
1670 let purchasedIds: [UInt64] = []
1671 var i: UInt64 = 0
1672 while i < amount {
1673 // Withdraw the payment
1674 let one <- payment.withdrawAsChange(amount: oneTicketCost)
1675 // Create a new ticket
1676 let newTicketId = lotteryRef.buyNewTicket(
1677 payment: <- one,
1678 recipient: recipient,
1679 powerup: powerup,
1680 )
1681 purchasedIds.append(newTicketId)
1682 i = i + 1
1683 }
1684
1685 assert(
1686 payment.getBalance() == 0.0,
1687 message: "The payment balance should be 0 after the purchase"
1688 )
1689 Burner.burn(<- payment)
1690
1691 // emit event
1692 emit TicketPurchased(
1693 poolAddr: self.getAddress(),
1694 lotteryId: self.currentEpochIndex,
1695 address: recipient.address,
1696 ticketIds: purchasedIds,
1697 costTick: self.jackpotPool.getOriginalTick(),
1698 costAmount: totalCost
1699 )
1700 }
1701
1702 /// Donate to the jackpot pool
1703 ///
1704 access(all)
1705 fun donateToJackpot(
1706 payment: @FRC20FTShared.Change
1707 ) {
1708 pre {
1709 payment.getOriginalTick() == self.jackpotPool.getOriginalTick(): "Invalid payment token"
1710 payment.getBalance() > 0.0: "Payment balance must be greater than 0"
1711 }
1712
1713 let jackpotRef = self.borrowJackpotPool()
1714 let donatableAmount = payment.getBalance()
1715
1716 // deposit the new added amount to the jackpot pool
1717 jackpotRef.forceMerge(from: <- payment)
1718
1719 emit LotteryJackpotDonated(
1720 poolAddr: self.getAddress(),
1721 donationAmount: donatableAmount
1722 )
1723 }
1724
1725 /** ---- Admin Methods ----- */
1726
1727 /// Start a new epoch
1728 ///
1729 access(Admin)
1730 fun startNewEpoch() {
1731 pre {
1732 self.isCurrentLotteryFinished(): "The current lottery is not finished"
1733 }
1734
1735 // Create a new lottery
1736 let newEpochIndex = self.currentEpochIndex + 1
1737 let newLottery <- create Lottery(
1738 epochIndex: newEpochIndex,
1739 jackpotPoolRef: self.borrowJackpotPool()
1740 )
1741
1742 let startedAt = newLottery.epochStartAt
1743
1744 // Save the new lottery
1745 self.lotteries[newEpochIndex] <-! newLottery
1746 self.currentEpochIndex = newEpochIndex
1747
1748 // emit event
1749 emit LotteryStarted(
1750 poolAddr: self.getAddress(),
1751 lotteryId: newEpochIndex,
1752 startTime: startedAt
1753 )
1754 }
1755
1756 /// This is a hotfix method to gather the jackpot pool from all lotteries
1757 access(Admin)
1758 fun gatherJackpotPool() {
1759 let jackpotRef = self.borrowJackpotPool()
1760
1761 let selfRef = &self as auth(Admin) &LotteryPool
1762 self.lotteries.forEachKey(fun (epochIndex: UInt64): Bool {
1763 if let ref = selfRef.borrowLottery(epochIndex) {
1764 if ref.getStatus() == LotteryStatus.DRAWN_AND_VERIFIED && !ref.isDisbursing() {
1765 let currentBalance = ref.getCurrentLotteryBalance()
1766 if currentBalance > 0.0 {
1767 let currPool = ref.borrowCurrentLotteryChange()
1768 jackpotRef.forceMerge(from: <- currPool.withdrawAsChange(amount: currentBalance))
1769 }
1770 }
1771 }
1772 return true
1773 })
1774 }
1775
1776 /** ---- Heartbeat Implementation Methods ----- */
1777
1778 /// The methods that is invoked when the heartbeat is executed
1779 /// Before try-catch is deployed, please ensure that there will be no panic inside the method.
1780 ///
1781 access(account)
1782 fun onHeartbeat(_ deltaTime: UFix64) {
1783 // Step 0. Handle the current lottery
1784
1785 // Active or ready to draw is one step
1786 if self.isCurrentLotteryActive() {
1787 // DO NOTHING if the current lottery is active
1788 } else if self.isCurrentLotteryReadyToDraw() {
1789 // Draw the current lottery if it is ready
1790 let lotteryRef = self.borrowLottery(self.currentEpochIndex)!
1791 // draw the lottery
1792 lotteryRef.drawLottery()
1793 // append the current epoch index to the finished epoches
1794 self.finishedEpoches.append(self.currentEpochIndex)
1795 }
1796 // Check if the current lottery is finished, and start a new epoch if the auto start is enabled
1797 // This is the second step, because the current lottery may be finished after the draw
1798 if self.isCurrentLotteryFinished() {
1799 // if the current lottery is finished
1800 // Start a new epoch if the auto start is enabled
1801 if self.isEpochAutoStart() {
1802 self.startNewEpoch()
1803 }
1804 }
1805
1806 // Step 1. Handle the finished epoches
1807 if self.finishedEpoches.length > 0 {
1808 let firstFinsihedEpochIndex = self.finishedEpoches[0]
1809 let lotteryRef = self.borrowLottery(firstFinsihedEpochIndex)!
1810
1811 // max entries to compute in one heartbeat
1812 let heartbeatComputeEntries = 200
1813
1814 // verify the participants' tickets
1815 var status = lotteryRef.getStatus()
1816 // we need to verify the participants' tickets if the lottery is drawn
1817 if status == LotteryStatus.DRAWN {
1818 // verify the participants' tickets
1819 lotteryRef.verifyParticipantsTickets(heartbeatComputeEntries)
1820 } else if status == LotteryStatus.DRAWN_AND_VERIFIED && lotteryRef.isDisbursing() {
1821 // disburse the prizes to the winners
1822 lotteryRef.disbursePrizes(heartbeatComputeEntries)
1823 }
1824
1825 // check if the lottery is finished
1826 status = lotteryRef.getStatus()
1827 if status == LotteryStatus.DRAWN_AND_VERIFIED && !lotteryRef.isDisbursing() {
1828 // move all remaining balance to the jackpot pool
1829 let currentBalance = lotteryRef.getCurrentLotteryBalance()
1830 if currentBalance > 0.0 {
1831 let jackpotRef = self.borrowJackpotPool()
1832 let currRef = lotteryRef.borrowCurrentLotteryChange()
1833 jackpotRef.forceMerge(from: <- currRef.withdrawAsChange(amount: currentBalance))
1834 }
1835 // remove the first finished epoch index and set the last sealed epoch index
1836 self.lastSealedEpochIndex = self.finishedEpoches.removeFirst()
1837 }
1838 }
1839 }
1840
1841 // --- Internal Methods ---
1842
1843 access(contract)
1844 view fun borrowJackpotPool(): auth(FRC20FTShared.Write) &FRC20FTShared.Change {
1845 return &self.jackpotPool
1846 }
1847
1848 access(self)
1849 view fun borrowConfigStore(): &FRC20FTShared.SharedStore {
1850 return FRC20FTShared.borrowStoreRef(self.owner!.address)
1851 ?? panic("Config store not found")
1852 }
1853 }
1854
1855 /* --- Account Access Methods --- */
1856
1857 /// Create a new lottery pool
1858 ///
1859 access(account)
1860 fun createLotteryPool(
1861 name: String,
1862 rewardTick: String,
1863 ticketPrice: UFix64,
1864 epochInterval: UFix64
1865 ): @LotteryPool {
1866 return <-create LotteryPool(
1867 name: name,
1868 rewardTick: rewardTick,
1869 ticketPrice: ticketPrice,
1870 epochInterval: epochInterval
1871 )
1872 }
1873
1874 /* --- Public methods --- */
1875
1876 /// Create a ticket collection
1877 ///
1878 access(all)
1879 fun createTicketCollection(): @TicketCollection {
1880 return <-create TicketCollection()
1881 }
1882
1883 /// Get the user's ticket collection capability
1884 ///
1885 access(all)
1886 view fun getUserTicketCollection(
1887 _ addr: Address
1888 ): Capability<&TicketCollection> {
1889 return getAccount(addr)
1890 .capabilities.get<&TicketCollection>(self.userCollectionPublicPath)
1891 }
1892
1893 /// Borrow Lottery Pool
1894 ///
1895 access(all)
1896 view fun borrowLotteryPool(_ addr: Address): &LotteryPool? {
1897 return getAccount(addr)
1898 .capabilities.get<&LotteryPool>(FGameLottery.lotteryPoolPublicPath)
1899 .borrow()
1900 }
1901
1902 init() {
1903 // Set the maximum white and red numbers
1904 self.MAX_WHITE_NUMBER = 33
1905 self.MAX_RED_NUMBER = 16
1906
1907 // Identifiers
1908 let identifier = "FGameLottery_".concat(self.account.address.toString())
1909 self.userCollectionStoragePath = StoragePath(identifier: identifier.concat("_UserCollection"))!
1910 self.userCollectionPublicPath = PublicPath(identifier: identifier.concat("_UserCollection"))!
1911
1912 self.lotteryPoolStoragePath = StoragePath(identifier: identifier.concat("_LotteryPool"))!
1913 self.lotteryPoolPublicPath = PublicPath(identifier: identifier.concat("_LotteryPool"))!
1914
1915 // Emit the ContractInitialized event
1916 emit ContractInitialized()
1917 }
1918}
1919