Smart Contract
Fomopoly
A.88dd257fcf26d3cc.Fomopoly
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