Smart Contract

FRC20VoteCommands

A.d2abb5dbf5e08666.FRC20VoteCommands

Valid From

86,129,038

Deployed

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

Dependents

2 imports
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# FRC20VoteCommands
5
6This contract is used to manage the frc20 vote commands.
7
8*/
9import FlowToken from 0x1654653399040a61
10import Fixes from 0xd2abb5dbf5e08666
11import FixesInscriptionFactory from 0xd2abb5dbf5e08666
12import FRC20Indexer from 0xd2abb5dbf5e08666
13import FRC20FTShared from 0xd2abb5dbf5e08666
14import FRC20AccountsPool from 0xd2abb5dbf5e08666
15import FRC20StakingManager from 0xd2abb5dbf5e08666
16import FRC20StakingVesting from 0xd2abb5dbf5e08666
17import FGameLottery from 0xd2abb5dbf5e08666
18import FGameLotteryRegistry from 0xd2abb5dbf5e08666
19import FGameLotteryFactory from 0xd2abb5dbf5e08666
20
21access(all) contract FRC20VoteCommands {
22
23    /// The Proposal command type.
24    ///
25    access(all) enum CommandType: UInt8 {
26        access(all) case None;
27        access(all) case SetBurnable;
28        access(all) case BurnUnsupplied;
29        access(all) case MoveTreasuryToLotteryJackpot;
30        access(all) case MoveTreasuryToStakingReward;
31    }
32
33    /// The interface of FRC20 vote command struct.
34    ///
35    access(all) struct interface IVoteCommand {
36        access(all)
37        let inscriptionIds: [UInt64]
38
39        // ----- Readonly Mehtods -----
40
41        /// Get the command type.
42        access(all)
43        view fun getCommandType(): CommandType
44        // It is readonly, but it is not a view function.
45        access(all)
46        fun verifyVoteCommands(): Bool
47
48        /// Check if all inscriptions are extracted.
49        ///
50        access(all)
51        view fun isAllInscriptionsExtracted(): Bool {
52            let store = FRC20VoteCommands.borrowSystemInscriptionsStore()
53            for id in self.inscriptionIds {
54                if let insRef = store.borrowInscription(id) {
55                    if !insRef.isExtracted() {
56                        return false
57                    }
58                }
59            }
60            return true
61        }
62
63        // ----- Account level methods -----
64
65        /// Refund the inscription cost for failed vote commands.
66        ///
67        access(account)
68        fun refundFailedVoteCommands(receiver: Address): Bool {
69            let recieverRef = Fixes.borrowFlowTokenReceiver(receiver)
70            if recieverRef == nil {
71                return false
72            }
73            let store = FRC20VoteCommands.borrowSystemInscriptionsStore()
74            let insRefArr = self.borrowSystemInscriptionWritableRefs()
75
76            let vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
77            for insRef in insRefArr {
78                if !insRef.isExtracted() {
79                    vault.deposit(from: <-insRef.extract())
80                }
81            }
82            // deposit to the receiver
83            recieverRef!.deposit(from: <- vault)
84            return true
85        }
86
87        // Methods: Write
88        access(account)
89        fun safeRunVoteCommands(): Bool
90
91        // ----- General Methods -----
92
93        /// Borrow the system inscriptions references from store.
94        ///
95        access(contract)
96        fun borrowSystemInscriptionWritableRefs(): [auth(Fixes.Extractable) &Fixes.Inscription] {
97            let store = FRC20VoteCommands.borrowSystemInscriptionsStore()
98            let ret: [auth(Fixes.Extractable) &Fixes.Inscription] = []
99            for id in self.inscriptionIds {
100                if let ref = store.borrowInscriptionWritableRefInAccount(id) {
101                    ret.append(ref)
102                }
103            }
104            return ret
105        }
106    }
107
108    /**
109     * Command: None
110     */
111    access(all) struct CommandNone: IVoteCommand {
112        access(all)
113        let inscriptionIds: [UInt64]
114
115        init() {
116            self.inscriptionIds = []
117        }
118
119        // ----- Methods: Read -----
120
121        access(all)
122        view fun getCommandType(): CommandType {
123            return CommandType.None
124        }
125
126        access(all)
127        fun verifyVoteCommands(): Bool {
128            return true
129        }
130
131        // ---- Methods: Write ----
132
133        access(account)
134        fun safeRunVoteCommands(): Bool {
135            return true
136        }
137    }
138
139    /**
140     * Command: SetBurnable
141     */
142    access(all) struct CommandSetBurnable: IVoteCommand {
143        access(all)
144        let inscriptionIds: [UInt64]
145
146        init(_ insIds: [UInt64]) {
147            self.inscriptionIds = insIds
148
149            assert(
150                self.verifyVoteCommands(),
151                message: "Invalid vote commands"
152            )
153        }
154
155        // ----- Methods: Read -----
156
157        access(all)
158        view fun getCommandType(): CommandType {
159            return CommandType.SetBurnable
160        }
161
162        access(all)
163        fun verifyVoteCommands(): Bool {
164            var isValid = false
165            isValid = self.inscriptionIds.length == 1
166            if isValid {
167                let store = FRC20VoteCommands.borrowSystemInscriptionsStore()
168                if let ins = store.borrowInscription(self.inscriptionIds[0]) {
169                    let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
170                    isValid = FRC20VoteCommands.isValidSystemInscription(ins)
171                        && meta["op"] == "burnable" && meta["tick"] != nil && meta["v"] != nil
172                } else {
173                    isValid = false
174                }
175            }
176            return isValid
177        }
178
179        // ---- Methods: Write ----
180
181        access(account)
182        fun safeRunVoteCommands(): Bool {
183            // Refs
184            let frc20Indexer = FRC20Indexer.getIndexer()
185            let insRefArr = self.borrowSystemInscriptionWritableRefs()
186
187            if insRefArr.length != 1 {
188                return false
189            }
190            frc20Indexer.setBurnable(ins: insRefArr[0])
191            return true
192        }
193    }
194
195    /**
196     * Command: BurnUnsupplied
197     */
198    access(all) struct CommandBurnUnsupplied: IVoteCommand {
199        access(all)
200        let inscriptionIds: [UInt64]
201
202        init(_ insIds: [UInt64]) {
203            self.inscriptionIds = insIds
204
205            assert(
206                self.verifyVoteCommands(),
207                message: "Invalid vote commands"
208            )
209        }
210
211        // ----- Methods: Read -----
212
213        access(all)
214        view fun getCommandType(): CommandType {
215            return CommandType.BurnUnsupplied
216        }
217
218        access(all)
219        fun verifyVoteCommands(): Bool {
220            // Refs
221            let insRefArr = self.borrowSystemInscriptionWritableRefs()
222
223            var isValid = insRefArr.length == 1
224            if isValid {
225                let ins = insRefArr[0]
226                let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
227                isValid = FRC20VoteCommands.isValidSystemInscription(ins)
228                    && meta["op"] == "burnUnsup" && meta["tick"] != nil && meta["perc"] != nil
229            }
230            return isValid
231        }
232
233        // ---- Methods: Write ----
234
235        access(account)
236        fun safeRunVoteCommands(): Bool {
237            // Refs
238            let frc20Indexer = FRC20Indexer.getIndexer()
239            let insRefArr = self.borrowSystemInscriptionWritableRefs()
240
241            if insRefArr.length != 1 {
242                return false
243            }
244            frc20Indexer.burnUnsupplied(ins: insRefArr[0])
245            return true
246        }
247    }
248
249    /**
250     * Command: MoveTreasuryToLotteryJackpot
251     */
252    access(all) struct CommandMoveTreasuryToLotteryJackpot: IVoteCommand {
253        access(all)
254        let inscriptionIds: [UInt64]
255
256        init(_ insIds: [UInt64]) {
257            self.inscriptionIds = insIds
258
259            assert(
260                self.verifyVoteCommands(),
261                message: "Invalid vote commands"
262            )
263        }
264
265        // ----- Methods: Read -----
266
267        access(all)
268        view fun getCommandType(): CommandType {
269            return CommandType.MoveTreasuryToLotteryJackpot
270        }
271
272        access(all)
273        fun verifyVoteCommands(): Bool {
274            var isValid = self.inscriptionIds.length == 1
275            if isValid {
276                let store = FRC20VoteCommands.borrowSystemInscriptionsStore()
277                if let ins = store.borrowInscription(self.inscriptionIds[0]) {
278                    let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
279                    isValid = FRC20VoteCommands.isValidSystemInscription(ins)
280                        && meta["op"] == "withdrawFromTreasury"
281                        && meta["usage"] == "lottery"
282                        && meta["tick"] != nil && meta["amt"] != nil
283                } else {
284                    isValid = false
285                }
286            }
287            return isValid
288        }
289
290        // ---- Methods: Write ----
291
292        access(account)
293        fun safeRunVoteCommands(): Bool {
294            // Refs
295            let frc20Indexer = FRC20Indexer.getIndexer()
296            let insRefArr = self.borrowSystemInscriptionWritableRefs()
297
298            if insRefArr.length != 1 {
299                return false
300            }
301            let flowLotteryName = FGameLotteryFactory.getFIXESMintingLotteryPoolName()
302            let registery = FGameLotteryRegistry.borrowRegistry()
303            if let poolAddr = registery.getLotteryPoolAddress(flowLotteryName) {
304                if let poolRef = FGameLottery.borrowLotteryPool(poolAddr) {
305                    let withdrawnChange <- frc20Indexer.withdrawFromTreasury(ins: insRefArr[0])
306                    poolRef.donateToJackpot(payment: <- withdrawnChange)
307                    return true
308                }
309            }
310            log("Failed to find the lottery pool")
311            return false
312        }
313    }
314
315    /**
316     * Command: MoveTreasuryToStakingReward
317     */
318    access(all) struct CommandMoveTreasuryToStakingReward: IVoteCommand {
319        access(all)
320        let inscriptionIds: [UInt64]
321
322        init(_ insIds: [UInt64]) {
323            self.inscriptionIds = insIds
324
325            assert(
326                self.verifyVoteCommands(),
327                message: "Invalid vote commands"
328            )
329        }
330
331        // ----- Methods: Read -----
332
333        access(all)
334        view fun getCommandType(): CommandType {
335            return CommandType.MoveTreasuryToStakingReward
336        }
337
338        access(all)
339        fun verifyVoteCommands(): Bool {
340            var isValid = self.inscriptionIds.length == 1
341            if isValid {
342                let store = FRC20VoteCommands.borrowSystemInscriptionsStore()
343                if let ins = store.borrowInscription(self.inscriptionIds[0]) {
344                    let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
345                    isValid = FRC20VoteCommands.isValidSystemInscription(ins)
346                        && meta["op"] == "withdrawFromTreasury" && meta["usage"] == "staking"
347                        && meta["tick"] != nil && meta["amt"] != nil
348                        && meta["batch"] != nil && meta["interval"] != nil
349                } else {
350                    isValid = false
351                }
352            }
353            return isValid
354        }
355
356        // ---- Methods: Write ----
357
358        access(account)
359        fun safeRunVoteCommands(): Bool {
360            // Refs
361            let frc20Indexer = FRC20Indexer.getIndexer()
362            let insRefArr = self.borrowSystemInscriptionWritableRefs()
363
364            if insRefArr.length != 1 {
365                return false
366            }
367            let ins = insRefArr[0]
368            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
369
370            // singleton resources
371            let acctsPool = FRC20AccountsPool.borrowAccountsPool()
372            let platformStakeTick = FRC20FTShared.getPlatformStakingTickerName()
373            let vestingBatch = UInt32.fromString(meta["batch"]!)
374            let vestingInterval = UFix64.fromString(meta["interval"]!)
375            if vestingBatch == nil || vestingInterval == nil {
376                log("Invalid vesting batch or interval")
377                return false
378            }
379            if let stakingAddress = acctsPool.getFRC20StakingAddress(tick: platformStakeTick) {
380                if let vestingVault = FRC20StakingVesting.borrowVaultRef(stakingAddress) {
381                    let withdrawnChange <- frc20Indexer.withdrawFromTreasury(ins: insRefArr[0])
382                    FRC20StakingManager.donateToVestingFromChange(
383                        changeToDonate: <- withdrawnChange,
384                        tick: platformStakeTick,
385                        vestingBatchAmount: vestingBatch!,
386                        vestingInterval: vestingInterval!
387                    )
388                    return true
389                }
390            }
391            log("Failed to find valid staking pool")
392            return false
393        }
394    }
395
396    /// Check if the given inscription is a valid system inscription.
397    ///
398    access(contract)
399    view fun isValidSystemInscription(_ ins: &Fixes.Inscription): Bool {
400        let frc20Indexer = FRC20Indexer.getIndexer()
401        return ins.owner?.address == self.account.address
402            && ins.isExtractable()
403            && frc20Indexer.isValidFRC20Inscription(ins: ins)
404    }
405
406    /// Borrow the system inscriptions store.
407    ///
408    access(all)
409    view fun borrowSystemInscriptionsStore(): &Fixes.InscriptionsStore {
410        let storePubPath = Fixes.getFixesStorePublicPath()
411        return self.account
412            .capabilities.get<&Fixes.InscriptionsStore>(storePubPath)
413            .borrow() ?? panic("Fixes.InscriptionsStore is not found")
414    }
415
416    /// Build the inscription strings by the given command type and meta.
417    ///
418    access(all)
419    view fun buildInscriptionStringsByCommand(_ type: CommandType, _ meta: {String: String}): [String] {
420        switch type {
421        case CommandType.None:
422            return []
423        case CommandType.SetBurnable:
424            return [
425                FixesInscriptionFactory.buildVoteCommandSetBurnable(
426                    tick: meta["tick"] ?? panic("Missing tick in params"),
427                    burnable: meta["v"] == "1"
428                )
429            ]
430        case CommandType.BurnUnsupplied:
431            return [
432                FixesInscriptionFactory.buildVoteCommandBurnUnsupplied(
433                    tick: meta["tick"] ?? panic("Missing tick in params"),
434                    percent: UFix64.fromString(meta["perc"] ?? panic("Missing perc in params")) ?? panic("Invalid perc")
435                )
436            ]
437        case CommandType.MoveTreasuryToLotteryJackpot:
438            return [
439                FixesInscriptionFactory.buildVoteCommandMoveTreasuryToLotteryJackpot(
440                    tick: meta["tick"] ?? panic("Missing tick in params"),
441                    amount: UFix64.fromString(meta["amt"] ?? panic("Missing amt in params")) ?? panic("Invalid amt")
442                )
443            ]
444        case CommandType.MoveTreasuryToStakingReward:
445            return [
446                FixesInscriptionFactory.buildVoteCommandMoveTreasuryToStaking(
447                    tick: meta["tick"] ?? panic("Missing tick in params"),
448                    amount: UFix64.fromString(meta["amt"] ?? panic("Missing amt in params")) ?? panic("Invalid amt"),
449                    vestingBatchAmount: UInt32.fromString(meta["batch"] ?? panic("Missing batch in params")) ?? panic("Invalid batch"),
450                    vestingInterval: UFix64.fromString(meta["interval"] ?? panic("Missing interval in params")) ?? panic("Invalid interval")
451                )
452            ]
453        }
454        panic("Invalid command type")
455    }
456
457    /// Create a vote command by the given type and inscriptions.
458    ///
459    access(all)
460    fun createByCommandType(_ type: CommandType, _ inscriptions: [UInt64]): {IVoteCommand} {
461        let store = self.borrowSystemInscriptionsStore()
462        // Ensure the inscriptions are valid
463        for ins in inscriptions {
464            let insRef = store.borrowInscription(ins)
465            if insRef == nil {
466                panic("Invalid inscription")
467            }
468        }
469
470        switch type {
471        case CommandType.None:
472            return CommandNone()
473        case CommandType.SetBurnable:
474            return CommandSetBurnable(inscriptions)
475        case CommandType.BurnUnsupplied:
476            return CommandBurnUnsupplied(inscriptions)
477        case CommandType.MoveTreasuryToLotteryJackpot:
478            return CommandMoveTreasuryToLotteryJackpot(inscriptions)
479        case CommandType.MoveTreasuryToStakingReward:
480            return CommandMoveTreasuryToStakingReward(inscriptions)
481        }
482        panic("Invalid command type")
483    }
484}
485