Smart Contract

FRC20Votes

A.d2abb5dbf5e08666.FRC20Votes

Valid From

86,129,063

Deployed

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

Dependents

3 imports
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# FRC20Votes
5
6This contract is used to manage the FRC20 votes.
7
8*/
9import MetadataViews from 0x1d7e57aa55817448
10import ViewResolver from 0x1d7e57aa55817448
11import NonFungibleToken from 0x1d7e57aa55817448
12import Burner from 0xf233dcee88fe0abe
13// Fixes Imports
14import Fixes from 0xd2abb5dbf5e08666
15import FixesHeartbeat from 0xd2abb5dbf5e08666
16import FRC20Indexer from 0xd2abb5dbf5e08666
17import FRC20FTShared from 0xd2abb5dbf5e08666
18import FRC20AccountsPool from 0xd2abb5dbf5e08666
19import FRC20SemiNFT from 0xd2abb5dbf5e08666
20import FRC20Staking from 0xd2abb5dbf5e08666
21import FRC20StakingManager from 0xd2abb5dbf5e08666
22import FRC20VoteCommands from 0xd2abb5dbf5e08666
23
24access(all) contract FRC20Votes {
25
26    access(all) entitlement Admin
27
28    /* --- Events --- */
29    /// Event emitted when the contract is initialized
30    access(all) event ContractInitialized()
31
32    /// Event emitted when a proposal is created
33    access(all) event ProposalCreated(
34        proposer: Address,
35        tick: String,
36        proposalId: UInt64,
37        title: String,
38        description: String,
39        discussionLink: String?,
40        executableThreshold: UFix64,
41        beginningTime: UFix64,
42        endingTime: UFix64,
43        slotCommands: [UInt8],
44        slotMessages: [String],
45    )
46    /// Event emitted when a proposal status is changed
47    access(all) event ProposalStatusChanged(
48        proposalId: UInt64,
49        tick: String,
50        prevStatus: UInt8?,
51        newStatus: UInt8,
52        timestamp: UFix64
53    )
54    /// Event emitted when a proposal is updated
55    access(all) event ProposalInfoUpdated(
56        proposalId: UInt64,
57        tick: String,
58        title: String?,
59        description: String?,
60        discussionLink: String?
61    )
62    /// Event emitted when a proposal is cancelled
63    access(all) event ProposalCancelled(
64        proposalId: UInt64,
65        tick: String
66    )
67
68    /// Event emitted when a proposal is voted
69    access(all) event ProposalVoted(
70        tick: String,
71        proposalId: UInt64,
72        voter: Address,
73        choice: Int,
74        points: UFix64,
75    )
76
77    /// Event emitted when a proposal is failed
78    access(all) event ProposalFailed(
79        tick: String,
80        proposalId: UInt64,
81        votedPoints: UFix64,
82        failedAt: UFix64
83    )
84
85    /// Event emitted when a proposal is executed
86    access(all) event ProposalExecuted(
87        tick: String,
88        proposalId: UInt64,
89        choice: Int,
90        executedAt: UFix64
91    )
92
93    /// Event emitted when the voter whitelist is updated
94    access(all) event VotesManagerWhitelistUpdated(
95        voter: Address,
96        isWhitelisted: Bool
97    )
98
99    /* --- Variable, Enums and Structs --- */
100
101    access(all)
102    let VoterStoragePath: StoragePath
103    access(all)
104    let VoterPublicPath: PublicPath
105    access(all)
106    let FRC20VotesManagerStoragePath: StoragePath
107    access(all)
108    let FRC20VotesManagerPublicPath: PublicPath
109
110    /* --- Interfaces & Resources --- */
111
112    /// The Proposal status.
113    ///
114    access(all) enum ProposalStatus: UInt8 {
115        access(all) case Created;
116        access(all) case Activated;
117        access(all) case Cancelled;
118        access(all) case Failed;
119        access(all) case Successed;
120        access(all) case Executed;
121    }
122
123    access(all) resource interface VoterPublic {
124        /// ---- Read: contract level ----
125        access(all)
126        view fun getVoterAddress(): Address
127        /// Get the voting power.
128        access(all)
129        view fun getVotingPower(): UFix64
130        /// Check whether the proposal is voted.
131        access(all)
132        view fun hasVoted(_ proposalId: UInt64): Bool
133        /// Get the voted points.
134        access(all)
135        view fun getVotedPoints(_ proposalId: UInt64): UFix64?
136        /// Get the voted choice.
137        access(all)
138        view fun getVotedChoice(_ proposalId: UInt64): Int?
139        /// Get the voted proposals.
140        access(all)
141        view fun getVotedProposals(tick: String): [UInt64]
142        /// ---- Write: contract level ----
143        access(contract)
144        fun onVote(choice: Int, proposal: &Proposal)
145        access(contract)
146        fun onProposalFinalized(proposal: &Proposal)
147    }
148
149    /// The resource of the FixesVotes voter identifier.
150    ///
151    access(all) resource VoterIdentity: VoterPublic, FRC20SemiNFT.FRC20SemiNFTCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic {
152        access(self)
153        let semiNFTColCap: Capability<auth(NonFungibleToken.Update, NonFungibleToken.Withdraw) &FRC20SemiNFT.Collection>
154        access(self)
155        let lockedSemiNFTCollection: @FRC20SemiNFT.Collection
156        // ----- Voting Info ----
157        /// Voted proposal id -> Points
158        access(self)
159        let voted: {UInt64: UFix64}
160        /// ProposalID -> Choice ID
161        access(self)
162        let votedChoices: {UInt64: Int}
163        /// Tick -> ProposalId[]
164        access(self)
165        let votedTicksMapping: {String: [UInt64]}
166        /// ProposalID -> EndedAt
167        access(self)
168        let activeProposals: {UInt64: UFix64}
169
170        init(
171            _ cap: Capability<auth(NonFungibleToken.Update, NonFungibleToken.Withdraw) &FRC20SemiNFT.Collection>
172        ) {
173            pre {
174                cap.check(): "The capability is invalid"
175            }
176            self.semiNFTColCap = cap
177            self.lockedSemiNFTCollection <- (FRC20SemiNFT.createEmptyCollection(nftType: Type<@FRC20SemiNFT.NFT>()) as! @FRC20SemiNFT.Collection)
178            self.voted = {}
179            self.votedChoices = {}
180            self.votedTicksMapping = {}
181            self.activeProposals = {}
182        }
183
184        /** ----- Implement NFT Standard ----- */
185
186        access(all)
187        fun deposit(token: @{NonFungibleToken.NFT}) {
188            self.lockedSemiNFTCollection.deposit(token: <- token)
189        }
190
191        access(NonFungibleToken.Withdraw)
192        fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
193            return <- self.lockedSemiNFTCollection.withdraw(withdrawID: withdrawID)
194        }
195
196        access(all)
197        view fun getSupportedNFTTypes(): {Type: Bool} {
198            return self.lockedSemiNFTCollection.getSupportedNFTTypes()
199        }
200
201        access(all)
202        view fun isSupportedNFTType(type: Type): Bool {
203            return self.lockedSemiNFTCollection.isSupportedNFTType(type: type)
204        }
205
206        access(all)
207        view fun getIDs(): [UInt64] {
208            return self.lockedSemiNFTCollection.getIDs()
209        }
210
211        access(all)
212        view fun getLength(): Int {
213            return self.lockedSemiNFTCollection.getLength()
214        }
215
216        access(all)
217        fun forEachID(_ f: fun (UInt64): Bool): Void {
218            self.lockedSemiNFTCollection.forEachID(f)
219        }
220
221        access(all)
222        view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
223            return self.lockedSemiNFTCollection.borrowNFT(id)
224        }
225
226        access(all)
227        view fun getIDsByTick(tick: String): [UInt64] {
228            return self.lockedSemiNFTCollection.getIDsByTick(tick: tick)
229        }
230
231        access(all)
232        view fun getStakedBalance(tick: String): UFix64 {
233            return self.lockedSemiNFTCollection.getStakedBalance(tick: tick)
234        }
235
236        access(all)
237        view fun borrowFRC20SemiNFTPublic(id: UInt64): &FRC20SemiNFT.NFT? {
238            return self.lockedSemiNFTCollection.borrowFRC20SemiNFTPublic(id: id)
239        }
240
241        /** ----- Read ----- */
242
243        /// Get the voter address.
244        ///
245        access(all)
246        view fun getVoterAddress(): Address {
247            return self.owner?.address ?? panic("Voter's owner is not found")
248        }
249
250        /// Get the voting power.
251        ///
252        access(all)
253        view fun getVotingPower(): UFix64 {
254            let stakeTick = FRC20FTShared.getPlatformStakingTickerName()
255            let selfAddr = self.getVoterAddress()
256
257            var power = 0.0
258            if let semiColRef = self.semiNFTColCap.borrow() {
259                // Get the staking pool address
260                power = semiColRef.getStakedBalance(tick: stakeTick)
261            }
262            return power + self.getStakedBalance(tick: stakeTick)
263        }
264
265        /// Check whether the proposal is voted.
266        ///
267        access(all)
268        view fun hasVoted(_ proposalId: UInt64): Bool {
269            return self.voted[proposalId] != nil
270        }
271
272        /// Get the voted points.
273        ///
274        access(all)
275        view fun getVotedPoints(_ proposalId: UInt64): UFix64? {
276            return self.voted[proposalId]
277        }
278
279        /// Get the voted choice.
280        ///
281        access(all)
282        view fun getVotedChoice(_ proposalId: UInt64): Int? {
283            return self.votedChoices[proposalId]
284        }
285
286        /// Get the voted proposals.
287        ///
288        access(all)
289        view fun getVotedProposals(tick: String): [UInt64] {
290            if let voted = self.votedTicksMapping[tick] {
291                return voted
292            } else {
293                return []
294            }
295        }
296
297        /** ----- Write ----- */
298
299        access(contract)
300        fun onVote(choice: Int, proposal: &Proposal) {
301            pre {
302                !self.hasVoted(proposal.uuid): "Proposal is already voted"
303            }
304            post {
305                self.hasVoted(proposal.uuid): "Proposal is not voted"
306            }
307            let details = proposal.getDetails()
308
309            // check the staked balance
310            let stakeTick = FRC20FTShared.getPlatformStakingTickerName()
311
312            let stakedNFTColRef = self.semiNFTColCap.borrow() ?? panic("The staked NFT collection is not found")
313            let unlockingStakedBalance = stakedNFTColRef.getStakedBalance(tick: stakeTick)
314            if unlockingStakedBalance > 0.0 {
315                // move the staked NFTs to the locked collection
316                let ids = stakedNFTColRef.getIDsByTick(tick: stakeTick)
317                for id in ids {
318                    self.lockedSemiNFTCollection.deposit(token: <- stakedNFTColRef.withdraw(withdrawID: id))
319                }
320            }
321
322            // check staked balance
323            let votingPower = self.getVotingPower()
324            assert(
325                votingPower > 0.0,
326                message: "The voting power is zero"
327            )
328            log("Voting Power:".concat(votingPower.toString()))
329
330            // vote on the proposal
331            let lockedNFTIds = self.lockedSemiNFTCollection.getIDsByTick(tick: stakeTick)
332            log("Locked NFTs Amount:".concat(lockedNFTIds.length.toString()))
333            var voted = false
334            for id in lockedNFTIds {
335                log("Vote on the proposal, ID:".concat(id.toString()))
336                let semiNFT = self.lockedSemiNFTCollection.borrowFRC20SemiNFTPublic(id: id)
337                    ?? panic("The semiNFT is not found")
338                if !proposal.isVoted(semiNFT) {
339                    proposal.vote(choice: choice, semiNFT: semiNFT)
340                    voted = true
341                }
342            }
343            assert(voted, message: "You have not voted on the proposal")
344
345            // update the local voting status
346            self.voted[proposal.uuid] = votingPower
347            self.votedChoices[proposal.uuid] = choice
348            if self.votedTicksMapping[details.tick] == nil {
349                self.votedTicksMapping[details.tick] = [proposal.uuid]
350            } else {
351                self.votedTicksMapping[details.tick]?.append(proposal.uuid)
352            }
353            // add the proposal to the locking queue
354            self.activeProposals[proposal.uuid] = details.endingTime
355        }
356
357        access(contract)
358        fun onProposalFinalized(proposal: &Proposal) {
359            if !proposal.isFinalized() {
360                return
361            }
362            // remove the proposal from the locking queue
363            self.activeProposals.remove(key: proposal.uuid)
364
365            // check is no more active proposals
366            if self.activeProposals.length == 0 {
367                // return all the staked NFTs to the staked collection
368                let stakeTick = FRC20FTShared.getPlatformStakingTickerName()
369                let lockedNFTIds = self.lockedSemiNFTCollection.getIDsByTick(tick: stakeTick)
370                if let semiNFTColRef = self.semiNFTColCap.borrow() {
371                    for id in lockedNFTIds {
372                        semiNFTColRef.deposit(token: <- self.lockedSemiNFTCollection.withdraw(withdrawID: id))
373                    }
374                }
375            }
376        }
377    }
378    /// The struct of the FixesVotes proposal choice slot details.
379    ///
380    access(all) struct ChoiceSlotDetails {
381        access(all)
382        let message: String
383        access(all)
384        let command: {FRC20VoteCommands.IVoteCommand}
385
386        view init(
387            message: String,
388            command: {FRC20VoteCommands.IVoteCommand},
389        ) {
390            self.message = message
391            self.command = command
392        }
393    }
394
395    /// The struct of the FixesVotes proposal details.
396    ///
397    access(all) struct ProposalDetails {
398        access(all)
399        let proposer: Address
400        access(all)
401        let tick: String
402        access(all)
403        let slots: [ChoiceSlotDetails]
404        access(all)
405        let beginningTime: UFix64
406        access(all)
407        let endingTime: UFix64
408        access(all)
409        let executableThreshold: UFix64
410        // ------ vars ------
411        access(all)
412        var title: String
413        access(all)
414        var description: String
415        access(all)
416        var discussionLink: String?
417        access(all)
418        var isCancelled: Bool
419
420        view init(
421            proposer: Address,
422            tick: String,
423            title: String,
424            description: String,
425            discussionLink: String?,
426            executableThreshold: UFix64,
427            beginningTime: UFix64,
428            endingTime: UFix64,
429            slots: [ChoiceSlotDetails]
430        ) {
431            pre {
432                slots.length > 0: "Slots must be greater than 0"
433                executableThreshold >= 0.1 && executableThreshold <= 0.8: "Executable threshold must be between 0.1 and 0.8"
434                beginningTime < endingTime: "Beginning time must be less than ending time"
435            }
436            self.proposer = proposer
437            self.tick = tick
438            self.title = title
439            self.description = description
440            self.discussionLink = discussionLink
441            self.slots = slots
442            self.beginningTime = beginningTime
443            self.endingTime = endingTime
444            self.executableThreshold = executableThreshold
445            self.isCancelled = false
446        }
447
448        /** ----- Read ----- */
449
450        access(all)
451        view fun isStarted(): Bool {
452            return self.beginningTime <= getCurrentBlock().timestamp
453        }
454
455        /// The Proposal is ended if the endAt is not nil.
456        ///
457        access(all)
458        view fun isEnded(): Bool {
459            return self.endingTime <= getCurrentBlock().timestamp
460        }
461
462        /** ----- Write ----- */
463
464        access(contract)
465        fun updateProposal(title: String?, description: String?, discussionLink: String?) {
466            if title != nil {
467                self.title = title!
468            }
469            if description != nil {
470                self.description = description!
471            }
472            if discussionLink != nil {
473                self.discussionLink = discussionLink!
474            }
475        }
476
477        access(contract)
478        fun cancelProposal() {
479            pre {
480                self.isCancelled == false: "Proposal is already cancelled"
481            }
482            self.isCancelled = true
483        }
484    }
485
486    access(all) struct StatusLog {
487        access(all)
488        let status: ProposalStatus
489        access(all)
490        let timestamp: UFix64
491
492        view init(
493            _ status: ProposalStatus,
494            _ timestamp: UFix64
495        ) {
496            self.status = status
497            self.timestamp = timestamp
498        }
499    }
500
501    access(all) resource interface ProposalPublic {
502        // --- Read Methods ---
503        access(all)
504        view fun getProposer(): Address
505        access(all)
506        view fun isEditable(): Bool
507        access(all)
508        view fun isFinalized(): Bool
509        access(all)
510        view fun getStatus(): ProposalStatus
511        access(all)
512        view fun getDetails(): ProposalDetails
513        access(all)
514        view fun getLogs(): [StatusLog]
515        access(all)
516        view fun getVotersAmount(): Int
517        access(all)
518        view fun getVoters(): [Address]
519        access(all)
520        view fun getTotalVotedPoints(): UFix64
521        access(all)
522        view fun getVotingChoices(): {Int: UFix64}
523        access(all)
524        view fun getWinningChoice(): Int?
525        access(all)
526        view fun isValidateForThreshold(): Bool
527        access(all)
528        view fun isVotingAllowed(): Bool
529        access(all)
530        view fun isVoteCommandsExecutable(): Bool
531        access(all)
532        view fun isWinningInscriptionsExecuted(): Bool
533        access(all)
534        view fun isChoiceInscriptionsExtracted(choice: Int): Bool
535        access(all)
536        view fun isVoted(_ semiNFT: &FRC20SemiNFT.NFT): Bool
537        // --- Write Methods ---
538        access(contract)
539        fun vote(choice: Int, semiNFT: &FRC20SemiNFT.NFT)
540    }
541
542    /// The struct of the FixesVotes proposal.
543    ///
544    access(all) resource Proposal: ProposalPublic, FixesHeartbeat.IHeartbeatHook {
545        access(self)
546        let proposer: Address
547        access(self)
548        let statusLog: [StatusLog]
549        access(self)
550        let details: ProposalDetails
551        // ----- Voting status -----
552        /// Address -> Points
553        access(self)
554        var votedAccounts: {Address: UFix64}
555        /// NFTId -> Bool
556        access(self)
557        var votedNFTs: {UInt64: Bool}
558        /// Vote choice -> Points
559        access(self)
560        var votes: {Int: UFix64}
561
562        init(
563            voter: Address,
564            tick: String,
565            title: String,
566            description: String,
567            discussionLink: String?,
568            executableThreshold: UFix64,
569            beginningTime: UFix64,
570            endingTime: UFix64,
571            slots: [ChoiceSlotDetails],
572        ) {
573            self.proposer = voter
574            self.statusLog = [StatusLog(ProposalStatus.Created, getCurrentBlock().timestamp)]
575            self.details = ProposalDetails(
576                proposer: voter,
577                tick: tick,
578                title: title,
579                description: description,
580                discussionLink: discussionLink,
581                executableThreshold: executableThreshold,
582                beginningTime: beginningTime,
583                endingTime: endingTime,
584                slots: slots,
585            )
586            self.votedAccounts = {}
587            self.votedNFTs = {}
588            // init votes
589            self.votes = {}
590
591            let messages: [String] = []
592            let commands: [UInt8] = []
593            var i = 0
594            while i < slots.length {
595                self.votes[i] = 0.0
596                messages.append(slots[i].message)
597                commands.append(slots[i].command.getCommandType().rawValue)
598                i = i + 1
599            }
600
601            emit ProposalCreated(
602                proposer: voter,
603                tick: tick,
604                proposalId: self.uuid,
605                title: title,
606                description: description,
607                discussionLink: discussionLink,
608                executableThreshold: executableThreshold,
609                beginningTime: beginningTime,
610                endingTime: endingTime,
611                slotCommands: commands,
612                slotMessages: messages
613            )
614        }
615
616        /** ------ Public Methods ------ */
617
618        access(all)
619        view fun getProposer(): Address {
620            return self.proposer
621        }
622
623        access(all)
624        view fun isEditable(): Bool {
625            let status = self.getStatus()
626            return status == ProposalStatus.Created || status == ProposalStatus.Activated ||
627                (status == ProposalStatus.Successed && !self.isWinningInscriptionsExecuted())
628        }
629
630        access(all)
631        view fun isFinalized(): Bool {
632            let status = self.getStatus()
633            return status == ProposalStatus.Failed || status == ProposalStatus.Cancelled || status == ProposalStatus.Executed
634        }
635
636        /// Get current status.
637        ///
638        access(all)
639        view fun getStatus(): ProposalStatus {
640            let now = getCurrentBlock().timestamp
641            if self.details.isCancelled {
642                return ProposalStatus.Cancelled
643            } else if now < self.details.beginningTime {
644                return ProposalStatus.Created
645            } else if now < self.details.endingTime {
646                return ProposalStatus.Activated
647            } else {
648                // ended
649                if !self.isValidateForThreshold() {
650                    return ProposalStatus.Failed
651                } else if !self.isWinningInscriptionsExecuted() {
652                    return ProposalStatus.Successed
653                } else {
654                    return ProposalStatus.Executed
655                }
656            }
657        }
658
659        access(all)
660        view fun getDetails(): ProposalDetails {
661            return self.details
662        }
663
664        access(all)
665        view fun getLogs(): [StatusLog] {
666            return self.statusLog
667        }
668
669        /// Get the voters amount.
670        ///
671        access(all)
672        view fun getVotersAmount(): Int {
673            return self.votedAccounts.length
674        }
675
676        /// Get the voters.
677        ///
678        access(all)
679        view fun getVoters(): [Address] {
680            return self.votedAccounts.keys
681        }
682
683        /// Get the total voted points.
684        ///
685        access(all)
686        view fun getTotalVotedPoints(): UFix64 {
687            var total = 0.0
688            for k in self.votes.keys {
689                total = total + self.votes[k]!
690            }
691            return total
692        }
693
694        /// Get the voting choices.
695        ///
696        access(all)
697        view fun getVotingChoices(): {Int: UFix64} {
698            return self.votes
699        }
700
701        /// Get the winning choice.
702        ///
703        access(all)
704        view fun getWinningChoice(): Int? {
705            if !self.details.isEnded() {
706                return nil
707            }
708            // get the winning choice
709            var winningChoice = 0
710            var winningVotes = 0.0
711            for k in self.votes.keys {
712                if let points = self.votes[k] {
713                    if points > winningVotes {
714                        winningChoice = k
715                        winningVotes = points
716                    }
717                }
718            }
719            return winningChoice
720        }
721
722        /// WHether the proposal is validate for the threshold.
723        ///
724        access(all)
725        view fun isValidateForThreshold(): Bool {
726            if !self.details.isEnded() {
727                return false
728            }
729            let totalStaked = FRC20Votes.getTotalStakedAmount()
730            let votedAmount = self.getTotalVotedPoints()
731            return totalStaked * self.details.executableThreshold <= votedAmount
732        }
733
734        /// Check whether the inscriptions are all executed
735        ///
736        access(all)
737        view fun isWinningInscriptionsExecuted(): Bool {
738            if !self.details.isEnded() {
739                return false
740            }
741            if let winningChoice = self.getWinningChoice() {
742                return self.isChoiceInscriptionsExtracted(choice: winningChoice)
743            }
744            return false
745        }
746
747        /// check whether the choice inscriptions are all executed
748        ///
749        access(all)
750        view fun isChoiceInscriptionsExtracted(choice: Int): Bool {
751            let slotInfoRef = self.details.slots[choice]
752            return slotInfoRef.command.isAllInscriptionsExtracted()
753        }
754
755        /// Check whether the voting is allowed.
756        ///
757        access(all)
758        view fun isVotingAllowed(): Bool {
759            return self.details.isStarted() && !self.details.isEnded()
760        }
761
762        /// Check whether the proposal is executable.
763        ///
764        access(all)
765        view fun isVoteCommandsExecutable(): Bool {
766            if self.getStatus() != ProposalStatus.Successed {
767                return false
768            }
769            var allExecutable = true
770            for i, slot in self.details.slots {
771                allExecutable = allExecutable && !self.isChoiceInscriptionsExtracted(choice: i)
772                if !allExecutable {
773                    break
774                }
775            }
776            return allExecutable
777        }
778
779        /// Check whether the NFT is voted.
780        ///
781        access(all)
782        view fun isVoted(_ semiNFT: &FRC20SemiNFT.NFT): Bool {
783            return self.votedNFTs[semiNFT.id] != nil
784        }
785
786        /** ------ Write Methods: Contract ------ */
787
788        /// Vote on a proposal.
789        ///
790        access(contract)
791        fun vote(choice: Int, semiNFT: &FRC20SemiNFT.NFT) {
792            pre {
793                self.isVotingAllowed(): "Voting is not allowed for now"
794                choice < self.details.slots.length: "Choice is out of range"
795                semiNFT.getOriginalTick() == FRC20FTShared.getPlatformStakingTickerName(): "The ticker is not the staking ticker"
796                semiNFT.isStakedTick(): "The ticker is not staked"
797                semiNFT.getBalance() > 0.0: "The NFT balance is zero"
798                self.votedNFTs[semiNFT.id] == nil: "NFT is already voted"
799            }
800            post {
801                self.votes[choice]! == before(self.votes[choice]!) + semiNFT.getBalance(): "Votes are not added"
802                self.votedNFTs[semiNFT.id] == true: "NFT is not added to the votedNFTs"
803            }
804
805            let points = semiNFT.getBalance()
806            self.votes[choice] = (self.votes[choice] ?? 0.0) + points
807            // add the voter
808            let voterAddr = semiNFT.owner?.address ?? panic("Voter's owner is not found")
809            self.votedAccounts[voterAddr] = (self.votedAccounts[voterAddr] ?? 0.0) + points
810            self.votedNFTs[semiNFT.id] = true
811
812            emit ProposalVoted(
813                tick: self.details.tick,
814                proposalId: self.uuid,
815                voter: semiNFT.owner?.address ?? panic("Voter's owner is not found"),
816                choice: choice,
817                points: semiNFT.getBalance()
818            )
819        }
820
821        /// Execute the proposal without panic
822        ///
823        access(contract)
824        fun executeSafe(): Bool {
825            if self.getStatus() != ProposalStatus.Successed {
826                return false
827            }
828
829            if let winningChoice = self.getWinningChoice() {
830                for i, slot in self.details.slots {
831                    var result = false
832                    // execute the winning choice
833                    if i == winningChoice {
834                        // run the commands
835                        result = slot.command.safeRunVoteCommands()
836                    } else {
837                        // refund the unexecuted inscriptions
838                        result = slot.command.refundFailedVoteCommands(receiver: self.proposer)
839                    }
840                    if !result {
841                        return false
842                    }
843                }
844
845                // emit event
846                emit ProposalExecuted(
847                    tick: self.details.tick,
848                    proposalId: self.uuid,
849                    choice: winningChoice,
850                    executedAt: getCurrentBlock().timestamp
851                )
852                return true
853            }
854            return false
855        }
856
857        /// Execute the proposal.
858        ///
859        access(contract)
860        fun onExecute() {
861            pre {
862                self.getStatus() == ProposalStatus.Successed: "Proposal is not successed"
863            }
864            let result = self.executeSafe()
865            assert(result, message: "The proposal is not executed")
866        }
867
868        /// Refund the inscriptions cost without panic
869        ///
870        access(contract)
871        fun refundInscriptionCostSafe(): Bool {
872            let status = self.getStatus()
873            if status == ProposalStatus.Successed || status == ProposalStatus.Executed {
874                return false
875            }
876
877            for i, slot in self.details.slots {
878                // refund the unexecuted inscriptions
879                let result = slot.command.refundFailedVoteCommands(receiver: self.proposer)
880                if !result {
881                    return false
882                }
883            }
884            return true
885        }
886
887        /// Refund the inscriptions cost.
888        ///
889        access(contract)
890        fun refundInscriptionCost() {
891            let result = self.refundInscriptionCostSafe()
892            assert(result, message: "The proposal cannot be not refunded")
893        }
894
895        /** ------ Internal Methods: Implement Heartbeat ------ */
896
897        /// Implement the heartbeat hook.
898        ///
899        access(account)
900        fun onHeartbeat(_ deltaTime: UFix64) {
901            let status = self.getStatus()
902            let lastIdx = self.statusLog.length - 1
903            let lastStatus = lastIdx >= 0
904                ? self.statusLog[lastIdx].status
905                : nil
906
907            if status != lastStatus {
908                let now = getCurrentBlock().timestamp
909                self.statusLog.append(StatusLog(status, now))
910
911                // emit event
912                emit ProposalStatusChanged(
913                    proposalId: self.uuid,
914                    tick: self.details.tick,
915                    prevStatus: lastStatus?.rawValue,
916                    newStatus: status.rawValue,
917                    timestamp: now
918                )
919
920                // check the status and do the action
921                switch status {
922                case ProposalStatus.Failed:
923                    self.refundInscriptionCostSafe()
924                    // emit event
925                    emit ProposalFailed(
926                        tick: self.details.tick,
927                        proposalId: self.uuid,
928                        votedPoints: self.getTotalVotedPoints(),
929                        failedAt: now
930                    )
931                    break
932                case ProposalStatus.Successed:
933                    // Excute the inscriptions
934                    self.executeSafe()
935                    break
936                }
937
938                // invoke onProposalFinalized in voters
939                if self.isFinalized() {
940                    let allVoters = self.getVoters()
941                    for addr in allVoters {
942                        if let voterRef = FRC20Votes.borrowVoterPublic(addr) {
943                            voterRef.onProposalFinalized(proposal: &self as &Proposal)
944                        }
945                    }
946                } // end finalized
947            }
948        }
949
950        access(contract)
951        view fun borrowDetails(): &ProposalDetails {
952            return &self.details
953        }
954    }
955
956    /// The public interface of the FixesVotes manager.
957    ///
958    access(all) resource interface VotesManagerPublic {
959        /// Check whether the proposer is valid.
960        access(all)
961        view fun isValidProposer(_ voterAddr: Address): Bool
962        /** ------ Proposal Getter ------ */
963        access(all)
964        view fun getProposalLength(): Int
965        access(all)
966        view fun getProposalIds(): [UInt64]
967        access(all)
968        view fun getActiveProposalIds(): [UInt64]
969        access(all)
970        view fun getProposalIdsByTick(tick: String): [UInt64]
971        /// Borrow the proposal.
972        access(all)
973        view fun borrowProposal(_ proposalId: UInt64): &Proposal?
974        /** ------ Write Methods ------ */
975        /// Create a new proposal.
976        access(all)
977        fun createProposal(
978            voter: auth(NonFungibleToken.Withdraw) &VoterIdentity,
979            tick: String,
980            title: String,
981            description: String,
982            discussionLink: String?,
983            executableThreshold: UFix64,
984            beginningTime: UFix64,
985            endingTime: UFix64,
986            commands: [FRC20VoteCommands.CommandType],
987            messages: [String],
988            inscriptions: @[[Fixes.Inscription]]
989        )
990        /// Vote on a proposal.
991        access(all)
992        fun vote(
993            voter: auth(NonFungibleToken.Withdraw) &VoterIdentity,
994            proposalId: UInt64,
995            choice: Int,
996        )
997        // --- Write Methods: Proposer ---
998        access(all)
999        fun updateProposal(
1000            voter: auth(NonFungibleToken.Withdraw) &VoterIdentity,
1001            proposalId: UInt64,
1002            title: String?,
1003            description: String?,
1004            discussionLink: String?,
1005        )
1006        access(all)
1007        fun cancelProposal(
1008            voter: auth(NonFungibleToken.Withdraw) &VoterIdentity,
1009            proposalId: UInt64,
1010        )
1011        access(all)
1012        fun executeProposal(
1013            voter: auth(NonFungibleToken.Withdraw) &VoterIdentity,
1014            proposalId: UInt64,
1015        )
1016    }
1017
1018    /// The resource of the FixesVotes manager.
1019    ///
1020    access(all) resource VotesManager: VotesManagerPublic, FixesHeartbeat.IHeartbeatHook {
1021        /// Voter -> Bool
1022        access(self)
1023        let whitelisted: {Address: Bool}
1024        /// ProposalId -> Proposal
1025        access(self)
1026        let proposals: @{UInt64: Proposal}
1027        /// active proposal ids
1028        access(self)
1029        let activeProposalIds: [UInt64]
1030        /// Ticker -> ProposalId[]
1031        access(self)
1032        let proposalIdsByTick: {String: [UInt64]}
1033
1034        init() {
1035            self.whitelisted = {}
1036            self.proposals <- {}
1037            self.activeProposalIds = []
1038            self.proposalIdsByTick = {}
1039        }
1040
1041        /** ----- Read ----- */
1042
1043        /// Check whether the proposer is valid.
1044        ///
1045        access(all)
1046        view fun isValidProposer(_ voterAddr: Address): Bool {
1047            if let whitelist = self.whitelisted[voterAddr] {
1048                if whitelist {
1049                    return true
1050                }
1051            }
1052            // check the staked amount
1053            let proposerThreshold = FRC20Votes.getProposerStakingThreshold()
1054            let totalStaked = FRC20Votes.getTotalStakedAmount()
1055            let thresholdPower = totalStaked * proposerThreshold
1056            if let voter = FRC20Votes.borrowVoterPublic(voterAddr) {
1057                // ensure the staked amount is enough
1058                return voter.getVotingPower() >= thresholdPower
1059            } else if let delegatorRef = FRC20Staking.borrowDelegator(voterAddr) {
1060                let stakeTick = FRC20FTShared.getPlatformStakingTickerName()
1061                return delegatorRef.getStakedBalance(tick: stakeTick) >= thresholdPower
1062            }
1063            return false
1064        }
1065
1066        access(all)
1067        view fun getProposalLength(): Int {
1068            return self.proposals.length
1069        }
1070
1071        access(all)
1072        view fun getProposalIds(): [UInt64] {
1073            return self.proposals.keys
1074        }
1075
1076        access(all)
1077        view fun getActiveProposalIds(): [UInt64] {
1078            return self.activeProposalIds
1079        }
1080
1081        access(all)
1082        view fun getProposalIdsByTick(tick: String): [UInt64] {
1083            return self.proposalIdsByTick[tick] ?? []
1084        }
1085
1086        access(all)
1087        view fun borrowProposal(_ proposalId: UInt64): &Proposal? {
1088            return self.borrowProposalRef(proposalId)
1089        }
1090
1091        /** ----- Write ----- */
1092
1093        /// Create a new proposal.
1094        ///
1095        access(all)
1096        fun createProposal(
1097            voter: auth(NonFungibleToken.Withdraw) &VoterIdentity,
1098            tick: String,
1099            title: String,
1100            description: String,
1101            discussionLink: String?,
1102            executableThreshold: UFix64,
1103            beginningTime: UFix64,
1104            endingTime: UFix64,
1105            commands: [FRC20VoteCommands.CommandType],
1106            messages: [String],
1107            inscriptions: @[[Fixes.Inscription]]
1108        ) {
1109            pre {
1110                beginningTime < endingTime: "Beginning time must be less than ending time"
1111                messages.length > 0: "Messages must be greater than 0"
1112                messages.length == inscriptions.length: "Messages and inscriptions must be the same length"
1113                messages.length == commands.length: "Messages and commands must be the same length"
1114            }
1115            let voterAddr = voter.owner?.address ?? panic("Voter's owner is not found")
1116            assert(
1117                self.isValidProposer(voterAddr),
1118                message: "The staked amount is not enough"
1119            )
1120
1121            // Save the inscriptions
1122            let inscriptionsStore = FRC20Votes.borrowSystemInscriptionsStore()
1123            let slots: [ChoiceSlotDetails] = []
1124
1125            let slotsLen = inscriptions.length
1126            var i = 0
1127            while i < slotsLen {
1128                let insList <- inscriptions.removeFirst()
1129                let insIds: [UInt64] = []
1130
1131                let insListLen = insList.length
1132                var j = 0
1133                while j < insListLen {
1134                    let ins <- insList.removeFirst()
1135                    insIds.append(ins.getId())
1136                    inscriptionsStore.store(<- ins)
1137                    j = j + 1
1138                }
1139                Burner.burn(<- insList)
1140
1141                slots.append(ChoiceSlotDetails(
1142                    message: messages[i],
1143                    command: FRC20VoteCommands.createByCommandType(commands[i], insIds),
1144                ))
1145                i = i + 1
1146            }
1147            Burner.burn(<- inscriptions)
1148
1149            let proposal <- create Proposal(
1150                voter: voterAddr,
1151                tick: tick,
1152                title: title,
1153                description: description,
1154                discussionLink: discussionLink,
1155                executableThreshold: executableThreshold,
1156                beginningTime: beginningTime,
1157                endingTime: endingTime,
1158                slots: slots
1159            )
1160            let proposalId = proposal.uuid
1161            self.proposals[proposalId] <-! proposal
1162            // update proposalIds by tick
1163            if self.proposalIdsByTick[tick] == nil {
1164                self.proposalIdsByTick[tick] = [proposalId]
1165            } else {
1166                self.proposalIdsByTick[tick]?.append(proposalId)
1167            }
1168            // insert activeProposalIds
1169            self.activeProposalIds.insert(at: 0, proposalId)
1170        }
1171
1172        access(all)
1173        fun vote(
1174            voter: auth(NonFungibleToken.Withdraw) &VoterIdentity,
1175            proposalId: UInt64,
1176            choice: Int,
1177        ) {
1178            let proposalRef = self.borrowProposal(proposalId)
1179                ?? panic("The proposal is not found")
1180            assert(
1181                proposalRef.isVotingAllowed(),
1182                message: "Voting is not allowed for now"
1183            )
1184            voter.onVote(choice: choice, proposal: proposalRef)
1185        }
1186
1187        /** ------ Write Methods: Proposer ------- */
1188
1189        /// Update the proposal.
1190        ///
1191        access(all)
1192        fun updateProposal(
1193            voter: auth(NonFungibleToken.Withdraw) &VoterIdentity,
1194            proposalId: UInt64,
1195            title: String?,
1196            description: String?,
1197            discussionLink: String?
1198        ) {
1199            let proposalRef = self.borrowProposalRef(proposalId)
1200                ?? panic("The proposal is not found")
1201            let detailsRef = proposalRef.borrowDetails()
1202            assert(
1203                detailsRef.proposer == voter.owner?.address,
1204                message: "The voter is not the proposer"
1205            )
1206            assert(
1207                proposalRef.isEditable(),
1208                message: "The proposal is not editable"
1209            )
1210            detailsRef.updateProposal(title: title, description: description, discussionLink: discussionLink)
1211
1212            emit ProposalInfoUpdated(
1213                proposalId: proposalRef.uuid,
1214                tick: detailsRef.tick,
1215                title: title,
1216                description: description,
1217                discussionLink: discussionLink
1218            )
1219        }
1220
1221        /// Cancel the proposal.
1222        ///
1223        access(all)
1224        fun cancelProposal(
1225            voter: auth(NonFungibleToken.Withdraw) &VoterIdentity,
1226            proposalId: UInt64
1227        ) {
1228            let proposalRef = self.borrowProposalRef(proposalId)
1229                ?? panic("The proposal is not found")
1230            let detailsRef = proposalRef.borrowDetails()
1231            assert(
1232                detailsRef.proposer == voter.owner?.address,
1233                message: "The voter is not the proposer"
1234            )
1235            assert(
1236                proposalRef.isEditable(),
1237                message: "The proposal is not editable"
1238            )
1239            detailsRef.cancelProposal()
1240            // refund the inscriptions
1241            proposalRef.refundInscriptionCost()
1242
1243            emit ProposalCancelled(
1244                proposalId: self.uuid,
1245                tick: detailsRef.tick
1246            )
1247        }
1248
1249        /// Manual execute the proposal.
1250        ///
1251        access(all)
1252        fun executeProposal(
1253            voter: auth(NonFungibleToken.Withdraw) &VoterIdentity,
1254            proposalId: UInt64
1255        ) {
1256            let proposalRef = self.borrowProposalRef(proposalId)
1257                ?? panic("The proposal is not found")
1258            let detailsRef = proposalRef.borrowDetails()
1259            assert(
1260                detailsRef.proposer == voter.owner?.address,
1261                message: "The voter is not the proposer"
1262            )
1263            let status = proposalRef.getStatus()
1264            assert(
1265                status == ProposalStatus.Successed,
1266                message: "The proposal is not successed"
1267            )
1268            proposalRef.onExecute()
1269        }
1270
1271        /** ----- Write: Private ----- */
1272
1273        access(Admin)
1274        fun updateWhitelist(voter: Address, isWhitelisted: Bool) {
1275            self.whitelisted[voter] = isWhitelisted
1276            emit VotesManagerWhitelistUpdated(voter: voter, isWhitelisted: isWhitelisted)
1277        }
1278
1279        access(Admin)
1280        fun forceHeartbeat() {
1281            let proposalIds = self.proposals.keys
1282            for proposalId in proposalIds {
1283                if let proposal = self.borrowProposalRef(proposalId) {
1284                    proposal.onHeartbeat(0.0)
1285                }
1286            }
1287        }
1288
1289        /** ------ Internal Methods ------ */
1290
1291        access(account)
1292        fun onHeartbeat(_ deltaTime: UFix64) {
1293            // update the active proposal ids
1294            var i = 0
1295            while i < self.activeProposalIds.length {
1296                let proposalId = self.activeProposalIds.removeFirst()
1297                if let proposal = self.borrowProposalRef(proposalId) {
1298                    // call the proposal heartbeat
1299                    proposal.onHeartbeat(deltaTime)
1300
1301                    // check if finalized
1302                    if !proposal.isFinalized() {
1303                        // re-insert the proposal id
1304                        self.activeProposalIds.append(proposalId)
1305                    }
1306                }
1307                i = i + 1
1308            }
1309        }
1310
1311        /** ----- Internal ----- */
1312
1313        access(self)
1314        view fun borrowProposalRef(_ proposalId: UInt64): &Proposal? {
1315            return &self.proposals[proposalId]
1316        }
1317    }
1318
1319    /// Borrow the system inscriptions store.
1320    ///
1321    access(contract)
1322    view fun borrowSystemInscriptionsStore(): auth(Fixes.Manage) &Fixes.InscriptionsStore {
1323        let storePubPath = Fixes.getFixesStoreStoragePath()
1324        return self.account.storage
1325            .borrow<auth(Fixes.Manage) &Fixes.InscriptionsStore>(from: storePubPath)
1326            ?? panic("Fixes.InscriptionsStore is not found")
1327    }
1328
1329    /* --- Public Functions --- */
1330
1331    access(all)
1332    fun createVoter(
1333        _ cap: Capability<auth(NonFungibleToken.Update, NonFungibleToken.Withdraw) &FRC20SemiNFT.Collection>
1334    ): @VoterIdentity {
1335        return <- create VoterIdentity(cap)
1336    }
1337
1338    /// Get the proposer staking threshold.
1339    ///
1340    access(all)
1341    view fun getProposerStakingThreshold(): UFix64 {
1342        return 0.15
1343    }
1344
1345    /// Get the total staked amount.
1346    ///
1347    access(all)
1348    view fun getTotalStakedAmount(): UFix64 {
1349        let pool = FRC20StakingManager.borrowPlatformStakingPool()
1350        return pool.getDetails().totalStaked
1351    }
1352
1353    /// Borrow the voter resource.
1354    ///
1355    access(all)
1356    view fun borrowVoterPublic(_ addr: Address): &VoterIdentity? {
1357        return getAccount(addr)
1358            .capabilities.get<&VoterIdentity>(self.VoterPublicPath)
1359            .borrow()
1360    }
1361
1362    /// Borrow the VotesManager resource.
1363    ///
1364    access(all)
1365    view fun borrowVotesManager(): &VotesManager {
1366        return self.account
1367            .capabilities.get<&VotesManager>(self.FRC20VotesManagerPublicPath)
1368            .borrow() ?? panic("VotesManager is not found")
1369    }
1370
1371    init() {
1372        let votesIdentifier = "FRC20VotesManager_".concat(self.account.address.toString())
1373        self.FRC20VotesManagerStoragePath = StoragePath(identifier: votesIdentifier)!
1374        self.FRC20VotesManagerPublicPath = PublicPath(identifier: votesIdentifier)!
1375
1376        // create the resource
1377        self.account.storage.save(<- create VotesManager(), to: self.FRC20VotesManagerStoragePath)
1378        self.account.capabilities.publish(
1379            self.account.capabilities.storage.issue<&VotesManager>(self.FRC20VotesManagerStoragePath),
1380            at: self.FRC20VotesManagerPublicPath
1381        )
1382
1383        // Register to FixesHeartbeat
1384        let heartbeatScope = "FRC20Votes"
1385        let accountAddr = self.account.address
1386        if !FixesHeartbeat.hasHook(scope: heartbeatScope, hookAddr: accountAddr) {
1387            FixesHeartbeat.addHook(
1388                scope: heartbeatScope,
1389                hookAddr: accountAddr,
1390                hookPath: self.FRC20VotesManagerPublicPath
1391            )
1392        }
1393
1394        // Ensure InscriptionsStore resource
1395        let insStoreStoragePath = Fixes.getFixesStoreStoragePath()
1396        if self.account.storage.borrow<&AnyResource>(from: insStoreStoragePath) == nil {
1397            self.account.storage.save<@Fixes.InscriptionsStore>(<- Fixes.createInscriptionsStore(), to: insStoreStoragePath)
1398        }
1399        let insStorePubPath = Fixes.getFixesStorePublicPath()
1400
1401        if !self.account
1402            .capabilities.get<&Fixes.InscriptionsStore>(insStorePubPath)
1403            .check() {
1404            self.account.capabilities.unpublish(insStorePubPath)
1405            self.account.capabilities.publish(
1406                self.account.capabilities.storage.issue<&Fixes.InscriptionsStore>(insStoreStoragePath),
1407                at: insStorePubPath
1408            )
1409        }
1410
1411        let voterIdentifier = "FRC20Voter_".concat(self.account.address.toString())
1412        self.VoterStoragePath = StoragePath(identifier: voterIdentifier)!
1413        self.VoterPublicPath = PublicPath(identifier: voterIdentifier)!
1414
1415        emit ContractInitialized()
1416    }
1417}
1418