Smart Contract

FGameLottery

A.d2abb5dbf5e08666.FGameLottery

Valid From

86,128,992

Deployed

3d ago
Feb 24, 2026, 11:54:28 PM UTC

Dependents

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