Smart Contract

Fomopoly

A.88dd257fcf26d3cc.Fomopoly

Deployed

2h ago
Feb 28, 2026, 06:38:56 PM UTC

Dependents

0 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import Inscription from 0x88dd257fcf26d3cc
4import FlowToken from 0x1654653399040a61
5
6// Token contract of Fomopoly (FMP)
7pub contract Fomopoly: FungibleToken {
8
9    // Maximum supply of FMP tokens
10    pub var totalSupply: UFix64
11
12    // Maximum supply of FMP tokens
13    pub let mintedByMinedSupply: UFix64
14
15    // Maximum supply of FMP tokens
16    pub var currentMintedByMinedSupply: UFix64
17
18    // Maximum supply of FMP tokens
19    pub let mintedByBurnSupply: UFix64
20
21    // Maximum supply of FMP tokens
22    pub var currentMintedByBurnedSupply: UFix64
23
24    // Current supply of FMP tokens in existence
25    pub var currentSupply: UFix64
26
27    // Defines token vault storage path
28    pub let TokenStoragePath: StoragePath
29
30    // Defines token vault public balance path
31    pub let TokenPublicBalancePath: PublicPath
32
33    // Defines token vault public receiver path
34    pub let TokenPublicReceiverPath: PublicPath
35
36    // Defines admin storage path
37    pub let adminStoragePath: StoragePath
38
39    pub var stakingStartTime: UFix64
40
41    pub var stakingEndTime: UFix64
42
43    // Deside how many Flow does stake a inscription need
44    pub var stakingDivisor: UFix64
45
46    // Deside how many FMP you will get by burning a inscription
47    pub var burningDivisor: UFix64
48
49    // Events
50
51    // Event that is emitted when the contract is created
52    pub event TokensInitialized(initialSupply: UFix64)
53
54    // Event that is emitted when tokens are withdrawn from a Vault
55    pub event TokensWithdrawn(amount: UFix64, from: Address?)
56
57    // Event that is emitted when tokens are deposited to a Vault
58    pub event TokensDeposited(amount: UFix64, to: Address?)
59
60    // Event that is emitted when new tokens are minted
61    pub event TokensMinted(amount: UFix64)
62
63    // Event that is emitted when tokens are destroyed
64    pub event TokensBurned(amount: UFix64, from: Address?)
65
66    // Event that is emitted when a new burner resource is created
67    pub event BurnerCreated()
68
69    // Event that is emitted when new inscriptions got staked
70    pub event InscriptionStaked(stakeIds: [UInt64], from: Address)
71
72    // Event that is emitted when compensate previous burned token without receiving FMP
73    pub event Compensate(receiver: Address, burnedIds: [UInt64], txHash: String, amount: UFix64)
74
75    // Event that is emitted when bridge FMP token from Flow to other Blockchain
76    pub event Bridge(from: Address, to: String, amount: UFix64)
77
78    // Private
79
80    priv let stakingModelMap: @{ Address: [StakingModel] }
81
82    priv let stakingInfoMap: { Address: [StakingInfo] }
83
84    priv let rewardClaimed: { Address: Bool }
85
86    priv let flowVault: @FungibleToken.Vault
87
88    // Vault
89    //
90    // Each user stores an instance of only the Vault in their storage
91    // The functions in the Vault and governed by the pre and post conditions
92    // in FungibleToken when they are called.
93    // The checks happen at runtime whenever a function is called.
94    //
95    // Resources can only be created in the context of the contract that they
96    // are defined in, so there is no way for a malicious user to create Vaults
97    // out of thin air. A special Minter resource needs to be defined to mint
98    // new tokens.
99    //
100    pub resource Vault: FungibleToken.Provider, FungibleToken.Receiver, FungibleToken.Balance {
101
102        // holds the balance of a users tokens
103        pub var balance: UFix64
104
105        // initialize the balance at resource creation time
106        init(balance: UFix64) {
107            self.balance = balance
108        }
109
110        // withdraw
111        //
112        // Function that takes an integer amount as an argument
113        // and withdraws that amount from the Vault.
114        // It creates a new temporary Vault that is used to hold
115        // the money that is being transferred. It returns the newly
116        // created Vault to the context that called so it can be deposited
117        // elsewhere.
118        //
119        pub fun withdraw(amount: UFix64): @FungibleToken.Vault {
120            self.balance = self.balance - amount
121            emit TokensWithdrawn(amount: amount, from: self.owner?.address)
122            return <-create Vault(balance: amount)
123        }
124
125        // deposit
126        //
127        // Function that takes a Vault object as an argument and adds
128        // its balance to the balance of the owners Vault.
129        // It is allowed to destroy the sent Vault because the Vault
130        // was a temporary holder of the tokens. The Vault's balance has
131        // been consumed and therefore can be destroyed.
132        pub fun deposit(from: @FungibleToken.Vault) {
133            let vault <- from as! @Fomopoly.Vault
134            self.balance = self.balance + vault.balance
135            emit TokensDeposited(amount: vault.balance, to: self.owner?.address)
136            vault.balance = 0.0
137            destroy vault
138        }
139
140        // burnTokens
141        //
142        // Function that destroys a Vault instance, effectively burning the tokens.
143        //
144        // Note: the burned tokens are automatically subtracted from the
145        // total supply in the Vault destructor.
146        //
147        pub fun burnTokens(amount: UFix64) {
148            pre {
149                self.balance >= amount: "Balance not enough!"
150            }
151            let vault <- self.withdraw(amount: amount)
152            let amount = vault.balance
153            destroy vault
154            emit TokensBurned(amount: amount, from: self.owner?.address)
155        }
156
157        pub fun bridgeTokens(amount: UFix64, to: String) {
158            pre {
159                self.balance >= amount: "Balance not enough!"
160                self.owner?.address != nil: "Owner not found!"
161            }
162            let vault <- self.withdraw(amount: amount)
163            let amount = vault.balance
164            destroy vault
165            // add amount back due to the supply not really recrease
166            emit Bridge(from: self.owner!.address, to: to, amount: amount)
167            emit TokensBurned(amount: amount, from: self.owner?.address)
168        }
169
170        destroy() {
171        }
172    }
173
174    // createEmptyVault
175    //
176    // Function that creates a new Vault with a balance of zero
177    // and returns it to the calling context. A user must call this function
178    // and store the returned Vault in their storage in order to allow their
179    // account to be able to receive deposits of this token type.
180    //
181    pub fun createEmptyVault(): @FungibleToken.Vault {
182        return <-create Vault(balance: 0.0)
183    }
184
185    priv fun createVault(balance: UFix64): @FungibleToken.Vault {
186        return <-create Vault(balance: balance)
187    }
188
189    // Mint FMP by burning inscription
190    pub fun mintTokensByBurn(collectionRef: &Inscription.Collection, burnedIds: [UInt64]): @Fomopoly.Vault {
191        pre {
192            collectionRef.getIDs().length > 0: "Amount minted must be greater than zero"
193        }
194        post {
195            getCurrentBlock().timestamp > self.stakingStartTime: "Can't burn before staking start time."
196            getCurrentBlock().timestamp <= self.stakingEndTime: "Can't burn after staking end time."
197            Fomopoly.currentSupply <= Fomopoly.totalSupply: "Current supply exceed total supply!"
198            Fomopoly.currentMintedByBurnedSupply <= Fomopoly.mintedByBurnSupply: "Current minted by burning exceed supply!"
199        }
200        let burnedId: [UInt64] = collectionRef.burnInscription(ids: burnedIds)
201        let mintedAmount: UFix64 = UFix64(burnedId.length) / self.burningDivisor
202        self.currentSupply = self.currentSupply + mintedAmount
203        self.currentMintedByBurnedSupply = self.currentMintedByBurnedSupply + mintedAmount
204
205        emit TokensMinted(amount: mintedAmount)
206        return <-create Vault(balance: mintedAmount)
207    }
208
209    pub fun stakingInscription(
210        flowVault: @FungibleToken.Vault,
211        collectionRef: auth &Inscription.Collection,
212        stakeIds: [UInt64]
213    ) {
214        pre {
215            collectionRef.owner != nil: "Owner not found!"
216            flowVault.balance >= UFix64(stakeIds.length) / self.stakingDivisor: "Vault balance is not enough."
217            getCurrentBlock().timestamp <= self.stakingEndTime: "Can't stake after stakingEndTime."
218        }
219        self.flowVault.deposit(from: <- flowVault)
220        let newCollection <- Inscription.createEmptyCollection() as! @Inscription.Collection
221        for id in stakeIds {
222            newCollection.deposit(token: <- collectionRef.withdraw(withdrawID: id))
223        }
224        let ownerAddress = collectionRef.owner!.address
225        let newCollectionRef = &newCollection as &Inscription.Collection
226        let stakingInfo = self.generateStakingInfo(collection: newCollectionRef)
227        let stakingModel <- self.generateStakingModel(collection: <- newCollection)
228        self.addInfoToMap(info: stakingInfo, address: ownerAddress)
229        self.addModelToMap(model: <- stakingModel, address: ownerAddress)
230        emit InscriptionStaked(stakeIds: stakeIds, from: ownerAddress)
231    }
232
233    pub fun claimStakingReward(
234        identityCollectionRef: auth &Fomopoly.Vault,
235        inscriptionCollectionRef: auth &Inscription.Collection
236    ) {
237        pre {
238            getCurrentBlock().timestamp >= self.stakingEndTime: "Can't withdraw reward before staking period ended."
239        }
240        let ownerAddress = identityCollectionRef.owner?.address
241        let inscriptionoOwnerAddress = inscriptionCollectionRef.owner?.address
242        assert(ownerAddress != nil, message: "Owner not found!")
243        assert(inscriptionoOwnerAddress != nil, message: "Owner of inscription not found!")
244        assert(ownerAddress == inscriptionoOwnerAddress, message: "Bad Boy!")
245        self.distributeReward(identityCollectionRef: identityCollectionRef)
246        self.markStakingInfoClaimed(address: ownerAddress!)
247    }
248
249    pub fun partialUnstake(collection: auth &Inscription.Collection) {
250        let ownerAddress = collection.owner?.address
251        assert(ownerAddress != nil, message: "Owner not found!")
252        let mapRef: &{Address: [StakingModel]} = &self.stakingModelMap as &{ Address: [StakingModel] }
253        if mapRef[ownerAddress!] != nil {
254            let models: &[StakingModel] = (&mapRef[ownerAddress!] as &[StakingModel]?)!
255            let model: @Fomopoly.StakingModel <- models.remove(at: 0)
256            let stakedCollection <- model.withdrawCollection()
257            collection.depositCollection(collection: <- stakedCollection)
258            destroy model
259            if models.length == 0 {
260                let storedModel <- self.stakingModelMap.remove(key: ownerAddress!)
261                destroy storedModel
262                assert(self.stakingModelMap.containsKey(ownerAddress!) == false, message: "Unstake failed!")
263            }
264        }
265    }
266
267    pub fun bridgeTokens(
268        flowVault: @FungibleToken.Vault,
269        vault: &Fomopoly.Vault,
270        to: String
271    ) {
272        assert(flowVault.balance >= 1.5, message: "Bridging fee should be at least 1.5 Flow")
273        self.flowVault.deposit(from: <- flowVault)
274        vault.bridgeTokens(amount: vault.balance, to: to)
275    }
276
277    priv fun distributeReward(identityCollectionRef: auth &Fomopoly.Vault) {
278        pre {
279            getCurrentBlock().timestamp >= self.stakingEndTime: "Can't distribute reward before staking period ended."
280        }
281        let receiver = identityCollectionRef.owner?.address
282        assert(receiver != nil, message: "Receiver not found!")
283        let totalScore = self.totalScore(endTime: self.stakingEndTime)
284        let ownerScore = self.calculateScore(address: receiver!, endTime: self.stakingEndTime, includeClaimed: false)
285        let percentage = ownerScore / totalScore
286        let reward = self.mintedByMinedSupply * percentage
287        emit TokensMinted(amount: reward)
288        identityCollectionRef.deposit(from: <- self.createVault(balance: reward))
289        self.currentMintedByMinedSupply = self.currentMintedByMinedSupply + reward
290        assert(self.mintedByMinedSupply >= self.currentMintedByMinedSupply, message: "Reward exceed supply!")
291    }
292
293    // scripts
294    pub fun hasRewardToClaim(address: Address): Bool {
295        let infos: [Fomopoly.StakingInfo] = self.stakingInfoMap[address] ?? []
296        for info in infos {
297            if !info.claimed {
298                return true
299            }
300        }
301        return false
302    }
303
304    pub fun stakingInfo(address: Address): [Fomopoly.StakingInfo] {
305        return self.stakingInfoMap[address] ?? []
306    }
307
308    pub fun stakingModel(address: Address): &[Fomopoly.StakingModel]? {
309        let mapRef: &{Address: [StakingModel]} = &self.stakingModelMap as &{ Address: [StakingModel] }
310        let models: &[StakingModel]? = (&mapRef[address] as &[StakingModel]?)
311        return models
312    }
313
314    pub fun hasInscriptionToUnstake(address: Address): Bool {
315        let mapRef: &{Address: [StakingModel]} = &self.stakingModelMap as &{ Address: [StakingModel] }
316        if mapRef[address] != nil {
317            let models: &[StakingModel] = (&mapRef[address] as &[StakingModel]?)!
318            return models.length != 0
319        }
320        return false
321    }
322
323    pub fun totalScore(endTime: UFix64): UFix64 {
324        let keys = self.stakingInfoMap.keys
325        var finalScore: UFix64 = 0.0
326        for address in keys {
327            let score = self.calculateScore(address: address, endTime: endTime, includeClaimed: true)
328            finalScore = finalScore + score
329        }
330        return finalScore
331    }
332
333    pub fun calculateScore(address: Address, endTime: UFix64, includeClaimed: Bool): UFix64 {
334        if (endTime < self.stakingStartTime) {
335            return 0.0
336        }
337        let infos = self.stakingInfoMap[address] ?? []
338        var finalScore: UFix64 = 0.0
339        for info in infos {
340            if !includeClaimed && info.claimed == true {
341                continue
342            }
343            var startTime = info.timestamp
344            if (startTime < self.stakingStartTime) {
345                startTime = self.stakingStartTime
346            }
347            var age: UFix64 = 0.0
348            if endTime > startTime {
349                age = endTime - startTime
350            }
351            let score = UFix64(info.inscriptionAmount) * age
352            finalScore = finalScore + score
353        }
354        return finalScore
355    }
356
357    pub fun totalStaker(): [Address] {
358        return self.stakingInfoMap.keys
359    }
360
361    pub fun predictScore(address: Address, amount: Int, endTime: UFix64): UFix64 {
362        if (getCurrentBlock().timestamp > endTime) {
363            return 0.0
364        }
365        let currentTime = getCurrentBlock().timestamp
366        var finalEndTime = endTime
367        if (endTime > self.stakingEndTime) {
368            finalEndTime = self.stakingEndTime
369        }
370        let age = finalEndTime - currentTime
371        let score = UFix64(amount) * age
372        return score
373    }
374
375    pub fun totalStakedAmount(address: Address): Int {
376        let infos = self.stakingInfoMap[address] ?? []
377        var sum: Int = 0
378        for info in infos {
379            sum = sum + info.inscriptionAmount
380        }
381        return sum
382    }
383
384    pub fun totalStaked(): Int {
385        var sum: Int = 0
386        for infos in self.stakingInfoMap.values {
387            for info in infos {
388                sum = sum + info.inscriptionAmount
389            }
390        }
391        return sum
392    }
393
394    pub fun vaultBalance(): UFix64 {
395        return self.flowVault.balance
396    }
397
398    priv fun addModelToMap(model: @Fomopoly.StakingModel, address: Address) {
399        let mapRef: &{Address: [StakingModel]} = &self.stakingModelMap as &{ Address: [StakingModel] }
400        if mapRef[address] != nil {
401            let arr: &[StakingModel] = (&mapRef[address] as &[StakingModel]?)!
402            arr.append(<- model)
403            return
404        } else {
405            let newArr: @[StakingModel] <- [<- model]
406            mapRef[address] <-! newArr
407        }
408    }
409
410    priv fun addInfoToMap(info: Fomopoly.StakingInfo, address: Address) {
411        let infos = self.stakingInfoMap[address] ?? []
412        infos.append(info)
413        self.stakingInfoMap[address] = infos
414    }
415
416    priv fun generateStakingModel(collection: @Inscription.Collection): @StakingModel {
417        let block = getCurrentBlock()
418        return <- create StakingModel(
419            timestamp: block.timestamp,
420            inscriptionCollection: <- collection
421        )
422    }
423
424    priv fun generateStakingInfo(collection: &Inscription.Collection): StakingInfo {
425        let block = getCurrentBlock()
426        return StakingInfo(
427            timestamp: block.timestamp,
428            inscriptionAmount: collection.getIDs().length
429        )
430    }
431
432    priv fun unstakeInscription(collection: auth &Inscription.Collection) {
433        let ownerAddress = collection.owner?.address
434        assert(ownerAddress != nil, message: "Owner not found!")
435        let mapRef: &{Address: [StakingModel]} = &self.stakingModelMap as &{ Address: [StakingModel] }
436        if mapRef[ownerAddress!] != nil {
437            let models: &[StakingModel] = (&mapRef[ownerAddress!] as &[StakingModel]?)!
438            var index = 0
439            while index < models.length {
440                let model: @Fomopoly.StakingModel <- models.remove(at: index)
441                let stakedCollection <- model.withdrawCollection()
442                collection.depositCollection(collection: <- stakedCollection)
443                index = index + 1
444                destroy model
445            }
446        }
447        let storedModel <- self.stakingModelMap.remove(key: ownerAddress!)
448        destroy storedModel
449        assert(self.stakingModelMap.containsKey(ownerAddress!) == false, message: "Unstake failed!")
450    }
451
452    priv fun markStakingInfoClaimed(address: Address) {
453        let infos: [Fomopoly.StakingInfo] = self.stakingInfoMap[address] ?? []
454        let newInfos: [Fomopoly.StakingInfo] = []
455        for info in infos {
456            info.claimed = true
457            newInfos.append(info)
458        }
459        self.stakingInfoMap[address] = newInfos
460    }
461
462    pub resource StakingModel {
463        pub let timestamp: UFix64
464        pub var inscriptionCollection: @Inscription.Collection?
465
466        init(
467            timestamp: UFix64,
468            inscriptionCollection: @Inscription.Collection
469        ) {
470            self.timestamp = timestamp
471            self.inscriptionCollection <- inscriptionCollection
472        }
473
474        pub fun withdrawCollection(): @Inscription.Collection {
475            assert(self.inscriptionCollection != nil, message: "Collection not exist!")
476            let collection <- self.inscriptionCollection <- nil
477            return <- collection!
478        }
479
480        destroy() {
481            destroy self.inscriptionCollection
482        }
483    }
484
485    pub struct StakingInfo {
486        pub let timestamp: UFix64
487        pub let inscriptionAmount: Int
488        pub(set) var claimed: Bool
489
490        init(
491            timestamp: UFix64,
492            inscriptionAmount: Int
493        ) {
494            self.timestamp = timestamp
495            self.inscriptionAmount = inscriptionAmount
496            self.claimed = false
497        }
498
499    }
500
501    pub resource Administrator {
502        // updateStakingStartTime
503        //
504        pub fun updateStakingTime(start: UFix64, end: UFix64) {
505            post {
506                Fomopoly.stakingEndTime > Fomopoly.stakingStartTime: "End time should be later than start time."
507            }
508            Fomopoly.stakingStartTime = start
509            Fomopoly.stakingEndTime = end
510        }
511
512        pub fun updateStakingDivisor(divisor: UFix64) {
513            Fomopoly.stakingDivisor = divisor
514        }
515
516        pub fun updateBurningDivisor(divisor: UFix64) {
517            Fomopoly.burningDivisor = divisor
518        }
519
520        pub fun compensate(
521            receiver: Address,
522            burnedIds: [UInt64],
523            txHash: String
524        ): @Fomopoly.Vault {
525            let mintedAmount = UFix64(burnedIds.length) / Fomopoly.burningDivisor
526            emit Compensate(receiver: receiver, burnedIds: burnedIds, txHash: txHash, amount: mintedAmount)
527            Fomopoly.currentSupply = Fomopoly.currentSupply + mintedAmount
528            Fomopoly.currentMintedByBurnedSupply = Fomopoly.currentMintedByBurnedSupply + mintedAmount
529            return <- create Vault(balance: mintedAmount)
530        }
531    }
532
533    init() {
534        // Total supply of FMP is 21M
535        // 30% will minted from staking and mining
536        self.totalSupply = 21_000_000.0
537        self.mintedByBurnSupply = 4_200_000.0
538        self.mintedByMinedSupply = self.totalSupply - self.mintedByBurnSupply
539        self.currentMintedByMinedSupply = 0.0
540        self.currentMintedByBurnedSupply = 0.0
541        self.currentSupply = 0.0
542        self.stakingStartTime = 0.0
543        self.stakingEndTime = 0.0
544        self.stakingDivisor = 50.0
545        self.burningDivisor = 0.5
546
547        self.stakingModelMap <- {}
548        self.stakingInfoMap = {}
549        self.rewardClaimed = {}
550        self.flowVault <- FlowToken.createEmptyVault()
551
552        self.TokenStoragePath = /storage/fomopolyTokenVault
553        self.TokenPublicReceiverPath = /public/fomopolyTokenReceiver
554        self.TokenPublicBalancePath = /public/fomopolyTokenBalance
555        self.adminStoragePath = /storage/fomopolyAdmin
556
557        // Create the Vault with the total supply of tokens and save it in storage
558        // let vault <- create Vault(balance: self.totalSupply)
559        // self.account.save(<-vault, to: self.TokenStoragePath)
560
561        // Create a public capability to the stored Vault that only exposes
562        // the `deposit` method through the `Receiver` interface
563        // self.account.link<&Fomopoly.Vault{FungibleToken.Receiver}>(
564        //     self.TokenPublicReceiverPath,
565        //     target: self.TokenStoragePath
566        // )
567
568        // Create a public capability to the stored Vault that only exposes
569        // the `balance` field through the `Balance` interface
570        // self.account.link<&Fomopoly.Vault{FungibleToken.Balance}>(
571        //     self.TokenPublicBalancePath,
572        //     target: self.TokenStoragePath
573        // )
574
575        let admin <- create Administrator()
576        self.account.save(<-admin, to: self.adminStoragePath)
577
578        // Emit an event that shows that the contract was initialized
579        emit TokensInitialized(initialSupply: self.totalSupply)
580    }
581}
582