Smart Contract
FRC20Indexer
A.d2abb5dbf5e08666.FRC20Indexer
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# FRC20 Indexer
5
6This the main contract of FRC20, it is used to deploy and manage the FRC20 tokens.
7
8*/
9// Third-party imports
10import MetadataViews from 0x1d7e57aa55817448
11import ViewResolver from 0x1d7e57aa55817448
12import FungibleToken from 0xf233dcee88fe0abe
13import FungibleTokenMetadataViews from 0xf233dcee88fe0abe
14import FlowToken from 0x1654653399040a61
15import Burner from 0xf233dcee88fe0abe
16import BlackHole from 0x4396883a58c3a2d1
17// Fixes imports
18import Fixes from 0xd2abb5dbf5e08666
19import FixesInscriptionFactory from 0xd2abb5dbf5e08666
20import FRC20FTShared from 0xd2abb5dbf5e08666
21
22access(all) contract FRC20Indexer {
23
24 access(all) entitlement Admin
25
26 /* --- Events --- */
27 /// Event emitted when the contract is initialized
28 access(all) event ContractInitialized()
29
30 /// Event emitted when the admin calls the sponsorship method
31 access(all) event PlatformTreasurySponsorship(amount: UFix64, to: Address, forTick: String)
32 /// Event emitted when the Treasury Withdrawn invoked
33 access(all) event TokenTreasuryWithdrawn(tick: String, amount: UFix64, byInsId: UInt64, reason: String)
34
35 /// Event emitted when a FRC20 token is deployed
36 access(all) event FRC20Deployed(tick: String, max: UFix64, limit: UFix64, deployer: Address)
37 /// Event emitted when a FRC20 token is minted
38 access(all) event FRC20Minted(tick: String, amount: UFix64, to: Address)
39 /// Event emitted when the owner of an inscription is updated
40 access(all) event FRC20Transfer(tick: String, from: Address, to: Address, amount: UFix64)
41 /// Event emitted when a FRC20 token is burned
42 access(all) event FRC20Burned(tick: String, amount: UFix64, from: Address, flowExtracted: UFix64)
43 /// Event emitted when a FRC20 token is withdrawn as change
44 access(all) event FRC20WithdrawnAsChange(tick: String, amount: UFix64, from: Address)
45 /// Event emitted when a FRC20 token is deposited from change
46 access(all) event FRC20DepositedFromChange(tick: String, amount: UFix64, to: Address, from: Address)
47 /// Event emitted when a FRC20 token is set to be burnable
48 access(all) event FRC20BurnableSet(tick: String, burnable: Bool)
49 /// Event emitted when a FRC20 token is burned unsupplied tokens
50 access(all) event FRC20UnsuppliedBurned(tick: String, amount: UFix64)
51
52 /* --- Variable, Enums and Structs --- */
53 access(all)
54 let IndexerStoragePath: StoragePath
55 access(all)
56 let IndexerPublicPath: PublicPath
57
58 /* --- Interfaces & Resources --- */
59
60 /// The meta-info of a FRC20 token
61 access(all) struct FRC20Meta {
62 access(all) let tick: String
63 access(all) let max: UFix64
64 access(all) let limit: UFix64
65 access(all) let deployAt: UFix64
66 access(all) let deployer: Address
67 access(all) var burnable: Bool
68 access(all) var supplied: UFix64
69 access(all) var burned: UFix64
70
71 init(
72 tick: String,
73 max: UFix64,
74 limit: UFix64,
75 deployAt: UFix64,
76 deployer: Address,
77 supplied: UFix64,
78 burned: UFix64,
79 burnable: Bool
80 ) {
81 self.tick = tick
82 self.max = max
83 self.limit = limit
84 self.deployAt = deployAt
85 self.deployer = deployer
86 self.supplied = supplied
87 self.burned = burned
88 self.burnable = burnable
89 }
90
91 access(all)
92 fun updateSupplied(_ amt: UFix64) {
93 self.supplied = amt
94 }
95
96 access(all)
97 fun updateBurned(_ amt: UFix64) {
98 self.burned = amt
99 }
100
101 access(all)
102 fun setBurnable(_ burnable: Bool) {
103 self.burnable = burnable
104 }
105 }
106
107 access(all) resource interface IndexerPublic {
108 /* --- read-only --- */
109 /// Get all the tokens
110 access(all)
111 view fun getTokens(): [String]
112 /// Get the meta-info of a token
113 access(all)
114 view fun getTokenMeta(tick: String): FRC20Meta?
115 /// Get the token display info
116 access(all)
117 view fun getTokenDisplay(tick: String): FungibleTokenMetadataViews.FTDisplay?
118 /// Check if an inscription is a valid FRC20 inscription
119 access(all)
120 view fun isValidFRC20Inscription(ins: &Fixes.Inscription): Bool
121 /// Get the balance of a FRC20 token
122 access(all)
123 view fun getBalance(tick: String, addr: Address): UFix64
124 /// Get all balances of some address
125 access(all)
126 view fun getBalances(addr: Address): {String: UFix64}
127 /// Get the holders of a FRC20 token
128 access(all)
129 view fun getHolders(tick: String): [Address]
130 /// Get the amount of holders of a FRC20 token
131 access(all)
132 view fun getHoldersAmount(tick: String): UInt64
133 /// Get the pool balance of a FRC20 token
134 access(all)
135 view fun getPoolBalance(tick: String): UFix64
136 /// Get the benchmark value of a FRC20 token
137 access(all)
138 view fun getBenchmarkValue(tick: String): UFix64
139 /// Get the pool balance of platform treasury
140 access(all)
141 view fun getPlatformTreasuryBalance(): UFix64
142 /** ---- borrow public interface ---- */
143 /// Borrow the token's treasury $FLOW receiver
144 access(all)
145 view fun borrowTokenTreasuryReceiver(tick: String): &{FungibleToken.Receiver}
146 /// Borrow the platform treasury $FLOW receiver
147 access(all)
148 view fun borowPlatformTreasuryReceiver(): &{FungibleToken.Receiver}
149 /* --- write --- */
150 /// Deploy a new FRC20 token
151 access(all)
152 fun deploy(ins: auth(Fixes.Extractable) &Fixes.Inscription)
153 /// Mint a FRC20 token
154 access(all)
155 fun mint(ins: auth(Fixes.Extractable) &Fixes.Inscription)
156 /// Transfer a FRC20 token
157 access(all)
158 fun transfer(ins: auth(Fixes.Extractable) &Fixes.Inscription)
159 /// Burn a FRC20 token
160 access(all)
161 fun burn(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FlowToken.Vault
162 /** ---- Account Methods for listing ---- */
163 /// Ensure the balance of an address exists
164 access(account)
165 fun ensureBalanceExists(tick: String, addr: Address)
166 /// Building a selling FRC20 Token order with the sale cut from a FRC20 inscription
167 /// This method will not extract all value of the inscription
168 access(account)
169 fun buildBuyNowListing(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.ValidFrozenOrder
170 /// Building a buying FRC20 Token order with the sale cut from a FRC20 inscription
171 /// This method will not extract all value of the inscription
172 access(account)
173 fun buildSellNowListing(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.ValidFrozenOrder
174 /// Extract a part of the inscription's value to a FRC20 token change
175 access(account)
176 fun extractFlowVaultChangeFromInscription(
177 _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
178 amount: UFix64
179 ): @FRC20FTShared.Change
180 /// Apply a listed order, maker and taker should be the same token and the same amount
181 access(account)
182 fun applyBuyNowOrder(
183 makerIns: auth(Fixes.Extractable) &Fixes.Inscription,
184 takerIns: auth(Fixes.Extractable) &Fixes.Inscription,
185 maxAmount: UFix64,
186 change: @FRC20FTShared.Change
187 ): @FRC20FTShared.Change
188 /// Apply a listed order, maker and taker should be the same token and the same amount
189 access(account)
190 fun applySellNowOrder(
191 makerIns: auth(Fixes.Extractable) &Fixes.Inscription,
192 takerIns: auth(Fixes.Extractable) &Fixes.Inscription,
193 maxAmount: UFix64,
194 change: @FRC20FTShared.Change,
195 _ distributeFlowTokenFunc: (fun (UFix64, @FlowToken.Vault): Bool)
196 ): @FRC20FTShared.Change
197 /// Cancel a listed order
198 access(account)
199 fun cancelListing(listedIns: auth(Fixes.Extractable) &Fixes.Inscription, change: @FRC20FTShared.Change)
200 /// Withdraw amount of a FRC20 token by a FRC20 inscription
201 access(account)
202 fun withdrawChange(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.Change
203 /// Deposit a FRC20 token change to indexer
204 access(account)
205 fun depositChange(ins: auth(Fixes.Extractable) &Fixes.Inscription, change: @FRC20FTShared.Change)
206 /// Return the change of a FRC20 order back to the owner
207 access(account)
208 fun returnChange(change: @FRC20FTShared.Change)
209 /// Burn a FRC20 token and withdraw $FLOW by SystemBurner
210 access(account)
211 fun burnFromTreasury(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FlowToken.Vault
212 /** ---- Account Methods for command inscriptions ---- */
213 /// Set a FRC20 token to be burnable
214 access(account)
215 fun setBurnable(ins: auth(Fixes.Extractable) &Fixes.Inscription)
216 // Burn unsupplied frc20 tokens
217 access(account)
218 fun burnUnsupplied(ins: auth(Fixes.Extractable) &Fixes.Inscription)
219 /// Withdraw some $FLOW from the treasury
220 access(account)
221 fun withdrawFromTreasury(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.Change
222 /// Allocate the tokens to some address
223 access(account)
224 fun allocate(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FlowToken.Vault
225 /// Extract the ins and ensure this ins is owned by the deployer
226 access(account)
227 fun executeByDeployer(ins: auth(Fixes.Extractable) &Fixes.Inscription): Bool
228 }
229
230 /// The resource that stores the inscriptions mapping
231 ///
232 access(all) resource InscriptionIndexer: IndexerPublic {
233 /// The mapping of tokens
234 access(self)
235 let tokens: {String: FRC20Meta}
236 /// The mapping of balances
237 access(self)
238 let balances: {String: {Address: UFix64}}
239 /// The extracted balance pool of the indexer
240 access(self)
241 let pool: @{String: FlowToken.Vault}
242 /// The treasury of the indexer
243 access(self)
244 let treasury: @FlowToken.Vault
245
246 init() {
247 self.tokens = {}
248 self.balances = {}
249 self.pool <- {}
250 self.treasury <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
251 }
252
253 /* ---- Public methds ---- */
254
255 /// Get all the tokens
256 ///
257 access(all)
258 view fun getTokens(): [String] {
259 return self.tokens.keys
260 }
261
262 /// Get the meta-info of a token
263 ///
264 access(all)
265 view fun getTokenMeta(tick: String): FRC20Meta? {
266 return self.tokens[tick.toLower()]
267 }
268
269 /// Get the token display info
270 ///
271 access(all)
272 view fun getTokenDisplay(tick: String): FungibleTokenMetadataViews.FTDisplay? {
273 let ticker = tick.toLower()
274 if self.tokens[ticker] == nil {
275 return nil
276 }
277 let tickNameSize = 80 + (10 - ticker.length > 0 ? 10 - ticker.length : 0) * 12
278 let svgStr = "data:image/svg+xml;utf8,"
279 .concat("%3Csvg%20xmlns%3D%5C%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%5C%22%20viewBox%3D%5C%22-256%20-256%20512%20512%5C%22%20width%3D%5C%22512%5C%22%20height%3D%5C%22512%5C%22%3E")
280 .concat("%3Cdefs%3E%3ClinearGradient%20gradientUnits%3D%5C%22userSpaceOnUse%5C%22%20x1%3D%5C%220%5C%22%20y1%3D%5C%22-240%5C%22%20x2%3D%5C%220%5C%22%20y2%3D%5C%22240%5C%22%20id%3D%5C%22gradient-0%5C%22%20gradientTransform%3D%5C%22matrix(0.908427%2C%20-0.41805%2C%200.320369%2C%200.696163%2C%20-69.267567%2C%20-90.441103)%5C%22%3E%3Cstop%20offset%3D%5C%220%5C%22%20style%3D%5C%22stop-color%3A%20rgb(244%2C%20246%2C%20246)%3B%5C%22%3E%3C%2Fstop%3E%3Cstop%20offset%3D%5C%221%5C%22%20style%3D%5C%22stop-color%3A%20rgb(35%2C%20133%2C%2091)%3B%5C%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E")
281 .concat("%3Cellipse%20style%3D%5C%22fill%3A%20rgb(149%2C%20225%2C%20192)%3B%20stroke-width%3A%201rem%3B%20paint-order%3A%20fill%3B%20stroke%3A%20url(%23gradient-0)%3B%5C%22%20ry%3D%5C%22240%5C%22%20rx%3D%5C%22240%5C%22%3E%3C%2Fellipse%3E")
282 .concat("%3Ctext%20style%3D%5C%22dominant-baseline%3A%20middle%3B%20fill%3A%20rgb(80%2C%20213%2C%20155)%3B%20font-family%3A%20system-ui%2C%20sans-serif%3B%20text-anchor%3A%20middle%3B%5C%22%20fill-opacity%3D%5C%220.2%5C%22%20y%3D%5C%22-12%5C%22%20font-size%3D%5C%22420%5C%22%3E%F0%9D%94%89%3C%2Ftext%3E")
283 .concat("%3Ctext%20style%3D%5C%22dominant-baseline%3A%20middle%3B%20fill%3A%20rgb(244%2C%20246%2C%20246)%3B%20font-family%3A%20system-ui%2C%20sans-serif%3B%20text-anchor%3A%20middle%3B%20font-style%3A%20italic%3B%20font-weight%3A%20700%3B%5C%22%20y%3D%5C%2212%5C%22%20font-size%3D%5C%22").concat(tickNameSize.toString()).concat("%5C%22%3E")
284 .concat(ticker).concat("%3C%2Ftext%3E%3C%2Fsvg%3E")
285 let medias = MetadataViews.Medias([MetadataViews.Media(
286 file: MetadataViews.HTTPFile(url: svgStr),
287 mediaType: "image/svg+xml"
288 )])
289 return FungibleTokenMetadataViews.FTDisplay(
290 name: "FIXeS FRC20 - ".concat(ticker),
291 symbol: ticker,
292 description: "This is a FRC20 Fungible Token created by [FIXeS](https://fixes.world/).",
293 externalURL: MetadataViews.ExternalURL("https://fixes.world/"),
294 logos: medias,
295 socials: {
296 "twitter": MetadataViews.ExternalURL("https://twitter.com/flowOnFlow")
297 }
298 )
299 }
300
301 /// Get the balance of a FRC20 token
302 ///
303 access(all)
304 view fun getBalance(tick: String, addr: Address): UFix64 {
305 let balancesRef = self._borrowBalancesRef(tick: tick)
306 return balancesRef[addr] ?? 0.0
307 }
308
309 /// Get all balances of some address
310 ///
311 access(all)
312 view fun getBalances(addr: Address): {String: UFix64} {
313 let ret: {String: UFix64} = {}
314 for tick in self.tokens.keys {
315 let balancesRef = self._borrowBalancesRef(tick: tick)
316 let balance = balancesRef[addr] ?? 0.0
317 if balance > 0.0 {
318 ret[tick] = balance
319 }
320 }
321 return ret
322 }
323
324 /// Get the holders of a FRC20 token
325 access(all)
326 view fun getHolders(tick: String): [Address] {
327 let balancesRef = self._borrowBalancesRef(tick: tick.toLower())
328 return balancesRef.keys.slice(from: 0, upTo: balancesRef.length)
329 }
330
331 /// Get the amount of holders of a FRC20 token
332 access(all)
333 view fun getHoldersAmount(tick: String): UInt64 {
334 let balancesRef = self._borrowBalancesRef(tick: tick.toLower())
335 return UInt64(balancesRef.length)
336 }
337
338 /// Get the pool balance of a FRC20 token
339 ///
340 access(all)
341 view fun getPoolBalance(tick: String): UFix64 {
342 let pool = self._borrowTokenTreasury(tick: tick)
343 return pool.balance
344 }
345
346 /// Get the benchmark value of a FRC20 token
347 access(all)
348 view fun getBenchmarkValue(tick: String): UFix64 {
349 let pool = self._borrowTokenTreasury(tick: tick)
350 let meta = self.borrowTokenMeta(tick: tick)
351 let totalExisting = meta.supplied.saturatingSubtract(meta.burned)
352 if totalExisting > 0.0 {
353 return pool.balance / totalExisting
354 } else {
355 return 0.0
356 }
357 }
358
359 /// Get the pool balance of global
360 ///
361 access(all)
362 view fun getPlatformTreasuryBalance(): UFix64 {
363 return self.treasury.balance
364 }
365
366 /// Check if an inscription is a valid FRC20 inscription
367 ///
368 access(all)
369 view fun isValidFRC20Inscription(ins: &Fixes.Inscription): Bool {
370 let p = ins.getMetaProtocol()
371 return ins.getMimeType() == "text/plain" &&
372 (p == "FRC20" || p == "frc20" || p == "frc-20" || p == "FRC-20")
373 }
374
375 /** ---- borrow public interface ---- */
376
377 /// Borrow the token's treasury $FLOW receiver
378 ///
379 access(all)
380 view fun borrowTokenTreasuryReceiver(tick: String): &{FungibleToken.Receiver} {
381 let pool = self._borrowTokenTreasury(tick: tick)
382 return pool
383 }
384
385 /// Borrow the platform treasury $FLOW receiver
386 ///
387 access(all)
388 view fun borowPlatformTreasuryReceiver(): &{FungibleToken.Receiver} {
389 let pool = self._borrowPlatformTreasury()
390 return pool
391 }
392
393 // ---- Admin Methods ----
394
395 /// Extract some $FLOW from global pool to sponsor the tick deployer
396 ///
397 access(Admin)
398 fun sponsorship(
399 amount: UFix64,
400 to: Capability<&{FungibleToken.Receiver}>,
401 forTick: String,
402 ) {
403 pre {
404 amount > 0.0: "The amount should be greater than 0.0"
405 to.check() != nil: "The receiver should be a valid capability"
406 }
407
408 let recipient = to.address
409 let meta = self.borrowTokenMeta(tick: forTick)
410 // The receiver should be the deployer of the token
411 assert(
412 recipient == meta.deployer,
413 message: "The receiver should be the deployer of the token"
414 )
415
416 let platformPool = self._borrowPlatformTreasury()
417 // check the balance
418 assert(
419 platformPool.balance >= amount,
420 message: "The platform treasury does not have enough balance"
421 )
422
423 let flowReceiver = to.borrow()
424 ?? panic("The receiver should be a valid capability")
425 let supportedTypes = flowReceiver.getSupportedVaultTypes()
426 assert(
427 supportedTypes[Type<@FlowToken.Vault>()] == true,
428 message: "The receiver should support the $FLOW vault"
429 )
430
431 let flowExtracted <- platformPool.withdraw(amount: amount)
432 let sponsorAmt = flowExtracted.balance
433 flowReceiver.deposit(from: <- (flowExtracted as! @FlowToken.Vault))
434
435 emit PlatformTreasurySponsorship(
436 amount: sponsorAmt,
437 to: recipient,
438 forTick: forTick
439 )
440 }
441
442 /** ------ Functionality ------ */
443
444 /// Deploy a new FRC20 token
445 ///
446 access(all)
447 fun deploy(ins: auth(Fixes.Extractable) &Fixes.Inscription) {
448 pre {
449 ins.isExtractable(): "The inscription is not extractable"
450 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
451 }
452 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
453 assert(
454 meta["op"] == "deploy" && meta["tick"] != nil && meta["max"] != nil && meta["lim"] != nil,
455 message: "The inscription is not a valid FRC20 inscription for deployment"
456 )
457
458 let tick = meta["tick"]!.toLower()
459 assert(
460 tick.length >= 3 && tick.length <= 10,
461 message: "The token tick should be between 3 and 10 characters"
462 )
463 // ensure the first letter is lowercase a-z
464 let firstLetterUTF8 = tick[0].utf8
465 assert(
466 firstLetterUTF8.length == 1 && firstLetterUTF8[0] >= 97 && firstLetterUTF8[0] <= 122,
467 message: "The token tick should start with a lowercase letter"
468 )
469 assert(
470 self.tokens[tick] == nil && self.balances[tick] == nil && self.pool[tick] == nil,
471 message: "The token has already been deployed"
472 )
473 let max = UFix64.fromString(meta["max"]!) ?? panic("The max supply is not a valid UFix64")
474 let limit = UFix64.fromString(meta["lim"]!) ?? panic("The limit is not a valid UFix64")
475 let deployer = ins.owner!.address
476 let burnable = meta["burnable"] == "true" || meta["burnable"] == "1" // default to false
477 self.tokens[tick] = FRC20Meta(
478 tick: tick,
479 max: max,
480 limit: limit,
481 deployAt: getCurrentBlock().timestamp,
482 deployer: deployer,
483 supplied: 0.0,
484 burned: 0.0,
485 burnable: burnable
486 )
487 self.balances[tick] = {} // init the balance mapping
488 self.pool[tick] <-! FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) // init the pool
489
490 // emit event
491 emit FRC20Deployed(
492 tick: tick,
493 max: max,
494 limit: limit,
495 deployer: deployer
496 )
497
498 // extract inscription
499 self._extractInscription(tick: tick, ins: ins)
500 }
501
502 /// Mint a FRC20 token
503 ///
504 access(all)
505 fun mint(ins: auth(Fixes.Extractable) &Fixes.Inscription) {
506 pre {
507 ins.isExtractable(): "The inscription is not extractable"
508 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
509 }
510 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
511 assert(
512 meta["op"] == "mint" && meta["tick"] != nil && meta["amt"] != nil,
513 message: "The inscription is not a valid FRC20 inscription for minting"
514 )
515
516 let tick = self._parseTickerName(meta)
517 let tokenMeta = self.borrowTokenMeta(tick: tick)
518 assert(
519 tokenMeta.supplied < tokenMeta.max,
520 message: "The token has reached the max supply"
521 )
522 let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
523 assert(
524 amt > 0.0 && amt <= tokenMeta.limit,
525 message: "The amount should be greater than 0.0 and less than the limit"
526 )
527 let fromAddr = ins.owner!.address
528
529 // get the balance mapping
530 let balancesRef = self._borrowBalancesRef(tick: tick)
531
532 // check the limit
533 var amtToAdd = amt
534 if tokenMeta.supplied + amt > tokenMeta.max {
535 amtToAdd = tokenMeta.max.saturatingSubtract(tokenMeta.supplied)
536 }
537 assert(
538 amtToAdd > 0.0,
539 message: "The amount should be greater than 0.0"
540 )
541
542 // update the balance
543 self.ensureBalanceExists(tick: tick, addr: fromAddr)
544 let oldBalance = balancesRef[fromAddr] ?? panic("Failed to refer to balance")
545 balancesRef[fromAddr] = oldBalance.saturatingAdd(amtToAdd)
546
547 tokenMeta.updateSupplied(tokenMeta.supplied + amtToAdd)
548
549 // emit event
550 emit FRC20Minted(
551 tick: tick,
552 amount: amtToAdd,
553 to: fromAddr
554 )
555
556 // extract inscription
557 self._extractInscription(tick: tick, ins: ins)
558 }
559
560 /// Transfer a FRC20 token
561 ///
562 access(all)
563 fun transfer(ins: auth(Fixes.Extractable) &Fixes.Inscription) {
564 pre {
565 ins.isExtractable(): "The inscription is not extractable"
566 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
567 }
568 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
569 assert(
570 meta["op"] == "transfer" && meta["tick"] != nil && meta["amt"] != nil && meta["to"] != nil,
571 message: "The inscription is not a valid FRC20 inscription for transfer"
572 )
573
574 let tick = self._parseTickerName(meta)
575 let tokenMeta = self.borrowTokenMeta(tick: tick)
576 let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
577 let to = Address.fromString(meta["to"]!) ?? panic("The receiver is not a valid address")
578 let fromAddr = ins.owner!.address
579
580 // call the internal transfer method
581 self._transferToken(tick: tick, fromAddr: fromAddr, to: to, amt: amt)
582
583 // extract inscription
584 self._extractInscription(tick: tick, ins: ins)
585 }
586
587 /// Burn a FRC20 token
588 /// it is public so it can be called by the frc20 holders
589 ///
590 access(all)
591 fun burn(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FlowToken.Vault {
592 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
593 let tick = self._parseTickerName(meta)
594 let tokenMeta = self.borrowTokenMeta(tick: tick)
595 // this method is public, so the token should be burnable
596 assert(
597 tokenMeta.burnable,
598 message: "The token is not burnable"
599 )
600 return <- self.burnFromTreasury(ins: ins)
601 }
602
603 /// Burn a FRC20 token and withdraw $FLOW
604 /// This method is account access, so it can be called by the indexer or other contracts in the account
605 ///
606 access(account)
607 fun burnFromTreasury(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FlowToken.Vault {
608 pre {
609 ins.isExtractable(): "The inscription is not extractable"
610 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
611 }
612 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
613 assert(
614 meta["op"] == "burn" && meta["tick"] != nil && meta["amt"] != nil,
615 message: "The inscription is not a valid FRC20 inscription for burning"
616 )
617
618 let tick = self._parseTickerName(meta)
619 let tokenMeta = self.borrowTokenMeta(tick: tick)
620 assert(
621 tokenMeta.supplied > tokenMeta.burned,
622 message: "The token has been burned out"
623 )
624 let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
625 let fromAddr = ins.owner!.address
626
627 // get the balance mapping
628 let balancesRef = self._borrowBalancesRef(tick: tick)
629
630 // check the amount for from address
631 let fromBalance = balancesRef[fromAddr] ?? panic("The from address does not have a balance")
632 assert(
633 fromBalance >= amt && amt > 0.0,
634 message: "The from address does not have enough balance"
635 )
636
637 let oldBurned = tokenMeta.burned
638 balancesRef[fromAddr] = fromBalance.saturatingSubtract(amt)
639 self._burnTokenInternal(tick: tick, amountToBurn: amt)
640
641 // extract inscription
642 self._extractInscription(tick: tick, ins: ins)
643
644 // extract flow from pool
645 let flowPool = self._borrowTokenTreasury(tick: tick)
646 let restAmt = tokenMeta.supplied.saturatingSubtract(oldBurned)
647 if restAmt > 0.0 {
648 let flowTokenToExtract = flowPool.balance * amt / restAmt
649 let flowExtracted <- flowPool.withdraw(amount: flowTokenToExtract)
650 // emit event
651 emit FRC20Burned(
652 tick: tick,
653 amount: amt,
654 from: fromAddr,
655 flowExtracted: flowExtracted.balance
656 )
657 return <- (flowExtracted as! @FlowToken.Vault)
658 } else {
659 return <- (FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()))
660 }
661 }
662
663 // ---- Account Methods for command inscriptions ----
664
665 /// Set a FRC20 token to be burnable
666 ///
667 access(account)
668 fun setBurnable(ins: auth(Fixes.Extractable) &Fixes.Inscription) {
669 pre {
670 ins.isExtractable(): "The inscription is not extractable"
671 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
672 // The command inscriptions should be only executed by the indexer
673 self._isOwnedByIndexer(ins): "The inscription is not owned by the indexer"
674 }
675 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
676 assert(
677 meta["op"] == "burnable" && meta["tick"] != nil && meta["v"] != nil,
678 message: "The inscription is not a valid FRC20 inscription for setting burnable"
679 )
680
681 let tick = self._parseTickerName(meta)
682 let tokenMeta = self.borrowTokenMeta(tick: tick)
683 let isTrue = meta["v"]! == "true" || meta["v"]! == "1"
684 tokenMeta.setBurnable(isTrue)
685
686 // emit event
687 emit FRC20BurnableSet(
688 tick: tick,
689 burnable: isTrue
690 )
691
692 // extract inscription
693 self._extractInscription(tick: tick, ins: ins)
694 }
695
696 /// Burn unsupplied frc20 tokens
697 ///
698 access(account)
699 fun burnUnsupplied(ins: auth(Fixes.Extractable) &Fixes.Inscription) {
700 pre {
701 ins.isExtractable(): "The inscription is not extractable"
702 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
703 // The command inscriptions should be only executed by the indexer
704 self._isOwnedByIndexer(ins): "The inscription is not owned by the indexer"
705 }
706 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
707 assert(
708 meta["op"] == "burnUnsup" && meta["tick"] != nil && meta["perc"] != nil,
709 message: "The inscription is not a valid FRC20 inscription for burning unsupplied tokens"
710 )
711
712 let tick = self._parseTickerName(meta)
713 let tokenMeta = self.borrowTokenMeta(tick: tick)
714 // check the burnable
715 assert(
716 tokenMeta.burnable,
717 message: "The token is not burnable"
718 )
719 // check the supplied, should be less than the max
720 assert(
721 tokenMeta.supplied < tokenMeta.max,
722 message: "The token has reached the max supply"
723 )
724
725 let perc = UFix64.fromString(meta["perc"]!) ?? panic("The percentage is not a valid UFix64")
726 // check the percentage
727 assert(
728 perc > 0.0 && perc <= 1.0,
729 message: "The percentage should be greater than 0.0 and less than or equal to 1.0"
730 )
731
732 // update the burned amount
733 let totalUnsupplied = tokenMeta.max.saturatingSubtract(tokenMeta.supplied)
734 let amtToBurn = totalUnsupplied * perc
735 // update the meta-info: supplied and burned
736 tokenMeta.updateSupplied(tokenMeta.supplied.saturatingAdd(amtToBurn))
737 self._burnTokenInternal(tick: tick, amountToBurn: amtToBurn)
738
739 // emit event
740 emit FRC20UnsuppliedBurned(
741 tick: tick,
742 amount: amtToBurn
743 )
744
745 // extract inscription
746 self._extractInscription(tick: tick, ins: ins)
747 }
748
749 /// Burn unsupplied frc20 tokens
750 ///
751 access(account)
752 fun withdrawFromTreasury(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.Change {
753 pre {
754 ins.isExtractable(): "The inscription is not extractable"
755 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
756 // The command inscriptions should be only executed by the indexer
757 self._isOwnedByIndexer(ins): "The inscription is not owned by the indexer"
758 }
759 post {
760 result.isBackedByFlowTokenVault(): "The result should be backed by a FlowToken.Vault"
761 result.getBalance() > 0.0: "The result should have a positive balance"
762 }
763
764 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
765 assert(
766 meta["op"] == "withdrawFromTreasury" && meta["tick"] != nil && meta["amt"] != nil && meta["usage"] != nil,
767 message: "The inscription is not a valid FRC20 inscription for withdrawing from treasury"
768 )
769
770 let tick = self._parseTickerName(meta)
771 let tokenMeta = self.borrowTokenMeta(tick: tick)
772 let amtToWithdraw = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
773 let usage = meta["usage"]!
774 assert(
775 usage == "lottery" || usage == "staking",
776 message: "The usage should be 'lottery'"
777 )
778
779 let treasury = self._borrowTokenTreasury(tick: tick)
780 assert(
781 treasury.balance >= amtToWithdraw,
782 message: "The treasury does not have enough balance"
783 )
784
785 let ret <- FRC20FTShared.wrapFungibleVaultChange(
786 ftVault: <- treasury.withdraw(amount: amtToWithdraw),
787 from: FRC20Indexer.getAddress()
788 )
789
790 assert(
791 ret.getBalance() == amtToWithdraw,
792 message: "The result should have the same balance as the amount to withdraw"
793 )
794
795 // emit event
796 emit TokenTreasuryWithdrawn(
797 tick: tick,
798 amount: amtToWithdraw,
799 byInsId: ins.getId(),
800 reason: usage
801 )
802
803 // extract inscription
804 self._extractInscription(tick: tick, ins: ins)
805
806 return <- ret
807 }
808
809 /// Allocate the tokens to some address
810 ///
811 access(account)
812 fun allocate(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FlowToken.Vault {
813 pre {
814 ins.isExtractable(): "The inscription is not extractable"
815 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
816 }
817
818 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
819 assert(
820 meta["op"] == "alloc" && meta["tick"] != nil && meta["amt"] != nil && meta["to"] != nil,
821 message: "The inscription is not a valid FRC20 inscription for allocating"
822 )
823
824 let tick = self._parseTickerName(meta)
825 let tokenMeta = self.borrowTokenMeta(tick: tick)
826 let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
827 let to = Address.fromString(meta["to"]!) ?? panic("The receiver is not a valid address")
828 let fromAddr = FRC20Indexer.getAddress()
829
830 // call the internal transfer method
831 self._transferToken(tick: tick, fromAddr: fromAddr, to: to, amt: amt)
832
833 return <- ins.extract()
834 }
835
836 /// Extract the ins and ensure this ins is owned by the deployer
837 ///
838 access(account)
839 fun executeByDeployer(ins: auth(Fixes.Extractable) &Fixes.Inscription): Bool {
840 pre {
841 ins.isExtractable(): "The inscription is not extractable"
842 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
843 }
844
845 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
846 // only check the tick property
847 assert(
848 meta["tick"] != nil,
849 message: "The inscription is not a valid FRC20 inscription for deployer execution"
850 )
851
852 // only the deployer can execute the inscription
853 let tick = self._parseTickerName(meta)
854 let tokenMeta = self.borrowTokenMeta(tick: tick)
855
856 assert(
857 ins.owner!.address == tokenMeta.deployer,
858 message: "The inscription is not owned by the deployer"
859 )
860
861 // extract inscription
862 self._extractInscription(tick: tick, ins: ins)
863
864 return true
865 }
866
867 /** ---- Account Methods without Inscription extrasction ---- */
868
869 /// Building a selling FRC20 Token order with the sale cut from a FRC20 inscription
870 ///
871 access(account)
872 fun buildBuyNowListing(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.ValidFrozenOrder {
873 pre {
874 ins.isExtractable(): "The inscription is not extractable"
875 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
876 }
877
878 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
879 assert(
880 meta["op"] == "list-buynow" && meta["tick"] != nil && meta["amt"] != nil && meta["price"] != nil,
881 message: "The inscription is not a valid FRC20 inscription for listing"
882 )
883
884 let tick = self._parseTickerName(meta)
885 let tokenMeta = self.borrowTokenMeta(tick: tick)
886 let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
887 assert(
888 amt > 0.0,
889 message: "The amount should be greater than 0.0"
890 )
891
892 // the price here means the total price
893 let totalPrice = UFix64.fromString(meta["price"]!) ?? panic("The price is not a valid UFix64")
894
895 let benchmarkValue = self.getBenchmarkValue(tick: tick)
896 let benchmarkPrice = benchmarkValue * amt
897 assert(
898 totalPrice >= benchmarkPrice,
899 message: "The price should be greater than or equal to the benchmark value: ".concat(benchmarkValue.toString())
900 )
901 // from address
902 let fromAddr = ins.owner!.address
903
904 // create the valid frozen order
905 let order <- FRC20FTShared.createValidFrozenOrder(
906 tick: tick,
907 amount: amt,
908 totalPrice: totalPrice,
909 cuts: self._buildFRC20SaleCuts(sellerAddress: fromAddr),
910 // withdraw the token to change
911 change: <- self._withdrawToTokenChange(tick: tick, fromAddr: fromAddr, amt: amt),
912 )
913 assert(
914 order.change != nil && order.change?.isBackedByVault() == false,
915 message: "The 'BuyNow' listing change should not be backed by a vault"
916 )
917 assert(
918 order.change?.getBalance() == amt,
919 message: "The change amount should be same as the amount"
920 )
921 return <- order
922 }
923
924 /// Building a buying FRC20 Token order with the sale cut from a FRC20 inscription
925 ///
926 access(account)
927 fun buildSellNowListing(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.ValidFrozenOrder {
928 pre {
929 ins.isExtractable(): "The inscription is not extractable"
930 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
931 }
932
933 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
934 assert(
935 meta["op"] == "list-sellnow" && meta["tick"] != nil && meta["amt"] != nil && meta["price"] != nil,
936 message: "The inscription is not a valid FRC20 inscription for listing"
937 )
938
939 let tick = self._parseTickerName(meta)
940 let tokenMeta = self.borrowTokenMeta(tick: tick)
941 let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
942 assert(
943 amt > 0.0,
944 message: "The amount should be greater than 0.0"
945 )
946
947 // the price here means the total price
948 let totalPrice = UFix64.fromString(meta["price"]!) ?? panic("The price is not a valid UFix64")
949
950 let benchmarkValue = self.getBenchmarkValue(tick: tick)
951 let benchmarkPrice = benchmarkValue * amt
952 assert(
953 totalPrice >= benchmarkValue,
954 message: "The price should be greater than or equal to the benchmark value: ".concat(benchmarkValue.toString())
955 )
956
957 // create the valid frozen order
958 let order <- FRC20FTShared.createValidFrozenOrder(
959 tick: tick,
960 amount: amt,
961 totalPrice: totalPrice,
962 cuts: self._buildFRC20SaleCuts(sellerAddress: nil),
963 change: <- self.extractFlowVaultChangeFromInscription(ins, amount: totalPrice),
964 )
965 assert(
966 order.change != nil && order.change?.isBackedByFlowTokenVault() == true,
967 message: "The 'SellNow' listing change should be backed by a vault"
968 )
969 assert(
970 order.change?.getBalance() == totalPrice,
971 message: "The 'SellNow' listing change amount should be same as the amount"
972 )
973 return <- order
974 }
975
976 /// Apply a listed order, maker and taker should be the same token and the same amount
977 access(account)
978 fun applyBuyNowOrder(
979 makerIns: auth(Fixes.Extractable) &Fixes.Inscription,
980 takerIns: auth(Fixes.Extractable) &Fixes.Inscription,
981 maxAmount: UFix64,
982 change: @FRC20FTShared.Change
983 ): @FRC20FTShared.Change {
984 pre {
985 makerIns.isExtractable(): "The MAKER inscription is not extractable"
986 takerIns.isExtractable(): "The TAKER inscription is not extractable"
987 self.isValidFRC20Inscription(ins: makerIns): "The MAKER inscription is not a valid FRC20 inscription"
988 self.isValidFRC20Inscription(ins: takerIns): "The TAKER inscription is not a valid FRC20 inscription"
989 change.isBackedByVault() == false: "The change should not be backed by a vault"
990 maxAmount > 0.0: "No Enough amount to transact"
991 maxAmount <= change.getBalance(): "The max amount should be less than or equal to the change balance"
992 }
993
994 let makerMeta = FixesInscriptionFactory.parseMetadata(&makerIns.getData() as &Fixes.InscriptionData)
995 let takerMeta = FixesInscriptionFactory.parseMetadata(&takerIns.getData() as &Fixes.InscriptionData)
996
997 assert(
998 makerMeta["op"] == "list-buynow" && makerMeta["tick"] != nil && makerMeta["amt"] != nil && makerMeta["price"] != nil,
999 message: "The MAKER inscription is not a valid FRC20 inscription for listing"
1000 )
1001 assert(
1002 takerMeta["op"] == "list-take-buynow" && takerMeta["tick"] != nil && takerMeta["amt"] != nil,
1003 message: "The TAKER inscription is not a valid FRC20 inscription for taking listing"
1004 )
1005
1006 let tick = self._parseTickerName(takerMeta)
1007 assert(
1008 makerMeta["tick"]!.toLower() == tick && change.tick == tick,
1009 message: "The MAKER and TAKER should be the same token"
1010 )
1011 let takerAmt = UFix64.fromString(takerMeta["amt"]!) ?? panic("The amount is not a valid UFix64")
1012 let makerAmt = UFix64.fromString(makerMeta["amt"]!) ?? panic("The amount is not a valid UFix64")
1013
1014 // the max amount should be less than or equal to the maker amount
1015 assert(
1016 maxAmount <= makerAmt,
1017 message: "The max takeable amount should be less than or equal to the maker amount"
1018 )
1019 // set the transact amount, max
1020 let transactAmount = takerAmt > maxAmount ? maxAmount : takerAmt
1021 assert(
1022 transactAmount > 0.0 && transactAmount <= change.getBalance(),
1023 message: "The transact amount should be greater than 0.0 and less than or equal to the change balance"
1024 )
1025
1026 let makerAddr = makerIns.owner!.address
1027 let takerAddr = takerIns.owner!.address
1028 assert(
1029 makerAddr != takerAddr,
1030 message: "The MAKER and TAKER should be different address"
1031 )
1032 assert(
1033 makerAddr == change.from,
1034 message: "The MAKER should be the same address as the change from address"
1035 )
1036
1037 // withdraw the token from the maker by given amount
1038 let tokenToTransfer <- change.withdrawAsChange(amount: transactAmount)
1039
1040 // deposit the token change to the taker
1041 self._depositFromTokenChange(change: <- tokenToTransfer, to: takerAddr)
1042
1043 // extract taker's inscription
1044 self._extractInscription(tick: tick, ins: takerIns)
1045 // check rest balance in the change, if empty, extract the maker's inscription
1046 if change.isEmpty() {
1047 self._extractInscription(tick: tick, ins: makerIns)
1048 }
1049 return <- change
1050 }
1051
1052 /// Apply a listed order, maker and taker should be the same token and the same amount
1053 access(account)
1054 fun applySellNowOrder(
1055 makerIns: auth(Fixes.Extractable) &Fixes.Inscription,
1056 takerIns: auth(Fixes.Extractable) &Fixes.Inscription,
1057 maxAmount: UFix64,
1058 change: @FRC20FTShared.Change,
1059 _ distributeFlowTokenFunc: (fun (UFix64, @FlowToken.Vault): Bool)
1060 ): @FRC20FTShared.Change {
1061 pre {
1062 makerIns.isExtractable(): "The MAKER inscription is not extractable"
1063 takerIns.isExtractable(): "The TAKER inscription is not extractable"
1064 self.isValidFRC20Inscription(ins: makerIns): "The MAKER inscription is not a valid FRC20 inscription"
1065 self.isValidFRC20Inscription(ins: takerIns): "The TAKER inscription is not a valid FRC20 inscription"
1066 maxAmount > 0.0: "No Enough amount to transact"
1067 change.isBackedByFlowTokenVault() == true: "The change should be backed by a flow vault"
1068 }
1069
1070 let makerMeta = FixesInscriptionFactory.parseMetadata(&makerIns.getData() as &Fixes.InscriptionData)
1071 let takerMeta = FixesInscriptionFactory.parseMetadata(&takerIns.getData() as &Fixes.InscriptionData)
1072
1073 assert(
1074 makerMeta["op"] == "list-sellnow" && makerMeta["tick"] != nil && makerMeta["amt"] != nil && makerMeta["price"] != nil,
1075 message: "The MAKER inscription is not a valid FRC20 inscription for listing"
1076 )
1077 assert(
1078 takerMeta["op"] == "list-take-sellnow" && takerMeta["tick"] != nil && takerMeta["amt"] != nil,
1079 message: "The TAKER inscription is not a valid FRC20 inscription for taking listing"
1080 )
1081
1082 let tick = self._parseTickerName(takerMeta)
1083 assert(
1084 makerMeta["tick"]!.toLower() == tick,
1085 message: "The MAKER and TAKER should be the same token"
1086 )
1087 let takerAmt = UFix64.fromString(takerMeta["amt"]!) ?? panic("The amount is not a valid UFix64")
1088 let makerAmt = UFix64.fromString(makerMeta["amt"]!) ?? panic("The amount is not a valid UFix64")
1089
1090 // the max amount should be less than or equal to the maker amount
1091 assert(
1092 maxAmount <= makerAmt,
1093 message: "The MAKER and TAKER should be the same amount"
1094 )
1095 // set the transact amount, max
1096 let transactAmount = takerAmt > maxAmount ? maxAmount : takerAmt
1097
1098 let makerAddr = makerIns.owner!.address
1099 let takerAddr = takerIns.owner!.address
1100 assert(
1101 makerAddr != takerAddr,
1102 message: "The MAKER and TAKER should be different address"
1103 )
1104 assert(
1105 makerAddr == change.from,
1106 message: "The MAKER should be the same address as the change from address"
1107 )
1108
1109 // the price here means the total price
1110 let totalPrice = UFix64.fromString(makerMeta["price"]!) ?? panic("The price is not a valid UFix64")
1111 let partialPrice = transactAmount / makerAmt * totalPrice
1112
1113 // transfer token from taker to maker
1114 self._transferToken(tick: tick, fromAddr: takerAddr, to: makerAddr, amt: transactAmount)
1115
1116 // withdraw the token from the maker by given amount
1117 let tokenToTransfer <- change.withdrawAsVault(amount: partialPrice)
1118 let isCompleted = distributeFlowTokenFunc(transactAmount, <- (tokenToTransfer as! @FlowToken.Vault))
1119
1120 // extract inscription
1121 self._extractInscription(tick: tick, ins: takerIns)
1122 // check rest balance in the change, if empty, extract the maker's inscription
1123 if change.isEmpty() || isCompleted {
1124 self._extractInscription(tick: tick, ins: makerIns)
1125 }
1126 return <- change
1127 }
1128
1129 /// Cancel a listed order
1130 access(account)
1131 fun cancelListing(listedIns: auth(Fixes.Extractable) &Fixes.Inscription, change: @FRC20FTShared.Change) {
1132 pre {
1133 listedIns.isExtractable(): "The inscription is not extractable"
1134 self.isValidFRC20Inscription(ins: listedIns): "The inscription is not a valid FRC20 inscription"
1135 }
1136
1137 let meta = FixesInscriptionFactory.parseMetadata(&listedIns.getData() as &Fixes.InscriptionData)
1138 assert(
1139 meta["op"]?.slice(from: 0, upTo: 5) == "list-" && meta["tick"] != nil && meta["amt"] != nil && meta["price"] != nil,
1140 message: "The inscription is not a valid FRC20 inscription for listing"
1141 )
1142 let fromAddr = listedIns.owner!.address
1143 assert(
1144 fromAddr == change.from,
1145 message: "The listed owner should be the same as the change from address"
1146 )
1147
1148 // deposit the token change return to change's from address
1149 let flowReceiver = Fixes.borrowFlowTokenReceiver(fromAddr)
1150 ?? panic("The flow receiver no found")
1151 let supportedTypes = flowReceiver.getSupportedVaultTypes()
1152 assert(
1153 supportedTypes[Type<@FlowToken.Vault>()] == true,
1154 message: "The receiver should support the $FLOW vault"
1155 )
1156 // extract inscription and return flow in the inscription to the owner
1157 flowReceiver.deposit(from: <- listedIns.extract())
1158
1159 // call the return change method
1160 self.returnChange(change: <- change)
1161 }
1162
1163 /// Withdraw amount of a FRC20 token by a FRC20 inscription
1164 ///
1165 access(account)
1166 fun withdrawChange(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.Change {
1167 pre {
1168 ins.isExtractable(): "The inscription is not extractable"
1169 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
1170 }
1171
1172 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
1173 assert(
1174 meta["op"] == "withdraw" && meta["tick"] != nil && meta["amt"] != nil && meta["usage"] != nil,
1175 message: "The inscription is not a valid FRC20 inscription for transfer"
1176 )
1177
1178 let tick = self._parseTickerName(meta)
1179 let tokenMeta = self.borrowTokenMeta(tick: tick)
1180 let usage = meta["usage"]!
1181 assert(
1182 usage == "staking" || usage == "donate" || usage == "lottery" || usage == "convert" || usage == "lock" || usage == "empty",
1183 message: "The usage should be 'staking', 'donate', 'lottery', 'convert', 'lock' or 'empty'"
1184 )
1185
1186 // The from address: the owner of the inscription
1187 let fromAddr = ins.owner!.address
1188
1189 // amount can be zero, which means withdraw nothing, just execute the inscription
1190 var retChange: @FRC20FTShared.Change? <- nil
1191 let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
1192 if amt == 0.0 {
1193 // ensure usage is empty
1194 assert(
1195 usage == "empty",
1196 message: "The usage should be 'empty' if the amount is zero"
1197 )
1198 retChange <-! FRC20FTShared.createEmptyChange(tick: tick, from: fromAddr)
1199 } else {
1200 // withdraw the token to change
1201 retChange <-! self._withdrawToTokenChange(
1202 tick: tick,
1203 fromAddr: fromAddr,
1204 amt: amt
1205 )
1206 }
1207
1208 // extract inscription
1209 self._extractInscription(tick: tick, ins: ins)
1210
1211 return <- retChange!
1212 }
1213
1214 /// Deposit a FRC20 token change to indexer
1215 ///
1216 access(account)
1217 fun depositChange(ins: auth(Fixes.Extractable) &Fixes.Inscription, change: @FRC20FTShared.Change) {
1218 pre {
1219 ins.isExtractable(): "The inscription is not extractable"
1220 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
1221 change.isBackedByVault() == false: "The change should not be backed by a vault"
1222 }
1223
1224 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
1225 assert(
1226 meta["op"] == "deposit" && meta["tick"] != nil,
1227 message: "The inscription is not a valid FRC20 inscription for transfer"
1228 )
1229
1230 let tick = self._parseTickerName(meta)
1231 let tokenMeta = self.borrowTokenMeta(tick: tick)
1232 assert(
1233 tokenMeta.tick == change.tick,
1234 message: "The token should be the same as the change"
1235 )
1236 let fromAddr = ins.owner!.address
1237
1238 self._depositFromTokenChange(change: <- change, to: fromAddr)
1239
1240 // extract inscription
1241 self._extractInscription(tick: tick, ins: ins)
1242 }
1243
1244 /// Return the change of a FRC20 order back to the owner
1245 ///
1246 access(account)
1247 fun returnChange(change: @FRC20FTShared.Change) {
1248 // if the change is empty, destroy it and return
1249 if change.getBalance() == 0.0 {
1250 Burner.burn(<- change)
1251 return
1252 }
1253 let fromAddr = change.from
1254
1255 if !change.isBackedByVault() {
1256 self._depositFromTokenChange(change: <- change, to: fromAddr)
1257 } else {
1258 let vault <- change.extractAsVault()
1259 let vaultType = vault.getType()
1260 if vaultType == Type<@FlowToken.Vault>() {
1261 // deposit the token change return to change's from address
1262 let flowReceiver = Fixes.borrowFlowTokenReceiver(fromAddr)
1263 ?? panic("The flow receiver no found")
1264 assert(
1265 flowReceiver.isSupportedVaultType(type: vaultType),
1266 message: "The receiver should support the $FLOW vault"
1267 )
1268 flowReceiver.deposit(from: <- vault)
1269 } else {
1270 let ftVaultData = FRC20FTShared.resolveFTVaultDataByTicker(change.getOriginalTick())
1271 ?? panic("Failed to resolve ticker")
1272 if let ftReceiver = getAccount(fromAddr).capabilities
1273 .borrow<&{FungibleToken.Vault}>(ftVaultData.receiverPath) {
1274 assert(
1275 ftReceiver.isSupportedVaultType(type: vaultType),
1276 message: "The receiver should support the $FLOW vault"
1277 )
1278 // Transfer to the Receiver
1279 ftReceiver.deposit(from: <- vault)
1280 } else {
1281 // IF receiver does not exist, just vanish the vault.
1282 BlackHole.vanish(<- vault)
1283 }
1284 }
1285 Burner.burn(<- change)
1286 }
1287 }
1288
1289 /// Ensure the balance of an address exists
1290 ///
1291 access(account)
1292 fun ensureBalanceExists(tick: String, addr: Address) {
1293 let balancesRef = self._borrowBalancesRef(tick: tick)
1294 if balancesRef[addr] == nil {
1295 balancesRef[addr] = 0.0
1296 }
1297 }
1298
1299 /// Extract a part of the inscription's value to a FRC20 token change
1300 ///
1301 access(account)
1302 fun extractFlowVaultChangeFromInscription(
1303 _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
1304 amount: UFix64
1305 ): @FRC20FTShared.Change {
1306 pre {
1307 ins.isExtractable(): "The inscription is not extractable"
1308 self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
1309 }
1310 post {
1311 ins.isExtractable() && ins.isValueValid(): "The inscription should be extractable and the value should be valid after partial extraction"
1312 }
1313 // extract payment from the buyer's inscription, the payment should be a FLOW token
1314 // the payment should be equal to the total price and the payer should be the buyer
1315 // in the partialExtract method, the inscription will be extracted if the payment is enough
1316 // and the inscription will be still extractable.
1317 let vault <- ins.partialExtract(amount)
1318 assert(
1319 vault.balance == amount,
1320 message: "The amount should be equal to the balance of the vault"
1321 )
1322 // return the change
1323 return <- FRC20FTShared.wrapFungibleVaultChange(
1324 ftVault: <- vault,
1325 from: ins.owner!.address, // Pay $FLOW to the buyer and get the FRC20 token
1326 )
1327 }
1328
1329 /** ----- Private methods ----- */
1330
1331 /// Build the sale cuts for a FRC20 order
1332 /// - Parameters:
1333 /// - sellerAddress: The seller address, if it is nil, then it is a buy order
1334 ///
1335 access(self)
1336 fun _buildFRC20SaleCuts(sellerAddress: Address?): [FRC20FTShared.SaleCut] {
1337 let ret: [FRC20FTShared.SaleCut] = []
1338
1339 // use the shared store to get the sale fee
1340 let sharedStore = FRC20FTShared.borrowGlobalStoreRef()
1341 // Default sales fee, 2% of the total price
1342 let salesFee = (sharedStore.getByEnum(FRC20FTShared.ConfigType.PlatformSalesFee) as! UFix64?) ?? 0.02
1343 assert(
1344 salesFee > 0.0 && salesFee <= 1.0,
1345 message: "The sales fee should be greater than 0.0 and less than or equal to 1.0"
1346 )
1347
1348 // Default 40% of sales fee to the token treasury pool
1349 let treasuryPoolCut = (sharedStore.getByEnum(FRC20FTShared.ConfigType.PlatformSalesCutTreasuryPoolRatio) as! UFix64?) ?? 0.4
1350 // Default 25% of sales fee to the platform pool
1351 let platformTreasuryCut = (sharedStore.getByEnum(FRC20FTShared.ConfigType.PlatformSalesCutPlatformPoolRatio) as! UFix64?) ?? 0.25
1352 // Default 25% of sales fee to the stakers pool
1353 let platformStakersCut = (sharedStore.getByEnum(FRC20FTShared.ConfigType.PlatformSalesCutPlatformStakersRatio) as! UFix64?) ?? 0.25
1354 // Default 10% of sales fee to the marketplace portion cut
1355 let marketplacePortionCut = (sharedStore.getByEnum(FRC20FTShared.ConfigType.PlatformSalesCutMarketRatio) as! UFix64?) ?? 0.1
1356
1357 // sum of all the cuts should be 1.0
1358 let totalCutsRatio = treasuryPoolCut + platformTreasuryCut + platformStakersCut + marketplacePortionCut
1359 assert(
1360 totalCutsRatio == 1.0,
1361 message: "The sum of all the cuts should be 1.0"
1362 )
1363
1364 // add to the sale cuts
1365 // The first cut is the token treasury cut to ensure residualReceiver will be this
1366 ret.append(FRC20FTShared.SaleCut(
1367 type: FRC20FTShared.SaleCutType.TokenTreasury,
1368 ratio: salesFee * treasuryPoolCut,
1369 receiver: nil
1370 ))
1371 ret.append(FRC20FTShared.SaleCut(
1372 type: FRC20FTShared.SaleCutType.PlatformTreasury,
1373 ratio: salesFee * platformTreasuryCut,
1374 receiver: nil
1375 ))
1376 ret.append(FRC20FTShared.SaleCut(
1377 type: FRC20FTShared.SaleCutType.PlatformStakers,
1378 ratio: salesFee * platformStakersCut,
1379 receiver: nil
1380 ))
1381 ret.append(FRC20FTShared.SaleCut(
1382 type: FRC20FTShared.SaleCutType.MarketplacePortion,
1383 ratio: salesFee * marketplacePortionCut,
1384 receiver: nil
1385 ))
1386
1387 // add the seller or buyer cut
1388 if sellerAddress != nil {
1389 // borrow the receiver reference
1390 let flowTokenReceiver = getAccount(sellerAddress!)
1391 .capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
1392 assert(
1393 flowTokenReceiver.check(),
1394 message: "Could not borrow receiver reference to the seller's Vault"
1395 )
1396 ret.append(FRC20FTShared.SaleCut(
1397 type: FRC20FTShared.SaleCutType.SellMaker,
1398 ratio: (1.0 - salesFee),
1399 // recevier is the FlowToken Vault of the seller
1400 receiver: flowTokenReceiver
1401 ))
1402 } else {
1403 ret.append(FRC20FTShared.SaleCut(
1404 type: FRC20FTShared.SaleCutType.BuyTaker,
1405 ratio: (1.0 - salesFee),
1406 receiver: nil
1407 ))
1408 }
1409
1410 // check cuts amount, should be same as the total price
1411 var totalRatio: UFix64 = 0.0
1412 for cut in ret {
1413 totalRatio = totalRatio.saturatingAdd(cut.ratio)
1414 }
1415 assert(
1416 totalRatio == 1.0,
1417 message: "The sum of all the cuts should be 1.0"
1418 )
1419 // return the sale cuts
1420 return ret
1421 }
1422
1423 /// Internal Transfer a FRC20 token
1424 ///
1425 access(self)
1426 fun _transferToken(
1427 tick: String,
1428 fromAddr: Address,
1429 to: Address,
1430 amt: UFix64
1431 ) {
1432 let change <- self._withdrawToTokenChange(tick: tick, fromAddr: fromAddr, amt: amt)
1433 self._depositFromTokenChange(change: <- change, to: to)
1434
1435 // emit event
1436 emit FRC20Transfer(
1437 tick: tick,
1438 from: fromAddr,
1439 to: to,
1440 amount: amt
1441 )
1442 }
1443
1444 /// Internal Build a FRC20 token change
1445 ///
1446 access(self)
1447 fun _withdrawToTokenChange(
1448 tick: String,
1449 fromAddr: Address,
1450 amt: UFix64
1451 ): @FRC20FTShared.Change {
1452 post {
1453 result.isBackedByVault() == false: "The change should not be backed by a vault"
1454 result.getBalance() == amt: "The change balance should be same as the amount"
1455 self.getBalance(tick: tick, addr: fromAddr) == before(self.getBalance(tick: tick, addr: fromAddr)) - amt
1456 : "The from address balance should be decreased by the amount"
1457 }
1458 // borrow the balance mapping
1459 let balancesRef = self._borrowBalancesRef(tick: tick)
1460
1461 // check the amount for from address
1462 let fromBalance = balancesRef[fromAddr] ?? panic("The from address does not have a balance")
1463 assert(
1464 fromBalance >= amt && amt > 0.0,
1465 message: "The from address does not have enough balance"
1466 )
1467
1468 balancesRef[fromAddr] = fromBalance.saturatingSubtract(amt)
1469
1470 // emit event
1471 emit FRC20WithdrawnAsChange(
1472 tick: tick,
1473 amount: amt,
1474 from: fromAddr
1475 )
1476
1477 // create the frc20 token change
1478 return <- FRC20FTShared.createChange(
1479 tick: tick,
1480 from: fromAddr,
1481 balance: amt,
1482 ftVault: nil
1483 )
1484 }
1485
1486 /// Internal Deposit a FRC20 token change
1487 ///
1488 access(self)
1489 fun _depositFromTokenChange(
1490 change: @FRC20FTShared.Change,
1491 to: Address
1492 ) {
1493 pre {
1494 change.isBackedByVault() == false: "The change should not be backed by a vault"
1495 }
1496 let tick = change.tick
1497 let amt = change.extract()
1498
1499 // ensure the balance exists
1500 self.ensureBalanceExists(tick: tick, addr: to)
1501 // borrow the balance mapping
1502 let balancesRef = self._borrowBalancesRef(tick: tick)
1503 // update the balance
1504 let oldBalance = balancesRef[to] ?? panic("Failed to refer to balance")
1505 balancesRef[to] = oldBalance.saturatingAdd(amt)
1506
1507 // emit event
1508 emit FRC20DepositedFromChange(
1509 tick: tick,
1510 amount: amt,
1511 to: to,
1512 from: change.from
1513 )
1514
1515 // destroy the empty change
1516 Burner.burn(<- change)
1517 }
1518
1519 /// Internal Burn a FRC20 token
1520 ///
1521 access(self)
1522 fun _burnTokenInternal(tick: String, amountToBurn: UFix64) {
1523 let meta = self.borrowTokenMeta(tick: tick)
1524 let oldBurned = meta.burned
1525 meta.updateBurned(oldBurned.saturatingAdd(amountToBurn))
1526 }
1527
1528 /// Extract the $FLOW from inscription
1529 ///
1530 access(self)
1531 fun _extractInscription(tick: String, ins: auth(Fixes.Extractable) &Fixes.Inscription) {
1532 pre {
1533 ins.isExtractable(): "The inscription is not extractable"
1534 self.pool[tick] != nil: "The token has not been deployed"
1535 }
1536
1537 // extract the tokens
1538 let token <- ins.extract()
1539 // 5% of the extracted tokens will be sent to the treasury
1540 let amtToTreasury = token.balance * 0.05
1541 // withdraw the tokens to the treasury
1542 let tokenToTreasuryVault <- token.withdraw(amount: amtToTreasury)
1543
1544 // deposit the tokens to pool and treasury
1545 let pool = self._borrowTokenTreasury(tick: tick)
1546 let treasury = self._borrowPlatformTreasury()
1547
1548 pool.deposit(from: <- token)
1549 treasury.deposit(from: <- tokenToTreasuryVault)
1550 }
1551
1552 /// Check if an inscription is owned by the indexer
1553 ///
1554 access(self)
1555 view fun _isOwnedByIndexer(_ ins: &Fixes.Inscription): Bool {
1556 return ins.owner?.address == FRC20Indexer.getAddress()
1557 }
1558
1559 /// Parse the ticker name from the meta-info of a FRC20 inscription
1560 ///
1561 access(self)
1562 view fun _parseTickerName(_ meta: {String: String}): String {
1563 let tick = meta["tick"]?.toLower() ?? panic("The token tick is not found")
1564 assert(
1565 self.tokens[tick] != nil && self.balances[tick] != nil && self.pool[tick] != nil,
1566 message: "The token has not been deployed"
1567 )
1568 return tick
1569 }
1570
1571 /// Borrow the meta-info of a token
1572 ///
1573 access(self)
1574 view fun borrowTokenMeta(tick: String): auth(Mutate) &FRC20Meta {
1575 let meta = &self.tokens[tick.toLower()] as auth(Mutate) &FRC20Meta?
1576 return meta ?? panic("The token meta is not found")
1577 }
1578
1579 /// Borrow the balance mapping of a token
1580 ///
1581 access(self)
1582 view fun _borrowBalancesRef(tick: String): auth(Mutate) &{Address: UFix64} {
1583 let balancesRef = &self.balances[tick.toLower()] as auth(Mutate) &{Address: UFix64}?
1584 return balancesRef ?? panic("The token balance is not found")
1585 }
1586
1587 /// Borrow the token's treasury $FLOW receiver
1588 ///
1589 access(self)
1590 view fun _borrowTokenTreasury(tick: String): auth(FungibleToken.Withdraw) &FlowToken.Vault {
1591 let pool = &self.pool[tick.toLower()] as auth(FungibleToken.Withdraw) &FlowToken.Vault?
1592 return pool ?? panic("The token pool is not found")
1593 }
1594
1595 /// Borrow the platform treasury $FLOW receiver
1596 ///
1597 access(self)
1598 view fun _borrowPlatformTreasury(): auth(FungibleToken.Withdraw) &FlowToken.Vault {
1599 return &self.treasury
1600 }
1601 }
1602
1603 /* --- Public Methods --- */
1604
1605 /// Get the address of the indexer
1606 ///
1607 access(all)
1608 view fun getAddress(): Address {
1609 return self.account.address
1610 }
1611
1612 /// Get the inscription indexer
1613 ///
1614 access(all)
1615 view fun getIndexer(): &{IndexerPublic} {
1616 let addr = self.account.address
1617 let cap = getAccount(addr)
1618 .capabilities.get<&{IndexerPublic}>(self.IndexerPublicPath)
1619 .borrow()
1620 return cap ?? panic("Could not borrow InscriptionIndexer")
1621 }
1622
1623 init() {
1624 let identifier = "FRC20Indexer_".concat(self.account.address.toString())
1625 self.IndexerStoragePath = StoragePath(identifier: identifier)!
1626 self.IndexerPublicPath = PublicPath(identifier: identifier)!
1627 // create the indexer
1628 self.account.storage.save(<- create InscriptionIndexer(), to: self.IndexerStoragePath)
1629 self.account.capabilities.publish(
1630 self.account.capabilities.storage.issue<&{IndexerPublic}>(self.IndexerStoragePath),
1631 at: self.IndexerPublicPath
1632 )
1633 }
1634}
1635