Smart Contract

FixesTradablePool

A.d2abb5dbf5e08666.FixesTradablePool

Valid From

86,129,121

Deployed

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

Dependents

10 imports
1/**
2
3> Author: Fixes Lab <https://github.com/fixes-world/>
4
5# FixesTradablePool
6
7The FixesTradablePool contract is a bonding curve contract that allows users to buy and sell fungible tokens at a price that is determined by a bonding curve algorithm.
8The bonding curve algorithm is a mathematical formula that determines the price of a token based on the token's supply.
9The bonding curve contract is designed to be used with the FungibleToken contract, which is a standard fungible token
10contract that allows users to create and manage fungible tokens.
11
12*/
13// Standard dependencies
14import FungibleToken from 0xf233dcee88fe0abe
15import FlowToken from 0x1654653399040a61
16import FungibleTokenMetadataViews from 0xf233dcee88fe0abe
17import Burner from 0xf233dcee88fe0abe
18// Third-party dependencies
19import BlackHole from 0x4396883a58c3a2d1
20import AddressUtils from 0xa340dc0a4ec828ab
21import PublicPriceOracle from 0xec67451f8a58216a
22import SwapFactory from 0xb063c16cac85dbd1
23import SwapInterfaces from 0xb78ef7afa52ff906
24import SwapConfig from 0xb78ef7afa52ff906
25// Fixes dependencies
26import Fixes from 0xd2abb5dbf5e08666
27import FixesHeartbeat from 0xd2abb5dbf5e08666
28import FixesInscriptionFactory from 0xd2abb5dbf5e08666
29import FixesFungibleTokenInterface from 0xd2abb5dbf5e08666
30import FixesBondingCurve from 0xd2abb5dbf5e08666
31import FRC20FTShared from 0xd2abb5dbf5e08666
32import FRC20AccountsPool from 0xd2abb5dbf5e08666
33import FRC20StakingManager from 0xd2abb5dbf5e08666
34import FRC20Converter from 0xd2abb5dbf5e08666
35import FGameLottery from 0xd2abb5dbf5e08666
36
37/// The bonding curve contract.
38/// This contract allows users to buy and sell fungible tokens at a price that is determined by a bonding curve algorithm.
39///
40access(all) contract FixesTradablePool {
41
42    access(all) entitlement Manage
43
44    // ------ Events -------
45
46    // Event that is emitted when the subject fee percentage is changed.
47    access(all) event LiquidityPoolSubjectFeePercentageChanged(subject: Address, subjectFeePercentage: UFix64)
48
49    // Event that is emitted when the liquidity pool is created.
50    access(all) event LiquidityPoolCreated(tokenType: Type, curveType: Type, tokenSymbol: String, subjectFeePerc: UFix64, freeAmount: UFix64, createdBy: Address)
51
52    // Event that is emitted when the liquidity pool is initialized.
53    access(all) event LiquidityPoolInitialized(subject: Address, tokenType: Type, mintedAmount: UFix64)
54
55    /// Event that is emitted when the liquidity pool is activated.
56    access(all) event LiquidityPoolInactivated(subject: Address, tokenType: Type)
57
58    /// Event that is emitted when the liquidity is added.
59    access(all) event LiquidityAdded(subject: Address, tokenType: Type, flowAmount: UFix64)
60
61    /// Event that is emitted when the liquidity is removed.
62    access(all) event LiquidityRemoved(subject: Address, tokenType: Type, flowAmount: UFix64)
63
64    // Event that is emitted when the liquidity is transferred.
65    access(all) event LiquidityTransferred(subject: Address, pairAddr: Address, tokenType: Type, tokenAmount: UFix64, flowAmount: UFix64)
66
67    // Event that is emitted when a user buys or sells tokens.
68    access(all) event Trade(trader: Address, isBuy: Bool, subject: Address, ticker: String, tokenAmount: UFix64, flowAmount: UFix64, protocolFee: UFix64, subjectFee: UFix64, supply: UFix64)
69
70    /// -------- Resources and Interfaces --------
71
72    // Utility struct for storing an address with a UFix64 score value.
73    access(all) struct AddressWithScore {
74        access(all) let address: Address
75        access(all) let score: UFix64
76        init(_ address: Address, _ score: UFix64) {
77            self.address = address
78            self.score = score
79        }
80    }
81
82    /// Public resource interface for the Trading Center
83    ///
84    access(all) resource interface CeneterPublic {
85        /// Get the total pool amount
86        access(all)
87        view fun totalPoolAmmount(): Int
88        /// (Readonly)Query the latest pools
89        access(all)
90        fun queryLatestPools(page: Int, size: Int): [AddressWithScore]
91        /// Get the total handovered pool amount
92        access(all)
93        view fun totalHandoveredAmmount(): Int
94        /// (Readonly)Query the latest handovered pools
95        access(all)
96        fun queryLatestHandoveredPools(page: Int, size: Int): [AddressWithScore]
97        /// Get the total trending pools
98        access(all)
99        fun getTopTrendingPools(): [AddressWithScore]
100        // --------- Contract Level Write Access ---------
101        /// Invoked when a new pool is initialized
102        access(contract)
103        fun onPoolInitialized(_ poolAddr: Address)
104        /// Invoked when a pool's liquidity is handovered
105        access(contract)
106        fun onPoolLPHandovered(_ poolAddr: Address)
107        /// Invoked when a pool's Flow token is updated
108        access(contract)
109        fun onPoolFlowTokenUpdated(_ poolAddr: Address)
110    }
111
112    /// Trading Center Resource
113    ///
114    access(all) resource TradingCenter: CeneterPublic {
115        // for new list, unlimited
116        access(self) let pools: [Address]
117        access(self) let poolsAddedAt: {Address: UFix64}
118        // for finalized list, unlimited
119        access(self) let handovered: [Address]
120        access(self) let poolsHandoveredAt: {Address: UFix64}
121        // for trending list, top 100 limited
122        access(self) let trendingPools: [Address]
123        access(self) let trendingScore: {Address: UFix64}
124
125        init() {
126            self.pools = []
127            self.poolsAddedAt = {}
128            self.handovered = []
129            self.poolsHandoveredAt = {}
130            self.trendingPools = []
131            self.trendingScore = {}
132        }
133
134        // ----- Implement CeneterPublic -----
135        /// Get the total pool amount
136        access(all)
137        view fun totalPoolAmmount(): Int {
138            return self.pools.length
139        }
140
141        /// Query the latest pools
142        access(all)
143        fun queryLatestPools(page: Int, size: Int): [AddressWithScore] {
144            let start = page * size
145            let len = self.pools.length
146            var end = start + size
147            if end > len {
148                end = len
149            }
150            let addrs = self.pools.slice(from: start, upTo: end)
151            let ret: [AddressWithScore] = []
152            for addr in addrs {
153                if let time = self.poolsAddedAt[addr] {
154                    ret.append(AddressWithScore(addr, time))
155                }
156            }
157            return ret
158        }
159
160        /// Get the total handovered pool amount
161        access(all)
162        view fun totalHandoveredAmmount(): Int {
163            return self.handovered.length
164        }
165
166        /// Query the latest handovered pools
167        access(all)
168        fun queryLatestHandoveredPools(page: Int, size: Int): [AddressWithScore] {
169            let start = page * size
170            let len = self.handovered.length
171            var end = start + size
172            if end > len {
173                end = len
174            }
175            let addrs = self.handovered.slice(from: start, upTo: end)
176            let ret: [AddressWithScore] = []
177            for addr in addrs {
178                if let time = self.poolsHandoveredAt[addr] {
179                    ret.append(AddressWithScore(addr, time))
180                }
181            }
182            return ret
183        }
184
185        /// Get the total trending pools
186        access(all)
187        fun getTopTrendingPools(): [AddressWithScore] {
188            let addrs = self.trendingPools
189            let ret: [AddressWithScore] = []
190            for addr in addrs {
191                if let score = self.trendingScore[addr] {
192                    ret.append(AddressWithScore(addr, score))
193                }
194            }
195            return ret
196        }
197
198        // --------- Contract Level Write Access ---------
199
200        /// Invoked when a new pool is initialized
201        access(contract)
202        fun onPoolInitialized(_ poolAddr: Address) {
203            pre {
204                FixesTradablePool.borrowTradablePool(poolAddr) != nil: "The pool is missing"
205            }
206            // check if the pool is already added
207            if self.poolsAddedAt[poolAddr] != nil {
208                return
209            }
210            // score is now
211            let now = getCurrentBlock().timestamp
212            self.pools.insert(at: 0, poolAddr)
213            self.poolsAddedAt[poolAddr] = now
214        }
215
216        /// Invoked when a pool's liquidity is handovered
217        access(contract)
218        fun onPoolLPHandovered(_ poolAddr: Address) {
219            pre {
220                FixesTradablePool.borrowTradablePool(poolAddr) != nil: "The pool is missing"
221            }
222            // check if the pool is already added
223            if self.poolsHandoveredAt[poolAddr] != nil {
224                return
225            }
226            // score is now
227            let now = getCurrentBlock().timestamp
228            self.handovered.insert(at: 0, poolAddr)
229            self.poolsHandoveredAt[poolAddr] = now
230        }
231
232        /// Invoked when a pool's Flow token is updated
233        access(contract)
234        fun onPoolFlowTokenUpdated(_ address: Address) {
235            if let poolRef = FixesTradablePool.borrowTradablePool(address) {
236                // remove the address from the top 100
237                if let idx = self.trendingPools.firstIndex(of: address) {
238                    self.trendingPools.remove(at: idx)
239                }
240
241                // now address is not in the top 100, we need to check balance and insert it
242                let balance = poolRef.getFlowBalanceInPool()
243                if balance > 0.0 {
244                    self.trendingScore[address] = balance
245                } else {
246                    self.trendingScore.remove(key: address)
247                }
248
249                var highBalanceIdx = 0
250                var lowBalanceIdx = self.trendingPools.length - 1
251                // use binary search to find the position
252                while lowBalanceIdx >= highBalanceIdx {
253                    let mid = (lowBalanceIdx + highBalanceIdx) / 2
254                    let midAddr = self.trendingPools[mid]
255                    let midBalance = self.trendingScore[midAddr] ?? 0.0
256                    // find the position
257                    if balance > midBalance {
258                        lowBalanceIdx = mid - 1
259                    } else if balance < midBalance {
260                        highBalanceIdx = mid + 1
261                    } else {
262                        break
263                    }
264                }
265                // insert the address
266                self.trendingPools.insert(at: highBalanceIdx, address)
267                log("onPoolFlowTokenUpdated - ".concat(address.toString())
268                    .concat(" balance: ").concat(balance.toString())
269                    .concat(" rank: ").concat(highBalanceIdx.toString()))
270                // remove the last one if the length is greater than 100
271                if self.trendingPools.length > 100 {
272                    self.trendingPools.removeLast()
273                }
274            }
275        }
276    }
277
278    /// The refund agent resource.
279    ///
280    access(all) resource FRC20RefundAgent: FRC20Converter.FRC20TreasuryReceiver {
281        /// The refund pool
282        access(self)
283        let flowPool: @{FungibleToken.Vault}
284
285        init() {
286            self.flowPool <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
287        }
288
289        // ----- Implement FRC20Converter.FRC20TreasuryReceiver -----
290
291        access(all)
292        fun depositFlowToken(_ token: @{FungibleToken.Vault}) {
293            self.flowPool.deposit(from: <- token)
294        }
295
296        // ----- Internal Methods -----
297
298        /// Extract the Flow token from the pool
299        ///
300        access(contract)
301        fun extract(): @{FungibleToken.Vault} {
302            return <- self.flowPool.withdraw(amount: self.flowPool.balance)
303        }
304    }
305
306    /// The liquidity pool interface.
307    ///
308    access(all) resource interface LiquidityPoolInterface: FixesFungibleTokenInterface.ITokenBasics, FixesFungibleTokenInterface.ITokenLiquidity {
309        access(contract)
310        let curve: {FixesBondingCurve.CurveInterface}
311
312        // ----- Basics -----
313
314        /// Get the subject address
315        access(all)
316        view fun getPoolAddress(): Address {
317            return self.owner?.address ?? panic("The owner is missing")
318        }
319
320        /// Check if the liquidity is handovered
321        ///
322        access(all)
323        view fun isLiquidityHandovered(): Bool {
324            return self.isInitialized() && !self.isLocalActive() && self.getSwapPairAddress() != nil
325        }
326
327        access(all)
328        view fun getHandoveredAt(): UFix64? {
329            if self.isLiquidityHandovered() {
330                if let store = FRC20FTShared.borrowStoreRef(self.getPoolAddress()) {
331                    return store.getByEnum(FRC20FTShared.ConfigType.TradablePoolHandoveredAt) as! UFix64?
332                }
333            }
334            return nil
335        }
336
337        /// Check if the liquidity pool is initialized
338        access(all)
339        view fun isInitialized(): Bool
340
341        /// Check if the liquidity pool is active
342        access(all)
343        view fun isLocalActive(): Bool
344
345        /// Get the subject fee percentage
346        access(all)
347        view fun getSubjectFeePercentage(): UFix64
348
349        // ----- Token in the liquidity pool -----
350
351        /// Get the swap pair address
352        access(all)
353        view fun getSwapPairAddress(): Address?
354
355        /// Get the circulating supply in the tradable pool
356        access(all)
357        view fun getTradablePoolCirculatingSupply(): UFix64
358
359        /// Get the balance of the token in liquidity pool
360        access(all)
361        view fun getTokenBalanceInPool(): UFix64
362
363        /// Get the balance of the flow token in liquidity pool
364        access(all)
365        view fun getFlowBalanceInPool(): UFix64
366
367        /// Get the token price
368        access(all)
369        view fun getTokenPriceInFlow(): UFix64
370
371        /// Get the LP token price
372        access(all)
373        view fun getLPPriceInFlow(): UFix64
374
375        /// Get the burned liquidity pair amount
376        access(all)
377        view fun getBurnedLP(): UFix64
378
379        /// Get the burned token amount
380        ///
381        access(all)
382        view fun getBurnedTokenAmount(): UFix64
383
384        /// Get the locked liquidity market cap
385        ///
386        access(all)
387        fun getBurnedLiquidityMarketCap(): UFix64 {
388            return self.getBurnedLiquidityValue() * FixesTradablePool.getFlowPrice()
389        }
390
391        /// Get the burned liquidity value
392        ///
393        access(all)
394        view fun getBurnedLiquidityValue(): UFix64 {
395            if self.isLocalActive() {
396                return 0.0
397            } else {
398                let burnedLP = self.getBurnedLP()
399                let lpPrice = self.getLPPriceInFlow()
400                return burnedLP * lpPrice
401            }
402        }
403
404        /// Get the swap pair reserved info for the liquidity pool
405        /// 0 - Token0 reserve
406        /// 1 - Token1 reserve
407        /// 2 - LP token supply
408        ///
409        access(all)
410        view fun getSwapPairReservedInfo(): [UFix64; 3]?
411
412        /// Get the estimated swap amount
413        ///
414        access(all)
415        view fun getSwapEstimatedAmount(
416            _ directionTokenToFlow: Bool,
417            amount: UFix64,
418        ): UFix64
419
420        /// Get the estimated swap amount by amount out
421        ///
422        access(all)
423        view fun getSwapEstimatedAmountIn(
424            _ directionTokenToFlow: Bool,
425            amountOut: UFix64,
426        ): UFix64
427
428        /// Get the estimated token buying amount by cost
429        access(all)
430        view fun getEstimatedBuyingAmountByCost(_ cost: UFix64): UFix64
431
432        /// Get the estimated token buying cost by amount
433        access(all)
434        view fun getEstimatedBuyingCostByAmount(_ amount: UFix64): UFix64
435
436        /// Get the estimated token selling value by amount
437        access(all)
438        view fun getEstimatedSellingValueByAmount(_ amount: UFix64): UFix64
439
440        // ---- Bonding Curve ----
441
442        /// Get the curve type
443        access(all)
444        view fun getCurveType(): Type {
445            return self.curve.getType()
446        }
447
448        /// Get the free amount
449        access(all)
450        view fun getFreeAmount(): UFix64 {
451            return self.curve.getFreeAmount()
452        }
453
454        /// Get the price of the token
455        access(all)
456        view fun getUnitPrice(): UFix64 {
457            return self.curve.calculateUnitPrice(supply: self.getTradablePoolCirculatingSupply())
458        }
459
460        /// Calculate the price of buying the token based on the amount
461        access(all)
462        view fun getBuyPrice(_ amount: UFix64): UFix64 {
463            return self.curve.calculatePrice(supply: self.getTradablePoolCirculatingSupply(), amount: amount)
464        }
465
466        /// Calculate the price of selling the token based on the amount
467        access(all)
468        view fun getSellPrice(_ amount: UFix64): UFix64 {
469            return self.curve.calculatePrice(supply: self.getTradablePoolCirculatingSupply() - amount, amount: amount)
470        }
471
472        /// Calculate the price of buying the token after the subject fee
473        access(all)
474        view fun getBuyPriceAfterFee(_ amount: UFix64): UFix64 {
475            let price = self.getBuyPrice(amount)
476            let protocolFee = price * FixesTradablePool.getProtocolTradingFee()
477            let subjectFee = price * self.getSubjectFeePercentage()
478            return price + protocolFee + subjectFee
479        }
480
481        /// Calculate the price of selling the token after the subject fee
482        access(all)
483        view fun getSellPriceAfterFee(_ amount: UFix64): UFix64 {
484            let price = self.getSellPrice(amount)
485            let protocolFee = price * FixesTradablePool.getProtocolTradingFee()
486            let subjectFee = price * self.getSubjectFeePercentage()
487            return price - protocolFee - subjectFee
488        }
489
490        /// Calculate the amount of tokens that can be bought with the given cost
491        access(all)
492        view fun getBuyAmount(_ cost: UFix64): UFix64 {
493            return self.curve.calculateAmount(supply: self.getTradablePoolCirculatingSupply(), cost: cost)
494        }
495
496        /// Calculate the amount of tokens that can be bought with the given cost after the subject fee
497        ///
498        access(all)
499        view fun getBuyAmountAfterFee(_ cost: UFix64): UFix64 {
500            let protocolFee = cost * FixesTradablePool.getProtocolTradingFee()
501            let subjectFee = cost * self.getSubjectFeePercentage()
502            return self.getBuyAmount(cost - protocolFee - subjectFee)
503        }
504    }
505
506    /// The liquidity pool resource.
507    ///
508    access(all) resource TradableLiquidityPool: LiquidityPoolInterface, FixesFungibleTokenInterface.LiquidityHolder, FixesFungibleTokenInterface.IMinterHolder, FixesHeartbeat.IHeartbeatHook, FungibleToken.Receiver {
509        /// The bonding curve of the liquidity pool
510        access(contract)
511        let curve: {FixesBondingCurve.CurveInterface}
512        // The minter of the token
513        access(self)
514        let minter: @{FixesFungibleTokenInterface.IMinter}
515        // The frc20 refund agent
516        access(self)
517        let refundAgent: @FRC20RefundAgent
518        // The vault for the token
519        access(self)
520        let vault: @{FungibleToken.Vault}
521        // The vault for the flow token in the liquidity pool
522        access(self)
523        let flowVault: @FlowToken.Vault
524        /// The subject fee percentage
525        access(self)
526        var subjectFeePercentage: UFix64
527        /// If the liquidity pool is active
528        access(self)
529        var acitve: Bool
530        /// The record of LP token burned
531        access(self)
532        var lpBurned: UFix64
533        /// The trade amount
534        access(self)
535        var tradedCount: UInt64
536
537        init(
538            _ minter: @{FixesFungibleTokenInterface.IMinter},
539            _ curve: {FixesBondingCurve.CurveInterface},
540            _ subjectFeePerc: UFix64?
541        ) {
542            pre {
543                minter.getTotalAllowedMintableAmount() > 0.0: "The mint amount must be greater than 0"
544                subjectFeePerc == nil || subjectFeePerc! < 0.01: "Invalid Subject Fee"
545            }
546            self.minter <- minter
547            self.refundAgent <- create FRC20RefundAgent()
548            self.curve = curve
549            self.subjectFeePercentage = subjectFeePerc ?? 0.0
550
551            let vaultData = self.minter.getVaultData()
552            self.vault <- vaultData.createEmptyVault()
553            self.flowVault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
554            self.acitve = false
555            self.lpBurned = 0.0
556            self.tradedCount = 0
557        }
558
559        // ----- Implement LiquidityPoolAdmin -----
560
561        /// Initialize the liquidity pool
562        ///
563        access(Manage)
564        fun initialize() {
565            pre {
566                self.acitve == false: "Tradable Pool is active"
567                self.vault.balance == 0.0: "Token vault should be zero"
568                self.minter.getCurrentMintableAmount() > 0.0: "The mint amount must be greater than 0"
569            }
570            post {
571                self.minter.getCurrentMintableAmount() == 0.0: "The mint amount must be zero"
572            }
573
574            let minter = self.borrowMinter()
575            let totalMintAmount = minter.getCurrentMintableAmount()
576            let newVault <- minter.mintTokens(amount: totalMintAmount)
577            // This is a un-initialized newVault, so it contains a FixesAssetMeta.ExclusiveMeta meta data
578            // After depositing the newVault to the vault, the vault will be initialized first.
579            // But the un-removed FixesAssetMeta.ExclusiveMeta in the newVault will keep existing.
580            // So, the vault is valid but still contains the FixesAssetMeta.ExclusiveMeta.
581            self.vault.deposit(from: <- newVault)
582            self.acitve = true
583
584            // Update in the trading center
585            let tradingCenter = FixesTradablePool.borrowTradingCenter()
586            tradingCenter.onPoolInitialized(self.getPoolAddress())
587
588            // Emit the event
589            emit LiquidityPoolInitialized(
590                subject: self.getPoolAddress(),
591                tokenType: self.getTokenType(),
592                mintedAmount: totalMintAmount
593            )
594        }
595
596        /// The admin can set the subject fee percentage
597        /// The subject fee percentage must be greater than or equal to 0 and less than or equal to 0.01
598        ///
599        access(Manage)
600        fun setSubjectFeePercentage(_ subjectFeePerc: UFix64) {
601            pre {
602                subjectFeePerc >= 0.0: "The subject fee percentage must be greater than or equal to 0"
603                subjectFeePerc <= 0.01: "The subject fee percentage must be less than or equal to 0.01"
604            }
605            self.subjectFeePercentage = subjectFeePerc
606
607            // Emit the event
608            emit LiquidityPoolSubjectFeePercentageChanged(
609                subject: self.getPoolAddress(),
610                subjectFeePercentage: subjectFeePerc
611            )
612        }
613
614        // ------ Implement LiquidityPoolInterface -----
615
616        /// Check if the liquidity pool is initialized
617        access(all)
618        view fun isInitialized(): Bool {
619            return self.minter.getCurrentMintableAmount() == 0.0
620        }
621
622        /// Check if the liquidity pool is active
623        access(all)
624        view fun isLocalActive(): Bool {
625            return self.acitve
626        }
627
628        /// Get the subject fee percentage
629        access(all)
630        view fun getSubjectFeePercentage(): UFix64 {
631            return self.subjectFeePercentage
632        }
633
634        /// Get the circulating supply in the tradable pool
635        access(all)
636        view fun getTradablePoolCirculatingSupply(): UFix64 {
637            let minter = self.borrowMinter()
638            // The circulating supply is the total supply minus the balance in the vault
639            return minter.getTotalAllowedMintableAmount() - self.getTokenBalanceInPool()
640        }
641
642        /// Get the balance of the token in liquidity pool
643        access(all)
644        view fun getTokenBalanceInPool(): UFix64 {
645            return self.vault.balance
646        }
647
648        /// Get the balance of the flow token in liquidity pool
649        access(all)
650        view fun getFlowBalanceInPool(): UFix64 {
651            return self.flowVault.balance
652        }
653
654        /// Get the burned liquidity pair amount
655        access(all)
656        view fun getBurnedLP(): UFix64 {
657            return self.lpBurned
658        }
659
660        /// Get the token price
661        ///
662        access(all)
663        view fun getTokenPriceInFlow(): UFix64 {
664            if self.isLocalActive() {
665                return self.getUnitPrice()
666            }
667            return self.getSwapEstimatedAmountIn(false, amountOut: 1.0)
668        }
669
670        /// Get the estimated swap amount by amount in
671        ///
672        access(all)
673        view fun getSwapEstimatedAmount(
674            _ directionTokenToFlow: Bool,
675            amount: UFix64,
676        ): UFix64 {
677            let pairInfo = self.getSwapPairReservedInfo()
678            if pairInfo == nil {
679                return 0.0
680            }
681            let reserveToken = pairInfo![0]
682            let reserveFlow = pairInfo![1]
683
684            if reserveToken == 0.0 || reserveFlow == 0.0 {
685                return 0.0
686            }
687
688            if directionTokenToFlow {
689                return SwapConfig.getAmountOut(amountIn: amount, reserveIn: reserveToken, reserveOut: reserveFlow)
690            } else {
691                return SwapConfig.getAmountOut(amountIn: amount, reserveIn: reserveFlow, reserveOut: reserveToken)
692            }
693        }
694
695        /// Get the estimated swap amount by amount out
696        ///
697        access(all)
698        view fun getSwapEstimatedAmountIn(
699            _ directionTokenToFlow: Bool,
700            amountOut: UFix64,
701        ): UFix64 {
702            let pairInfo = self.getSwapPairReservedInfo()
703            if pairInfo == nil {
704                return 0.0
705            }
706            let reserveToken = pairInfo![0]
707            let reserveFlow = pairInfo![1]
708            if directionTokenToFlow {
709                return SwapConfig.getAmountIn(amountOut: amountOut, reserveIn: reserveToken, reserveOut: reserveFlow)
710            } else {
711                return SwapConfig.getAmountIn(amountOut: amountOut, reserveIn: reserveFlow, reserveOut: reserveToken)
712            }
713        }
714
715        /// Get the estimated token amount by cost
716        ///
717        access(all)
718        view fun getEstimatedBuyingAmountByCost(_ cost: UFix64): UFix64 {
719            if !self.isLiquidityHandovered() {
720                return self.getBuyAmountAfterFee(cost)
721            } else {
722                let protocolFee = cost * FixesTradablePool.getProtocolTradingFee()
723                return self.getSwapEstimatedAmount(false, amount: cost - protocolFee)
724            }
725        }
726
727        /// Get the estimated token buying cost by amount
728        ///
729        access(all)
730        view fun getEstimatedBuyingCostByAmount(_ amount: UFix64): UFix64 {
731            if !self.isLiquidityHandovered() {
732                return self.getBuyPriceAfterFee(amount)
733            } else {
734                let cost = self.getSwapEstimatedAmountIn(false, amountOut: amount)
735                return cost / (1.0 - FixesTradablePool.getProtocolTradingFee())
736            }
737        }
738
739        /// Get the estimated token selling value by amount
740        ///
741        access(all)
742        view fun getEstimatedSellingValueByAmount(_ amount: UFix64): UFix64 {
743            if !self.isLiquidityHandovered() {
744                return self.getSellPriceAfterFee(amount)
745            } else {
746                return self.getSwapEstimatedAmount(true, amount: amount)
747            }
748        }
749
750        /// Get the LP token price
751        ///
752        access(all)
753        view fun getLPPriceInFlow(): UFix64 {
754            if self.isLocalActive() {
755                return 0.0
756            }
757
758            let pairInfo = self.getSwapPairReservedInfo()
759            if pairInfo == nil {
760                return 0.0
761            }
762            let reserve0 = pairInfo![0]
763            let reserve1 = pairInfo![1]
764            let lpTokenSupply = pairInfo![2]
765
766            // amount1 is the price
767            let amount1 = SwapConfig.quote(amountA: 1.0, reserveA: reserve0, reserveB: reserve1)
768            let totalValueInFlow = amount1 * reserve0 + 1.0 * reserve1
769            return lpTokenSupply == 0.0 ? 0.0 : totalValueInFlow / lpTokenSupply
770        }
771
772        /// Get the burned token amount
773        ///
774        access(all)
775        view fun getBurnedTokenAmount(): UFix64 {
776            if self.isLocalActive() {
777                return 0.0
778            }
779
780            let burnedLP = self.getBurnedLP()
781            if burnedLP == 0.0 {
782                return 0.0
783            }
784            let pairInfo = self.getSwapPairReservedInfo()
785            if pairInfo == nil {
786                return 0.0
787            }
788
789            let reserve0 = pairInfo![0]
790            let reserve1 = pairInfo![1]
791            let lpTokenSupply = pairInfo![2]
792
793            let scaledBurnedLP = SwapConfig.UFix64ToScaledUInt256(burnedLP)
794            let scaledlpTokenSupply = SwapConfig.UFix64ToScaledUInt256(lpTokenSupply)
795            let scaledReserve0 = SwapConfig.UFix64ToScaledUInt256(reserve0)
796            let scaledBurnedToken0 = scaledReserve0 * scaledBurnedLP / scaledlpTokenSupply
797            return SwapConfig.ScaledUInt256ToUFix64(scaledBurnedToken0)
798        }
799
800        /// Get the swap pair reserved info for the liquidity pool
801        /// 0 - Token0 reserve
802        /// 1 - Token1 reserve
803        /// 2 - LP token supply
804        ///
805        access(all)
806        view fun getSwapPairReservedInfo(): [UFix64; 3]? {
807            let pairRef = self.borrowSwapPairRef()
808            if pairRef == nil {
809                return nil
810            }
811            let pairInfo = pairRef!.getPairInfo()
812            let tokenKey = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: self.vault.getType().identifier)
813
814            var reserve0 = 0.0
815            var reserve1 = 0.0
816            if tokenKey == (pairInfo[0] as! String) {
817                reserve0 = (pairInfo[2] as! UFix64)
818                reserve1 = (pairInfo[3] as! UFix64)
819            } else {
820                reserve0 = (pairInfo[3] as! UFix64)
821                reserve1 = (pairInfo[2] as! UFix64)
822            }
823            let lpTokenSupply = pairInfo[5] as! UFix64
824            return [reserve0, reserve1, lpTokenSupply]
825        }
826
827        /// Get the swap pair address
828        ///
829        access(all)
830        view fun getSwapPairAddress(): Address? {
831            let token0Key = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: self.vault.getType().identifier)
832            let token1Key = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: self.flowVault.getType().identifier)
833            return SwapFactory.getPairAddress(token0Key: token0Key, token1Key: token1Key)
834        }
835
836        /// Borrow the swap pair reference
837        ///
838        access(all)
839        view fun borrowSwapPairRef(): &{SwapInterfaces.PairPublic}? {
840            if let pairAddr = self.getSwapPairAddress() {
841                // ensure the pair's contract exists
842                // Note: because the pair is created by the factory, in the first transactions. the contract can not be borrowed.
843                // So, We need to ensure the pair contract exists.
844                // But that will be improved in the future.
845                let acct = getAccount(pairAddr)
846                let allNames = acct.contracts.names
847                if !allNames.contains("SwapPair") {
848                    return nil
849                }
850                // Now we can borrow the reference
851                return acct
852                    .capabilities.get<&{SwapInterfaces.PairPublic}>(SwapConfig.PairPublicPath)
853                    .borrow()
854            }
855            return nil
856        }
857
858        /// Borrow the token global public reference
859        ///
860        access(all)
861        view fun borrowTokenGlobalPublic(): &{FixesFungibleTokenInterface.IGlobalPublic} {
862            let acctsPool = FRC20AccountsPool.borrowAccountsPool()
863            let key = self.minter.getAccountsPoolKey() ?? panic("The accounts pool key is missing")
864            let contractRef = acctsPool.borrowFTContract(key) ?? panic("The FT contract reference is missing")
865            return contractRef.borrowGlobalPublic()
866        }
867
868        // ----- Implement LiquidityHolder -----
869
870        /// Check if the address is authorized user for the liquidity holder
871        ///
872        access(all)
873        view fun isAuthorizedUser(_ callerAddr: Address): Bool {
874            // singleton resources
875            let acctsPool = FRC20AccountsPool.borrowAccountsPool()
876            // The caller should be authorized user for the token
877            let key = self.minter.getAccountsPoolKey() ?? panic("The accounts pool key is missing")
878            // borrow the contract
879            let contractRef = acctsPool.borrowFTContract(key) ?? panic("The contract is missing")
880            let globalPublicRef = contractRef.borrowGlobalPublic()
881            return globalPublicRef.isAuthorizedUser(callerAddr)
882        }
883
884        /// Get the liquidity market cap
885        access(all)
886        fun getLiquidityMarketCap(): UFix64 {
887            return self.getLiquidityValue() * FixesTradablePool.getFlowPrice()
888        }
889
890        /// Get the liquidity pool value
891        access(all)
892        view fun getLiquidityValue(): UFix64 {
893            if self.isLocalActive() {
894                let flowAmount = self.getFlowBalanceInPool()
895                // The market cap is the flow amount * flow price * 2.0
896                // According to the Token value is equal to the Flow token value.
897                return flowAmount * 2.0
898            } else {
899                // current no liquidity in the pool, all LP token is burned
900                return 0.0
901            }
902        }
903
904        /// Get the token market cap
905        ///
906        access(all)
907        fun getTotalTokenMarketCap(): UFix64 {
908            return self.getTotalTokenValue() * FixesTradablePool.getFlowPrice()
909        }
910
911        /// Get token supply value
912        ///
913        access(all)
914        view fun getTotalTokenValue(): UFix64 {
915            let reservedSupply = self.getTokenBalanceInPool()
916            let circulatingSupply = self.getTotalSupply() - reservedSupply
917            let tokenPrice = self.getTokenPriceInFlow()
918            return circulatingSupply * tokenPrice
919        }
920
921        /// Get the token holders
922        access(all)
923        view fun getHolders(): UInt64 {
924            let globalInfo = self.borrowTokenGlobalPublic()
925            return globalInfo.getHoldersAmount()
926        }
927
928        /// Get the trade count
929        access(all)
930        view fun getTrades(): UInt64 {
931            return self.tradedCount
932        }
933
934        /// Pull liquidity from the pool
935        access(account)
936        fun pullLiquidity(): @{FungibleToken.Vault} {
937            if self.flowVault.balance < 1.0 {
938                return <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
939            }
940
941            // only leave 1.0 $FLOW to setup the liquidity pair
942            let liquidityAmount = self.flowVault.balance - 1.0
943            let liquidityVault <- self._withdrawFlowToken(liquidityAmount)
944
945            // All Liquidity is pulled, so the pool should be set to be inactive
946            self.transferLiquidity()
947
948            return <- liquidityVault
949        }
950
951        /// Push liquidity to the pool
952        ///
953        access(account)
954        fun addLiquidity(_ vault: @{FungibleToken.Vault}) {
955            self._depositFlowToken(<- vault)
956
957            // if not active, then try to add liquidity
958            let swapThreshold = 10.0
959            if self.isLiquidityHandovered() && self.flowVault.balance >= swapThreshold {
960                self._transferLiquidity()
961            }
962        }
963
964        /// Transfer liquidity to swap pair
965        ///
966        access(account)
967        fun transferLiquidity(): Bool {
968            // check if flow vault has enough liquidity, if not then do nothing
969            if self.flowVault.balance < 0.02 {
970                // DO NOT PANIC
971                return false
972            }
973
974            // Ensure the swap pair is created
975            self._ensureSwapPair()
976            // Transfer the liquidity to the swap pair
977            return self._transferLiquidity()
978        }
979
980        // ----- Implement FungibleToken.Receiver -----
981
982        /// Returns whether or not the given type is accepted by the Receiver
983        /// A vault that can accept any type should just return true by default
984        access(all)
985        view fun isSupportedVaultType(type: Type): Bool {
986            return type == Type<@FlowToken.Vault>()
987        }
988
989        /// A getter function that returns the token types supported by this resource,
990        /// which can be deposited using the 'deposit' function.
991        ///
992        /// @return Array of FT types that can be deposited.
993        access(all)
994        view fun getSupportedVaultTypes(): {Type: Bool} {
995            let supportedVaults: {Type: Bool} = {}
996            supportedVaults[Type<@FlowToken.Vault>()] = true
997            return supportedVaults
998        }
999
1000        // deposit
1001        //
1002        // Function that takes a Vault object as an argument and forwards
1003        // it to the recipient's Vault using the stored reference
1004        //
1005        access(all)
1006        fun deposit(from: @{FungibleToken.Vault}) {
1007            self.addLiquidity(<- from)
1008        }
1009
1010        // ----- Trade (Writable) -----
1011
1012        /// Buy the token with the given inscription
1013        /// - ins: The inscription to buy the token
1014        /// - amount: The amount of token to buy, if nil, use all the inscription value to buy the token
1015        access(all)
1016        fun buyTokens(
1017            _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
1018            _ amount: UFix64?,
1019            recipient: &{FungibleToken.Receiver},
1020        ) {
1021            pre {
1022                self.isInitialized(): "The liquidity pool is not initialized"
1023                ins.isExtractable(): "The inscription is not extractable"
1024                ins.owner?.address == recipient.owner?.address: "The inscription owner is not the recipient owner"
1025                recipient.isSupportedVaultType(type: self.getTokenType()): "The recipient does not support the token type"
1026            }
1027            post {
1028                ins.isExtracted(): "The inscription is not extracted"
1029            }
1030
1031            let flowPaymentVault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
1032            let flowAvailableAmount = ins.getInscriptionValue() - ins.getMinCost()
1033            // deposit the available Flow tokens in the inscription to the flow vault
1034            if flowAvailableAmount > 0.0 {
1035                flowPaymentVault.deposit(from: <- ins.partialExtract(flowAvailableAmount))
1036            }
1037
1038            // check inscription command
1039            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
1040            let op = meta["op"] ?? panic("The inscription operation is missing")
1041            assert(
1042                op == "exec" || op == "burn",
1043                message: "Only the 'exec' or 'burn' operation is allowed"
1044            )
1045            // if the operation is burn, then burn the frc20 token and get the flow token refunded
1046            if op == "burn" {
1047                // borrow the system burner
1048                let systemBurner = FRC20Converter.borrowSystemBurner()
1049                let refundAgent = self.borrowRefundAgent()
1050                // this method will execute the inscription command and refund the Flow tokens
1051                systemBurner.burnAndSend(ins, recipient: refundAgent)
1052                // extract the refunded $FLOW and deposit to the payment flow vault
1053                flowPaymentVault.deposit(from: <- refundAgent.extract())
1054            }
1055
1056            log("Trader: ".concat(ins.owner?.address?.toString() ?? "Unknown")
1057                .concat(" Inscription Value: ").concat(ins.getInscriptionValue().toString())
1058                .concat(" Flow AvailableAmount: ").concat(flowAvailableAmount.toString())
1059                .concat(" Flow Payment Vault: ").concat(flowPaymentVault.balance.toString())
1060            )
1061            // buy the coin with the Flow tokens
1062            let tokenVault <- self._buyTokens(ins, <- flowPaymentVault, amount, false)
1063            self._depositToken(<- tokenVault, recipient: recipient)
1064        }
1065
1066        /// Buy the coin, but a portion of the Flow tokens will be used to buy the lottery tickets
1067        ///
1068        access(all)
1069        fun buyTokensWithLottery (
1070            _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
1071            recipient: &{FungibleToken.Receiver},
1072        ): @{FungibleToken.Vault} {
1073            pre {
1074                self.isInitialized(): "The liquidity pool is not initialized"
1075                ins.isExtractable(): "The inscription is not extractable"
1076                ins.owner?.address == recipient.owner?.address: "The inscription owner is not the recipient owner"
1077                recipient.isSupportedVaultType(type: self.getTokenType()): "The recipient does not support the token type"
1078            }
1079            post {
1080                ins.isExtracted(): "The inscription is not extracted"
1081            }
1082
1083            let flowPaymentVault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
1084            let flowAvailableAmount = ins.getInscriptionValue() - ins.getMinCost()
1085            // deposit the available Flow tokens in the inscription to the flow vault
1086            if flowAvailableAmount > 0.0 {
1087                flowPaymentVault.deposit(from: <- ins.partialExtract(flowAvailableAmount))
1088            }
1089
1090            let callerAddr = ins.owner?.address ?? panic("The inscription owner is missing")
1091
1092            // borrow the minter
1093            let minter = self.borrowMinter()
1094            let meta = FixesTradablePool.verifyAndExecuteInscription(ins, symbol: minter.getSymbol(), usage: "*")
1095
1096            log("Trader: ".concat(ins.owner?.address?.toString() ?? "Unknown")
1097                .concat(" Inscription Value: ").concat(ins.getInscriptionValue().toString())
1098                .concat(" Flow AvailableAmount: ").concat(flowAvailableAmount.toString())
1099                .concat(" Flow Payment Vault: ").concat(flowPaymentVault.balance.toString())
1100            )
1101
1102            let tokenYouObtained <- self._buyTokens(ins, <- flowPaymentVault, nil, true)
1103            // check if lottery exists
1104            if let lotteryPool = FGameLottery.borrowLotteryPool(self.getPoolAddress()) {
1105                let tokenType = self.getTokenType()
1106                let internalTicker = FRC20FTShared.buildTicker(tokenType) ?? panic("The token type is invalid")
1107                // get the ticket price
1108                let ticketPice =lotteryPool.getTicketPrice()
1109                // ensure the ticket type is the same as the token type
1110                assert(
1111                    lotteryPool.getLotteryToken() == internalTicker,
1112                    message: "The lottery token is not the same as the token type"
1113                )
1114
1115                // During bonding curve is active, the lottery ticket will be bought with 10% of the token you obtained
1116                if self.isLocalActive() {
1117                    // 10% of the token you obtained will be used to buy the lottery tickets
1118                    // 90% of the token you obtained will be deposited to the recipient
1119                    var depositToUser = tokenYouObtained.balance * 0.9
1120                    // if no lottery, then deposit the token to the recipient
1121                    self._depositToken(<- tokenYouObtained.withdraw(amount: depositToUser), recipient: recipient)
1122                }
1123            } else {
1124                panic("The lottery pool does not exist")
1125            }
1126            return <- tokenYouObtained
1127        }
1128
1129        /// Internal function to buy the token with the Flow Token
1130        ///
1131        access(self)
1132        fun _buyTokens(
1133            _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
1134            _ flowPaymentVault: @FlowToken.Vault,
1135            _ targetAmount: UFix64?,
1136            _ isExtendingBuy: Bool,
1137        ): @{FungibleToken.Vault} {
1138            post {
1139                result.isInstance(self.getTokenType()):
1140                    "The result vault is not the same type as the liquidity pool. Result Type: "
1141                    .concat(result.getType().identifier)
1142                    .concat(" Pool Type: ")
1143                    .concat(self.getTokenType().identifier)
1144            }
1145
1146            let callerAddr = ins.owner?.address ?? panic("The inscription owner is missing")
1147
1148            // borrow the minter
1149            let minter = self.borrowMinter()
1150
1151            // initialize the vault by the inscription (the extracted inscription is also valid)
1152            let vaultData = minter.getVaultData()
1153            let initializedCoinVault <- minter.initializeVaultByInscription(
1154                vault: <- vaultData.createEmptyVault(),
1155                ins: ins
1156            )
1157
1158            // calculate the price
1159            var price: UFix64 = 0.0
1160            var protocolFee: UFix64 = 0.0
1161            var subjectFee: UFix64 = 0.0
1162            var buyAmount: UFix64 = 0.0
1163
1164            // currently bonding curve is active
1165            if self.isLocalActive() {
1166                if targetAmount != nil && targetAmount! > 0.0 {
1167                    buyAmount = targetAmount!
1168                    price = self.getBuyPrice(buyAmount)
1169                    protocolFee = price * FixesTradablePool.getProtocolTradingFee()
1170                    subjectFee = price * self.getSubjectFeePercentage()
1171                } else {
1172                    protocolFee = flowPaymentVault.balance * FixesTradablePool.getProtocolTradingFee()
1173                    subjectFee = flowPaymentVault.balance * self.getSubjectFeePercentage()
1174                    price = flowPaymentVault.balance - protocolFee - subjectFee
1175                    buyAmount = self.getBuyAmount(price)
1176                }
1177            } else {
1178                // the bonding curve is not active
1179                // the price is from the swap pair
1180                // we need to calculate the amount of tokens that can be bought with the given cost
1181                protocolFee = flowPaymentVault.balance * FixesTradablePool.getProtocolTradingFee()
1182                price = flowPaymentVault.balance - protocolFee
1183                buyAmount = self.getSwapEstimatedAmount(false, amount: price)
1184            }
1185
1186            // check the total cost
1187            let totalCost = price + protocolFee + subjectFee
1188
1189            log("Trader: ".concat(callerAddr.toString())
1190                .concat(" Flow Payment Vault: ").concat(flowPaymentVault.balance.toString())
1191                .concat(" Price: ").concat(price.toString())
1192                .concat(" Buy Amount: ").concat(buyAmount.toString())
1193                .concat(" Protocol Fee: ").concat(protocolFee.toString())
1194                .concat(" Subject Fee: ").concat(subjectFee.toString())
1195                .concat(" Total Cost: ").concat(totalCost.toString())
1196            )
1197
1198            assert(
1199                flowPaymentVault.isAvailableToWithdraw(amount: totalCost),
1200                message: "Insufficient payment: The total cost is greater than the available Flow tokens"
1201            )
1202
1203            // Pay the fee first
1204            let payment <- flowPaymentVault.withdraw(amount: totalCost)
1205            if protocolFee > 0.0 {
1206                let protocolFeeVault <- payment.withdraw(amount: protocolFee)
1207                let protocolFeeReceiverRef = Fixes.borrowFlowTokenReceiver(Fixes.getPlatformAddress())
1208                    ?? panic("The protocol fee destination does not have a FlowTokenReceiver capability")
1209                // split the protocol fee 40% to the staking pool and 60% to the platform
1210                let stakingFRC20Tick = FRC20FTShared.getPlatformStakingTickerName()
1211                let acctsPool = FRC20AccountsPool.borrowAccountsPool()
1212                if let poolAddr = acctsPool.getAddress(type: FRC20AccountsPool.ChildAccountType.Staking, stakingFRC20Tick) {
1213                    if let stakingFlowReciever = Fixes.borrowFlowTokenReceiver(poolAddr) {
1214                        // withdraw the tokens to the treasury
1215                        stakingFlowReciever.deposit(from: <- protocolFeeVault.withdraw(amount: protocolFee * 0.4))
1216                    }
1217                }
1218                // rest of the protocol fee goes to the platform
1219                protocolFeeReceiverRef.deposit(from: <- protocolFeeVault)
1220            }
1221            if subjectFee > 0.0 {
1222                let subjectFeeVault <- payment.withdraw(amount: subjectFee)
1223                let subjectFeeReceiverRef = Fixes.borrowFlowTokenReceiver(self.getPoolAddress())
1224                    ?? panic("The subject does not have a FlowTokenReceiver capability")
1225                subjectFeeReceiverRef.deposit(from: <- subjectFeeVault)
1226            }
1227
1228            // check the BuyAmount
1229            assert(
1230                buyAmount > 0.0,
1231                message: "The buy amount must be greater than 0"
1232            )
1233
1234            // in bonding curve, the vault has enough tokens
1235            if self.isLocalActive() {
1236                // Check if the vault has enough tokens
1237                assert(
1238                    self.vault.balance >= buyAmount,
1239                    message: "Insufficient token balance: The vault does not have enough tokens"
1240                )
1241                // deposit the tokens to the initialized return vault
1242                initializedCoinVault.deposit(from: <- self.vault.withdraw(amount: buyAmount))
1243            } else {
1244                // For the bonding curve is not active
1245                let restBalance = self.vault.balance
1246                var tokenToBuyFromSwapPair = 0.0
1247
1248                // only extending buy can buy the rest of the tokens from the bonding curve
1249                if isExtendingBuy {
1250                    if restBalance >= buyAmount {
1251                        // deposit the tokens to the initialized return vault
1252                        initializedCoinVault.deposit(from: <- self.vault.withdraw(amount: buyAmount))
1253                    } else if restBalance > 0.0 {
1254                        initializedCoinVault.deposit(from: <- self.vault.withdraw(amount: self.vault.balance))
1255                        tokenToBuyFromSwapPair = buyAmount - restBalance
1256                    } else {
1257                        tokenToBuyFromSwapPair = buyAmount
1258                    }
1259                } else {
1260                    tokenToBuyFromSwapPair = buyAmount
1261                }
1262
1263                // we need to by the rest of the tokens from the swap pair
1264                if tokenToBuyFromSwapPair > 0.0 {
1265                    let buyAmountInFlow = self.getSwapEstimatedAmountIn(false, amountOut: tokenToBuyFromSwapPair)
1266
1267                    assert(
1268                        payment.isAvailableToWithdraw(amount: buyAmountInFlow),
1269                        message: "Insufficient payment: The flow vault does not have enough tokens"
1270                    )
1271                    // swap and deposit the tokens to the initialized return vault
1272                    let pairPublicRef = self.borrowSwapPairRef() ?? panic("The swap pair is missing")
1273                    initializedCoinVault.deposit(from: <- pairPublicRef.swap(
1274                        vaultIn: <- payment.withdraw(amount: buyAmountInFlow),
1275                        exactAmountOut: nil
1276                    ))
1277                }
1278            }
1279
1280            // deposit the payment to the flow vault in the liquidity pool
1281            if payment.balance > 0.0 {
1282                self._depositFlowToken(<- payment)
1283            } else {
1284                Burner.burn(<- payment)
1285            }
1286
1287            // return remaining Flow tokens to the inscription owner
1288            if flowPaymentVault.balance > 0.0 {
1289                let ownerFlowVaultRef = Fixes.borrowFlowTokenReceiver(callerAddr)
1290                    ?? panic("The inscription owner does not have a FlowTokenReceiver capability")
1291                ownerFlowVaultRef.deposit(from: <- flowPaymentVault)
1292            } else {
1293                Burner.burn(<- flowPaymentVault)
1294            }
1295
1296            let recievedAmount = initializedCoinVault.balance
1297            // You should be able to deposit the tokens to the recipient
1298            assert(
1299                recievedAmount >= buyAmount,
1300                message: "The initialized coin vault does not have enough tokens"
1301            )
1302
1303            var sellerAddr = self.getPoolAddress()
1304            if !self.isLocalActive() {
1305                sellerAddr = self.getSwapPairAddress() ?? panic("The swap pair address is missing")
1306            }
1307
1308            // invoke the transaction hook
1309            self._onTransactionDeal(
1310                seller: sellerAddr,
1311                buyer: callerAddr,
1312                dealAmount: recievedAmount,
1313                dealPrice: totalCost
1314            )
1315
1316            // emit the trade event
1317            emit Trade(
1318                trader: callerAddr,
1319                isBuy: true,
1320                subject: self.getPoolAddress(),
1321                ticker: minter.getSymbol(),
1322                tokenAmount: recievedAmount,
1323                flowAmount: totalCost,
1324                protocolFee: protocolFee,
1325                subjectFee: subjectFee,
1326                supply: self.getTradablePoolCirculatingSupply()
1327            )
1328
1329            return <- initializedCoinVault
1330        }
1331
1332        /// execute the real transfer action
1333        ///
1334        access(self)
1335        fun _depositToken(
1336            _ tokenToTransfer: @{FungibleToken.Vault},
1337            recipient: &{FungibleToken.Receiver},
1338        ) {
1339            pre {
1340                tokenToTransfer.isInstance(self.getTokenType()): "The token type is not the same as the pool"
1341                recipient.isSupportedVaultType(type: tokenToTransfer.getType()): "The recipient does not support the token type"
1342            }
1343            // deposit the tokens to the recipient
1344            recipient.deposit(from: <- tokenToTransfer)
1345        }
1346
1347        /// Sell the token to the liquidity pool
1348        /// - tokenVault: The token vault to sell
1349        access(all)
1350        fun sellTokens(
1351            _ tokenVault: @{FungibleToken.Vault},
1352            recipient: &{FungibleToken.Receiver},
1353        ) {
1354            pre {
1355                self.isInitialized(): "The liquidity pool is not initialized"
1356                tokenVault.isInstance(self.getTokenType()): "The token vault is not the same type as the liquidity pool"
1357                tokenVault.balance > 0.0: "The token vault balance must be greater than 0"
1358                recipient.isSupportedVaultType(type: Type<@FlowToken.Vault>()): "The recipient does not support FlowToken.Vault"
1359            }
1360
1361            if !self.isLocalActive() {
1362                self.quickSwapToken(<- tokenVault, recipient)
1363                return
1364            }
1365
1366            let minter = self.borrowMinter()
1367            // calculate the price
1368            let totalPrice = self.getSellPrice(tokenVault.balance)
1369            assert(
1370                totalPrice > 0.0,
1371                message: "The total payment must be greater than 0"
1372            )
1373            assert(
1374                self.flowVault.balance >= totalPrice,
1375                message: "Insufficient payment: The flow vault does not have enough tokens"
1376            )
1377
1378            // withdraw the tokens from the vault
1379            let payment <- self._withdrawFlowToken(totalPrice)
1380
1381            let protocolFee = totalPrice * FixesTradablePool.getProtocolTradingFee()
1382            let subjectFee = totalPrice * self.getSubjectFeePercentage()
1383            // withdraw the protocol fee from the flow vault
1384            if protocolFee > 0.0 {
1385                let protocolFeeVault <- payment.withdraw(amount: protocolFee)
1386                let protocolFeeReceiverRef = Fixes.borrowFlowTokenReceiver(Fixes.getPlatformAddress())
1387                    ?? panic("The protocol fee destination does not have a FlowTokenReceiver capability")
1388                // split the protocol fee 40% to the staking pool and 60% to the platform
1389                let stakingFRC20Tick = FRC20FTShared.getPlatformStakingTickerName()
1390                let acctsPool = FRC20AccountsPool.borrowAccountsPool()
1391                if let poolAddr = acctsPool.getAddress(type: FRC20AccountsPool.ChildAccountType.Staking, stakingFRC20Tick) {
1392                    if let stakingFlowReciever = Fixes.borrowFlowTokenReceiver(poolAddr) {
1393                        // withdraw the tokens to the treasury
1394                        stakingFlowReciever.deposit(from: <- protocolFeeVault.withdraw(amount: protocolFee * 0.4))
1395                    }
1396                }
1397                // rest of the protocol fee goes to the platform
1398                protocolFeeReceiverRef.deposit(from: <- protocolFeeVault)
1399            }
1400            // withdraw the subject fee from the flow vault
1401            if subjectFee > 0.0 {
1402                let subjectFeeVault <- payment.withdraw(amount: subjectFee)
1403                let subjectFeeReceiverRef = Fixes.borrowFlowTokenReceiver(self.getPoolAddress())
1404                    ?? panic("The subject does not have a FlowTokenReceiver capability")
1405                subjectFeeReceiverRef.deposit(from: <- subjectFeeVault)
1406            }
1407            // withdraw the user fund from the flow vault
1408            recipient.deposit(from: <- payment)
1409
1410            // deposit the tokens to the token vault
1411            let tokenAmount = tokenVault.balance
1412            self.vault.deposit(from: <- tokenVault)
1413
1414            // emit the trade event
1415            let poolAddr = self.getPoolAddress()
1416            let traderAddr = recipient.owner?.address ?? panic("The recipient owner is missing")
1417
1418            // invoke the transaction hook
1419            self._onTransactionDeal(
1420                seller: traderAddr,
1421                buyer: poolAddr,
1422                dealAmount: tokenAmount,
1423                dealPrice: totalPrice
1424            )
1425
1426            emit Trade(
1427                trader: traderAddr,
1428                isBuy: false,
1429                subject: self.getPoolAddress(),
1430                ticker: minter.getSymbol(),
1431                tokenAmount: tokenAmount,
1432                flowAmount: totalPrice,
1433                protocolFee: protocolFee,
1434                subjectFee: subjectFee,
1435                supply: self.getTradablePoolCirculatingSupply()
1436            )
1437        }
1438
1439        /// Swap all tokens in the vault and deposit to the token out recipient
1440        /// Only invokable when the local pool is not active
1441        ///
1442        access(all)
1443        fun quickSwapToken(
1444            _ tokenInVault: @{FungibleToken.Vault},
1445            _ tokenOutRecipient: &{FungibleToken.Receiver},
1446        ) {
1447            pre {
1448                self.isInitialized(): "The liquidity pool is not initialized"
1449                !self.isLocalActive(): "The liquidity pool is active"
1450                tokenInVault.balance > 0.0: "The token vault balance must be greater than 0"
1451                tokenInVault.isInstance(self.getTokenType()) || tokenInVault.isInstance(Type<@FlowToken.Vault>()): "The token vault is not the same type as the liquidity pool"
1452            }
1453            let supportTypes = tokenOutRecipient.getSupportedVaultTypes()
1454            let minterTokenType = self.getTokenType()
1455            let tokenInVaultType = tokenInVault.getType()
1456            let isTokenInFlowToken = tokenInVault.isInstance(Type<@FlowToken.Vault>())
1457            // check if the recipient supports the token type
1458            if isTokenInFlowToken {
1459                assert(
1460                    supportTypes[minterTokenType] == true,
1461                    message: "The recipient does not support FungibleToken.Vault"
1462                )
1463            } else {
1464                assert(
1465                    supportTypes[Type<@FlowToken.Vault>()] == true,
1466                    message: "The recipient does not support FlowToken.Vault"
1467                )
1468            }
1469
1470            let pairPublicRef = self.borrowSwapPairRef() ?? panic("The swap pair is missing")
1471            let tokenInAmount = tokenInVault.balance
1472            let tokenOutVault <- pairPublicRef.swap(
1473                vaultIn: <- tokenInVault,
1474                exactAmountOut: nil
1475            )
1476            let tokenOutAmount = tokenOutVault.balance
1477
1478            // parameters for the event
1479            let symbol = self.minter.getSymbol()
1480            let pairAddr = pairPublicRef.owner?.address ?? panic("The pair owner is missing")
1481            let traderAddr = tokenOutRecipient.owner?.address ?? panic("The recipient owner is missing")
1482            let tradeTokenAmount = isTokenInFlowToken ? tokenOutAmount : tokenInAmount
1483            let tradeTokenPrice = isTokenInFlowToken ? tokenInAmount : tokenOutAmount
1484
1485            tokenOutRecipient.deposit(from: <- tokenOutVault)
1486
1487            // emit the trade event
1488            self._onTransactionDeal(
1489                seller: isTokenInFlowToken ? pairAddr : traderAddr,
1490                buyer: isTokenInFlowToken ? traderAddr : pairAddr,
1491                dealAmount: tradeTokenAmount,
1492                dealPrice: tradeTokenPrice
1493            )
1494
1495            emit Trade(
1496                trader: traderAddr,
1497                isBuy: isTokenInFlowToken,
1498                subject: self.getPoolAddress(),
1499                ticker: symbol,
1500                tokenAmount:tradeTokenAmount,
1501                flowAmount: tradeTokenPrice,
1502                protocolFee: 0.0,
1503                subjectFee: 0.0,
1504                supply: self.getTradablePoolCirculatingSupply()
1505            )
1506        }
1507
1508        // ----- Internal Methods -----
1509
1510        /// The hook that is invoked when a deal is executed
1511        ///
1512        access(self)
1513        fun _onTransactionDeal(
1514            seller: Address,
1515            buyer: Address,
1516            dealAmount: UFix64,
1517            dealPrice: UFix64,
1518        ) {
1519            // update the traded count
1520            self.tradedCount = self.tradedCount + 1
1521
1522            let minter = self.borrowMinter()
1523            // for fixes fungible token, the ticker is $ + {symbol}
1524            let tickerName = "$".concat(minter.getSymbol())
1525
1526            // ------- start -- Invoke Hooks --------------
1527            // Invoke transaction hooks to do things like:
1528            // -- Record the transction record
1529            // -- Record trading Volume
1530
1531            // for TradablePool hook
1532            let poolAddr = self.getPoolAddress()
1533            // Buyer or Seller should be the pool address
1534            let isInPoolTrading = buyer == poolAddr || seller == poolAddr
1535            let storefront = isInPoolTrading ? poolAddr : (self.getSwapPairAddress() ?? poolAddr)
1536
1537            // invoke the buyer transaction hook
1538            if let buyerTransactionHook = FRC20FTShared.borrowTransactionHook(buyer) {
1539                buyerTransactionHook.onDeal(
1540                    seller: seller,
1541                    buyer: buyer,
1542                    tick: tickerName,
1543                    dealAmount: dealAmount,
1544                    dealPrice: dealPrice,
1545                    storefront: storefront,
1546                    listingId: nil,
1547                )
1548            }
1549
1550            // invoke the seller transaction hook
1551            if let sellerTransactionHook = FRC20FTShared.borrowTransactionHook(seller) {
1552                sellerTransactionHook.onDeal(
1553                    seller: seller,
1554                    buyer: buyer,
1555                    tick: tickerName,
1556                    dealAmount: dealAmount,
1557                    dealPrice: dealPrice,
1558                    storefront: storefront,
1559                    listingId: nil,
1560                )
1561            }
1562
1563            // invoke the pool transaction hook for non-pool trading
1564            if !isInPoolTrading {
1565                if let poolTransactionHook = FRC20FTShared.borrowTransactionHook(poolAddr) {
1566                    poolTransactionHook.onDeal(
1567                        seller: seller,
1568                        buyer: buyer,
1569                        tick: tickerName,
1570                        dealAmount: dealAmount,
1571                        dealPrice: dealPrice,
1572                        storefront: storefront,
1573                        listingId: nil,
1574                    )
1575                }
1576            }
1577
1578            // invoke the system transaction hook
1579            if let systemTransactionHook = FRC20FTShared.borrowTransactionHook(Fixes.getPlatformAddress()) {
1580                systemTransactionHook.onDeal(
1581                    seller: seller,
1582                    buyer: buyer,
1583                    tick: tickerName,
1584                    dealAmount: dealAmount,
1585                    dealPrice: dealPrice,
1586                    storefront: storefront,
1587                    listingId: nil,
1588                )
1589            }
1590        }
1591
1592        /// Borrow the refund agent
1593        ///
1594        access(self)
1595        view fun borrowRefundAgent(): &FRC20RefundAgent {
1596            return &self.refundAgent
1597        }
1598
1599        // ----- Implement IMinterHolder -----
1600
1601        access(contract)
1602        view fun borrowMinter(): auth(FixesFungibleTokenInterface.Manage) &{FixesFungibleTokenInterface.IMinter} {
1603            return &self.minter
1604        }
1605
1606        // ----- Implement IHeartbeatHook -----
1607
1608        /// The methods that is invoked when the heartbeat is executed
1609        /// Before try-catch is deployed, please ensure that there will be no panic inside the method.
1610        ///
1611        access(account)
1612        fun onHeartbeat(_ deltaTime: UFix64) {
1613            // if active, then move the liquidity pool to the next stage
1614            if self.isLocalActive() {
1615                // Check the market cap
1616                let totalMarketCap = self.getTotalTokenMarketCap()
1617                let targetMarketCap = FixesTradablePool.getTargetMarketCap()
1618
1619                // if the market cap is less than the target market cap, then do nothing
1620                if totalMarketCap < targetMarketCap {
1621                    // DO NOT PANIC
1622                    return
1623                }
1624            }
1625
1626            // if the liquidity is handovered but the flow vault is empty, then do nothing
1627            if self.isLiquidityHandovered() && self.flowVault.balance < 1.0 {
1628                // DO NOT PANIC
1629                return
1630            }
1631
1632            // Transfer the liquidity to the swap pair
1633            self.transferLiquidity()
1634        }
1635
1636        // ----- Internal Methods -----
1637
1638        /// Withdraw the flow token from the pool
1639        ///
1640        access(self)
1641        fun _withdrawFlowToken(_ amount: UFix64): @{FungibleToken.Vault} {
1642            pre {
1643                amount > 0.0: "The amount must be greater than 0"
1644            }
1645            post {
1646                result.balance == amount: "The returned vault balance must be equal to the amount"
1647                self.flowVault.balance == before(self.flowVault.balance) - amount: "The flow vault balance must be decreased"
1648            }
1649            let vault <- self.flowVault.withdraw(amount: amount)
1650
1651            // Update in the trading center
1652            let tradingCenter = FixesTradablePool.borrowTradingCenter()
1653            tradingCenter.onPoolFlowTokenUpdated(self.getPoolAddress())
1654
1655            // Emit the event
1656            emit LiquidityRemoved(
1657                subject: self.getPoolAddress(),
1658                tokenType: self.getTokenType(),
1659                flowAmount: amount
1660            )
1661
1662            return <- vault
1663        }
1664
1665        /// Deposit the flow token to the pool
1666        ///
1667        access(self)
1668        fun _depositFlowToken(_ vault: @{FungibleToken.Vault}) {
1669            post {
1670                self.flowVault.balance == before(self.flowVault.balance) + before(vault.balance): "The flow vault balance must be increased"
1671            }
1672            let amount = vault.balance
1673            if amount > 0.0 {
1674                self.flowVault.deposit(from: <- vault)
1675            } else {
1676                Burner.burn(<- vault)
1677                return
1678            }
1679
1680            // Update in the trading center
1681            let tradingCenter = FixesTradablePool.borrowTradingCenter()
1682            tradingCenter.onPoolFlowTokenUpdated(self.getPoolAddress())
1683
1684            // Emit the event
1685            emit LiquidityAdded(
1686                subject: self.getPoolAddress(),
1687                tokenType: self.getTokenType(),
1688                flowAmount: amount
1689            )
1690        }
1691
1692        /// Ensure the swap pair is created
1693        ///
1694        access(self)
1695        fun _ensureSwapPair() {
1696            var pairAddr = self.getSwapPairAddress()
1697            // if the pair is not created, then create the pair
1698            if pairAddr == nil && self.flowVault.balance >= 0.01 {
1699                // Now we can add liquidity to the swap pair
1700                let minterRef = self.borrowMinter()
1701                let vaultData = minterRef.getVaultData()
1702
1703                // create the account creation fee vault
1704                let acctCreateFeeVault <- self._withdrawFlowToken(0.01)
1705                // create the pair
1706                SwapFactory.createPair(
1707                    token0Vault: <- vaultData.createEmptyVault(),
1708                    token1Vault: <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()),
1709                    accountCreationFee: <- acctCreateFeeVault,
1710                    stableMode: false
1711                )
1712            }
1713        }
1714
1715        /// Create a new pair and add liquidity
1716        ///
1717        access(self)
1718        fun _transferLiquidity(): Bool {
1719            // Now we can add liquidity to the swap pair
1720            let minterRef = self.borrowMinter()
1721            let vaultData = minterRef.getVaultData()
1722
1723            // add all liquidity to the pair
1724            let pairPublicRef = self.borrowSwapPairRef()
1725            if pairPublicRef == nil {
1726                // DO NOT PANIC
1727                return false
1728            }
1729
1730            // Token0 => self.vault, Token1 => self.flowVault
1731            let tokenKey = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: self.vault.getType().identifier)
1732            // get the pair info
1733            let pairInfo = pairPublicRef!.getPairInfo()
1734            var token0Reserve = 0.0
1735            var token1Reserve = 0.0
1736            if tokenKey == (pairInfo[0] as! String) {
1737                token0Reserve = (pairInfo[2] as! UFix64)
1738                token1Reserve = (pairInfo[3] as! UFix64)
1739            } else {
1740                token0Reserve = (pairInfo[3] as! UFix64)
1741                token1Reserve = (pairInfo[2] as! UFix64)
1742            }
1743
1744            // the vaults for adding liquidity
1745            let token0Vault <- vaultData.createEmptyVault()
1746            let token1Vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
1747
1748            // add all the token to the pair
1749            if self.isLocalActive() {
1750                let token0Max = self.getTokenBalanceInPool()
1751                let token1Max = self.getFlowBalanceInPool()
1752                // init the liquidity pool with current price
1753                let tokenInPoolPrice = self.getUnitPrice()
1754                var token1In = token1Max
1755                var token0In = token1In / tokenInPoolPrice
1756
1757                // setup swap pair based on current price
1758                if token0Reserve != 0.0 || token1Reserve != 0.0 {
1759                    let scaledTokenInPoolPrice = SwapConfig.UFix64ToScaledUInt256(tokenInPoolPrice)
1760                    let scaledToken0Reserve = SwapConfig.UFix64ToScaledUInt256(token0Reserve)
1761                    let scaledToken1Reserve = SwapConfig.UFix64ToScaledUInt256(token1Reserve)
1762                    // let the price close to the tokenInPoolPrice
1763                    let amountInScaled = SwapConfig.UFix64ToScaledUInt256(1.0)
1764                    let amountInWithFeeScaled = SwapConfig.UFix64ToScaledUInt256(0.997) * amountInScaled / SwapConfig.scaleFactor
1765                    let scaledSwapPrice = amountInWithFeeScaled * scaledToken1Reserve / (scaledToken0Reserve + amountInWithFeeScaled)
1766
1767                    // we need ensure the final swap price is quite close to the tokenInPoolPrice
1768                    let sf = SwapConfig.scaleFactor
1769                    // if the swap price is greater than the tokenInPoolPrice, we need to make the swap price cheaper
1770                    if scaledSwapPrice > scaledTokenInPoolPrice {
1771                        // So we need to add a portion of token0 in pool, increase the token0Reserve and decrease the token1Reserve
1772                        // Before: swapPrice = token1Reserve / token0Reserve
1773                        // After: swapPriceAfter = (token1Reserve - y) / (token0Reserve + x) = tokenInPoolPrice
1774                        /* ------------------------------
1775                            y is swapToken1Out, x is swapToken0In, so we just need to calculate the optimized x
1776                            Swap Formula: token0Reserve * token1Reserve = (token0Reserve + x) * (token1Reserve - y)
1777                            => (token1Reserve - y) = (token0Reserve * token1Reserve) / (token0Reserve + x)
1778                            => tokenInPoolPrice = (token0Reserve * token1Reserve) / (token0Reserve + x) / (token0Reserve + x)
1779                                                = (token0Reserve * token1Reserve) / (token0Reserve + x)^2
1780                            => x = sqrt(token0Reserve * token1Reserve / tokenInPoolPrice) - token0Reserve
1781                            - Scaled Calculation -
1782                                sf = 10^18
1783                                ScaledToken0Reserve = token0Reserve * sf
1784                                ScaledToken1Reserve = token1Reserve * sf
1785                                ScaledTokenInPoolPrice = tokenInPoolPrice * sf
1786                            => resXSqrt = sqrt(ScaledToken0Reserve * ScaledToken1Reserve / ScaledTokenInPoolPrice)
1787                                        = sqrt(token0Reserve * sf * token1Reserve * sf / tokenInPoolPrice / sf)
1788                                        = sqrt(token0Reserve * token1Reserve / tokenInPoolPrice) * sqrt(sf)
1789                            => scaledX  = sqrt(token0Reserve * token1Reserve / tokenInPoolPrice) * sf - ScaledToken0Reserve
1790                                        = resXSqrt / sqrt(sf) * sf - ScaledToken0Reserve
1791                        */
1792                        let resXSqrt = SwapConfig.sqrt(scaledToken0Reserve * scaledToken1Reserve / scaledTokenInPoolPrice)
1793                        let scaledX = (resXSqrt / SwapConfig.sqrt(sf) * sf).saturatingSubtract(scaledToken0Reserve)
1794                        let swapToken0In = SwapConfig.ScaledUInt256ToUFix64(scaledX)
1795                        if swapToken0In > token0Max {
1796                            // panic("The swap token0In is greater than the token0Max. "
1797                            //     .concat(swapToken0In.toString())
1798                            //     .concat(" > ")
1799                            //     .concat(token0Max.toString())
1800                            //     .concat(" swapPrice: ")
1801                            //     .concat(scaledSwapPrice.toString())
1802                            //     .concat(" bcPrice: ")
1803                            //     .concat(scaledTokenInPoolPrice.toString())
1804                            // )
1805                            // DON'T DO ANYTHING
1806                            Burner.burn(<- token0Vault)
1807                            Burner.burn(<- token1Vault)
1808                            // DO NOT PANIC
1809                            return false
1810                        }
1811                        // swap the token0 to token1 first, and then add liquidity to token1 vault
1812                        if swapToken0In > 0.0 && swapToken0In <= token0Max {
1813                            let token0ToSwap <- self.vault.withdraw(amount: swapToken0In)
1814                            let token1SwappedVault <- pairPublicRef!.swap(
1815                                vaultIn: <- token0ToSwap,
1816                                exactAmountOut: nil
1817                            )
1818                            // now the swapPrice should be close to the tokenInPoolPrice
1819                            // so we can re-calculate the token0In & token1In to add liquidity
1820                            token0In = (token1SwappedVault.balance + token1In) / tokenInPoolPrice
1821                            // deposit the token1Swapped to the token1Vault
1822                            token1Vault.deposit(from: <- token1SwappedVault)
1823                        }
1824                    } else {
1825                        // if the swap price is less than the tokenInPoolPrice, we need to make the swap price higher
1826                        // So we need to add a portion of token0 in pool, decrease the token0Reserve and increase the token1Reserve
1827                        // Before: swapPrice = token1Reserve / token0Reserve
1828                        // After: swapPriceAfter = (token1Reserve + y) / (token0Reserve - x) = tokenInPoolPrice
1829                        /* ------------------------------
1830                            y is swapToken1In, x is swapToken0Out, so we just need to calculate the optimized y
1831                            Swap Formula: token0Reserve * token1Reserve = (token0Reserve - x) * (token1Reserve + y)
1832                            => (token0Reserve - x) = token0Reserve * token1Reserve / (token1Reserve + y)
1833                            => tokenInPoolPrice = (token1Reserve + y) / (token0Reserve * token1Reserve) * (token1Reserve + y)
1834                                                = (token1Reserve + y)^2 / (token0Reserve * token1Reserve)
1835                            => y = sqrt(token0Reserve * token1Reserve * tokenInPoolPrice) - token1Reserve
1836                            - Scaled Calculation -
1837                                sf = 10^18
1838                                ScaledToken0Reserve = token0Reserve * sf
1839                                ScaledToken1Reserve = token1Reserve * sf
1840                                ScaledTokenInPoolPrice = tokenInPoolPrice * sf
1841                            => resYSqrt = sqrt(ScaledToken0Reserve * ScaledToken1Reserve * ScaledTokenInPoolPrice)
1842                                        = sqrt(token0Reserve * sf * token1Reserve * sf * tokenInPoolPrice * sf)
1843                                        = sqrt(token0Reserve * token1Reserve * tokenInPoolPrice) * sqrt(sf^3)
1844                            => scaledY  = sqrt(token0Reserve * token1Reserve * tokenInPoolPrice) * sf - SacledToken1Reserve
1845                                        = resYSqrt / sqrt(sf^3) * sf - ScaledToken1Reserve
1846                                        = resYSqrt / sf / sqrt(sf) * sf - ScaledToken1Reserve
1847                                        = resYSqrt / sqrt(sf) - ScaledToken1Reserve
1848                        */
1849                        let resYSqrt = SwapConfig.sqrt(scaledToken0Reserve * scaledToken1Reserve * scaledTokenInPoolPrice)
1850                        let scaledY = (resYSqrt / SwapConfig.sqrt(sf)).saturatingSubtract(scaledToken1Reserve)
1851                        let swapToken1In = SwapConfig.ScaledUInt256ToUFix64(scaledY)
1852                        if swapToken1In > token1Max {
1853                            // panic("The swap token1In is greater than the token1Max. "
1854                            //     .concat(swapToken1In.toString())
1855                            //     .concat(" > ")
1856                            //     .concat(token1Max.toString())
1857                            //     .concat(" swapPrice: ")
1858                            //     .concat(scaledSwapPrice.toString())
1859                            //     .concat(" bcPrice: ")
1860                            //     .concat(scaledTokenInPoolPrice.toString())
1861                            // )
1862                            // DON'T DO ANYTHING
1863                            Burner.burn(<- token0Vault)
1864                            Burner.burn(<- token1Vault)
1865                            // DO NOT PANIC
1866                            return false
1867                        }
1868                        // swap the token1 to token0 first, and then add liquidity to token0 vault
1869                        if swapToken1In > 0.0 {
1870                            let token1ToSwap <- self._withdrawFlowToken(swapToken1In)
1871                            let token0SwappedVault <- pairPublicRef!.swap(
1872                                vaultIn: <- token1ToSwap,
1873                                exactAmountOut: nil
1874                            )
1875                            // now the swapPrice should be close to the tokenInPoolPrice
1876                            // add token0 back to the pool
1877                            self.vault.deposit(from: <- token0SwappedVault)
1878                            // re-calculate the token0In & token1In to add liquidity
1879                            token1In = self.getFlowBalanceInPool()
1880                            token0In = token1In / tokenInPoolPrice
1881                        }
1882                    }
1883                }
1884
1885                // deposit the token0In & token1In to the vaults
1886                if token0In > token0Max {
1887                    token0In = token0Max
1888                }
1889                if token1In > token1Max {
1890                    token1In = token1Max
1891                }
1892                if token0In > 0.0 {
1893                    token0Vault.deposit(from: <- self.vault.withdraw(amount: token0In))
1894                }
1895                token1Vault.deposit(from: <- self._withdrawFlowToken(token1In))
1896
1897                // set the local liquidity pool to inactive
1898                self.acitve = false
1899
1900                // Update in the trading center
1901                let tradingCenter = FixesTradablePool.borrowTradingCenter()
1902                tradingCenter.onPoolLPHandovered(self.getPoolAddress())
1903
1904                // set the flag in shared store
1905                let acctsPool = FRC20AccountsPool.borrowAccountsPool()
1906                if let acctKey = self.getAccountsPoolKey() {
1907                    if let store = acctsPool.borrowWritableConfigStore(type: FRC20AccountsPool.ChildAccountType.FungibleToken, acctKey) {
1908                        store.setByEnum(
1909                            FRC20FTShared.ConfigType.TradablePoolHandoveredAt,
1910                            value: getCurrentBlock().timestamp
1911                        )
1912                    }
1913                }
1914
1915                // emit the liquidity pool inactivated event
1916                emit LiquidityPoolInactivated(
1917                    subject: self.getPoolAddress(),
1918                    tokenType: self.getTokenType(),
1919                )
1920            } else {
1921                let token0Max = self.getTokenBalanceInPool()
1922                let allFlowAmount = self.getFlowBalanceInPool()
1923                let estimatedToken0In = SwapConfig.quote(amountA: allFlowAmount, reserveA: token1Reserve, reserveB: token0Reserve)
1924                if token0Max >= estimatedToken0In {
1925                    token0Vault.deposit(from: <- self.vault.withdraw(amount: estimatedToken0In))
1926                    token1Vault.deposit(from: <- self._withdrawFlowToken(allFlowAmount))
1927                } else if token0Max > 0.0 {
1928                    let part1EstimatedToken1In = SwapConfig.quote(amountA: token0Max, reserveA: token0Reserve, reserveB: token1Reserve)
1929                    let part2EstimatedToken1In = allFlowAmount.saturatingSubtract(part1EstimatedToken1In)
1930                    if part2EstimatedToken1In > 0.0 {
1931                        token0Vault.deposit(from: <- self.vault.withdraw(amount: token0Max))
1932                        token1Vault.deposit(from: <- self._withdrawFlowToken(part1EstimatedToken1In))
1933                    } else {
1934                        Burner.burn(<- token0Vault)
1935                        Burner.burn(<- token1Vault)
1936                        // DO NOT PANIC
1937                        return false
1938                    }
1939                } else {
1940                    // All Token0 is added to the pair, so we need to calculate the optimized zapped amount through dex
1941                    let zappedAmt = self._calcZappedAmmount(
1942                        tokenInput: allFlowAmount,
1943                        tokenReserve: token1Reserve,
1944                        swapFeeRateBps: pairInfo[6] as! UInt64
1945                    )
1946                    // withdraw all the flow token and add liquidity to the pair
1947                    let payment <- self._withdrawFlowToken(allFlowAmount)
1948
1949                    let swapVaultIn <- payment.withdraw(amount: zappedAmt)
1950                    // withdraw all the token0 and add liquidity to the pair
1951                    token1Vault.deposit(from: <- payment)
1952                    // swap the token1 to token0 first, and then add liquidity vault
1953                    token0Vault.deposit(from: <- pairPublicRef!.swap(
1954                        vaultIn: <- swapVaultIn,
1955                        exactAmountOut: nil
1956                    ))
1957                }
1958            }
1959
1960            // cache value
1961            let token0Amount = token0Vault.balance
1962            let token1Amount = token1Vault.balance
1963
1964            // add liquidity to the pair
1965            let lpTokenVault <- pairPublicRef!.addLiquidity(
1966                tokenAVault: <- token0Vault,
1967                tokenBVault: <- token1Vault
1968            )
1969            // record the burned LP token
1970            self.lpBurned = self.lpBurned + lpTokenVault.balance
1971
1972            // Send the LP token vault to the BlackHole for soft burning
1973            FixesTradablePool.softBurnVault(<- lpTokenVault)
1974
1975            // emit the liquidity pool transferred event
1976            emit LiquidityTransferred(
1977                subject: self.getPoolAddress(),
1978                pairAddr: self.getSwapPairAddress()!,
1979                tokenType: self.getTokenType(),
1980                tokenAmount: token0Amount,
1981                flowAmount: token1Amount
1982            )
1983
1984            return true
1985        }
1986
1987        /// Calculate the optimized zapped amount through dex
1988        ///
1989        access(self)
1990        fun _calcZappedAmmount(tokenInput: UFix64, tokenReserve: UFix64, swapFeeRateBps: UInt64): UFix64 {
1991            // Cal optimized zapped amount through dex
1992            let r0Scaled = SwapConfig.UFix64ToScaledUInt256(tokenReserve)
1993            let fee = 1.0 - UFix64(swapFeeRateBps)/10000.0
1994            let kplus1SquareScaled = SwapConfig.UFix64ToScaledUInt256((1.0+fee)*(1.0+fee))
1995            let kScaled = SwapConfig.UFix64ToScaledUInt256(fee)
1996            let kplus1Scaled = SwapConfig.UFix64ToScaledUInt256(fee+1.0)
1997            let tokenInScaled = SwapConfig.UFix64ToScaledUInt256(tokenInput)
1998            let qScaled = SwapConfig.sqrt(
1999                r0Scaled * r0Scaled / SwapConfig.scaleFactor * kplus1SquareScaled / SwapConfig.scaleFactor
2000                + 4 * kScaled * r0Scaled / SwapConfig.scaleFactor * tokenInScaled / SwapConfig.scaleFactor)
2001            return SwapConfig.ScaledUInt256ToUFix64(
2002                (qScaled - r0Scaled*kplus1Scaled/SwapConfig.scaleFactor)*SwapConfig.scaleFactor/(kScaled*2)
2003            )
2004        }
2005    }
2006
2007    /// ------ Public Methods ------
2008
2009    /// Create a new tradable liquidity pool(bonding curve) resource
2010    ///
2011    access(account)
2012    fun createTradableLiquidityPool(
2013        ins: auth(Fixes.Extractable) &Fixes.Inscription,
2014        _ minter: @{FixesFungibleTokenInterface.IMinter},
2015    ): @TradableLiquidityPool {
2016        post {
2017            ins.isValueEmpty(): "The inscription value must be empty after the execution"
2018        }
2019
2020        // execute the inscription
2021        let meta = self.verifyAndExecuteInscription(ins, symbol: minter.getSymbol(), usage: "*")
2022
2023        // get the fee percentage from the inscription metadata
2024        let subjectFeePerc = UFix64.fromString(meta["feePerc"] ?? "0.0") ?? 0.0
2025        // get free amount from the inscription metadata
2026        let freeAmount = UFix64.fromString(meta["freeAmount"] ?? "0.0") ?? 0.0
2027        // using total allowed mintable amount as the max supply
2028        let maxSupply = minter.getTotalAllowedMintableAmount()
2029        // create the bonding curve
2030        let curve = FixesBondingCurve.Quadratic(freeAmount: freeAmount, maxSupply: maxSupply)
2031
2032        let tokenType = minter.getTokenType()
2033        let tokenSymbol = minter.getSymbol()
2034
2035        let pool <- create TradableLiquidityPool(<- minter, curve, subjectFeePerc)
2036
2037        // emit the liquidity pool created event
2038        emit LiquidityPoolCreated(
2039            tokenType: tokenType,
2040            curveType: curve.getType(),
2041            tokenSymbol: tokenSymbol,
2042            subjectFeePerc: subjectFeePerc,
2043            freeAmount: freeAmount,
2044            createdBy: ins.owner?.address ?? panic("The inscription owner is missing")
2045        )
2046
2047        return <- pool
2048    }
2049
2050    /// Buy the fungible token from the tradable pool
2051    ///
2052    access(all)
2053    fun buy(
2054        _ coinAddr: Address,
2055        flowProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider},
2056        flowAmount: UFix64,
2057        ftReceiver: &{FungibleToken.Receiver},
2058        inscriptionStore: auth(Fixes.Manage) &Fixes.InscriptionsStore,
2059    ) {
2060        pre {
2061            ftReceiver.owner?.address == inscriptionStore.owner?.address: "FT Receiver must be the owner of the inscription store"
2062        }
2063
2064        // resources
2065        let tradablePoolRef = FixesTradablePool.borrowTradablePool(coinAddr)
2066            ?? panic("Tradable pool not found")
2067
2068        // coin's token type
2069        let tokenType = tradablePoolRef.getTokenType()
2070        assert(
2071            ftReceiver.isSupportedVaultType(type: tokenType),
2072            message: "Unsupported token type"
2073        )
2074        let tickerName = "$".concat(tradablePoolRef.getSymbol())
2075
2076        // ---- create the basic inscription ----
2077        let dataStr = FixesInscriptionFactory.buildPureExecuting(
2078            tick: tickerName,
2079            usage: "init",
2080            {}
2081        )
2082        // estimate the required storage
2083        let estimatedReqValue = FixesInscriptionFactory.estimateFrc20InsribeCost(dataStr)
2084        let costReserve <- flowProvider.withdraw(amount: estimatedReqValue)
2085        // create the inscription
2086        let insId = FixesInscriptionFactory.createAndStoreFrc20Inscription(
2087            dataStr,
2088            <- (costReserve as! @FlowToken.Vault),
2089            inscriptionStore
2090        )
2091        // apply the inscription
2092        let ins = inscriptionStore.borrowInscriptionWritableRef(insId)!
2093        // ---- end ----
2094
2095        // ---- deposit the but token cost to inscription ----
2096        let flowCanUse = flowAmount - estimatedReqValue
2097        let costVault <- flowProvider.withdraw(amount: flowCanUse)
2098        ins.deposit(<- (costVault as! @FlowToken.Vault))
2099        // ---- end ----
2100
2101        // buy token
2102        tradablePoolRef.buyTokens(ins, nil, recipient: ftReceiver)
2103    }
2104
2105    /// Soft burn the vault
2106    ///
2107    access(all)
2108    fun softBurnVault(_ vault: @{FungibleToken.Vault}) {
2109        let network = AddressUtils.currentNetwork()
2110
2111        // Send the vault to the BlackHole
2112        if network == "MAINNET" {
2113            // Use IncrementFi's BlackHole: https://app.increment.fi/profile/0x9c1142b81f1ae962
2114            let blackHoleAddr = Address.fromString("0x".concat("9c1142b81f1ae962"))!
2115            if let blackHoleRef = BlackHole.borrowBlackHoleReceiver(blackHoleAddr) {
2116                blackHoleRef.deposit(from: <- vault)
2117            } else {
2118                BlackHole.vanish(<- vault)
2119            }
2120        } else {
2121            BlackHole.vanish(<- vault)
2122        }
2123    }
2124
2125    /// Verify the inscription for executing the Fungible Token
2126    ///
2127    access(all)
2128    fun verifyAndExecuteInscription(
2129        _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
2130        symbol: String,
2131        usage: String
2132    ): {String: String} {
2133        // borrow the accounts pool
2134        let acctsPool = FRC20AccountsPool.borrowAccountsPool()
2135
2136        // inscription data
2137        let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
2138        // check the operation
2139        assert(
2140            meta["op"] == "exec",
2141            message: "The inscription operation must be 'exec'"
2142        )
2143        // check the symbol
2144        let tick = meta["tick"] ?? panic("The token symbol is not found")
2145        assert(
2146            acctsPool.getFTContractAddress(tick) != nil,
2147            message: "The FungibleToken contract is not found"
2148        )
2149        assert(
2150            tick == "$".concat(symbol),
2151            message: "The minter's symbol is not matched. Required: $".concat(symbol)
2152        )
2153        // check the usage
2154        let usageInMeta = meta["usage"] ?? panic("The token operation is not found")
2155        assert(
2156            usageInMeta == usage || usage == "*",
2157            message: "The inscription is not for initialize a Fungible Token account"
2158        )
2159        // execute the inscription
2160        acctsPool.executeInscription(type: FRC20AccountsPool.ChildAccountType.FungibleToken, ins)
2161        // return the metadata
2162        return meta
2163    }
2164
2165    /// Check if the caller is an advanced token player(by staked $flows)
2166    ///
2167    access(all)
2168    view fun isAdvancedTokenPlayer(_ addr: Address): Bool {
2169        let stakeTick = FRC20FTShared.getPlatformStakingTickerName()
2170        // the threshold is 100K staked $flows
2171        let threshold = 100_000.0
2172        return FRC20StakingManager.isEligibleByStakePower(stakeTick: stakeTick, addr: addr, threshold: threshold)
2173    }
2174
2175    /// Get the flow price from IncrementFi Oracle
2176    ///
2177    access(all)
2178    fun getFlowPrice(): UFix64 {
2179        let network = AddressUtils.currentNetwork()
2180        // reference: https://docs.increment.fi/protocols/decentralized-price-feed-oracle/deployment-addresses
2181        var oracleAddress: Address? = nil
2182        if network == "MAINNET" {
2183            // TO FIX stupid fcl bug
2184            oracleAddress = Address.fromString("0x".concat("e385412159992e11"))
2185        }
2186        if oracleAddress == nil {
2187            // Hack for testnet and emulator
2188            return 1.0
2189        } else {
2190            // Uncomment the following code when the oracle is available
2191            return PublicPriceOracle.getLatestPrice(oracleAddr: oracleAddress!)
2192        }
2193    }
2194
2195    /// Get the target market cap for creating LP
2196    ///
2197    access(all)
2198    view fun getTargetMarketCap(): UFix64 {
2199        // use the shared store to get the sale fee
2200        let sharedStore = FRC20FTShared.borrowGlobalStoreRef()
2201        let valueInStore: UFix64? = sharedStore.getByEnum(FRC20FTShared.ConfigType.TradablePoolCreateLPTargetMarketCap) as! UFix64?
2202        // Default is 32800 USD
2203        let defaultTargetMarketCap = 32800.0
2204        return valueInStore ?? defaultTargetMarketCap
2205    }
2206
2207    /// Get the trading pool protocol fee
2208    ///
2209    access(all)
2210    view fun getProtocolTradingFee(): UFix64 {
2211        post {
2212            result >= 0.0: "The platform sales fee must be greater than or equal to 0"
2213            result <= 0.02: "The platform sales fee must be less than or equal to 0.02"
2214        }
2215        // use the shared store to get the sale fee
2216        let sharedStore = FRC20FTShared.borrowGlobalStoreRef()
2217        // Default sales fee, 0.5% of the total price
2218        let defaultSalesFee = 0.005
2219        let salesFee = (sharedStore.getByEnum(FRC20FTShared.ConfigType.TradablePoolTradingFee) as! UFix64?) ?? defaultSalesFee
2220        return salesFee
2221    }
2222
2223    /// Get the public capability of Tradable Pool
2224    ///
2225    access(all)
2226    view fun borrowTradablePool(_ addr: Address): &TradableLiquidityPool? {
2227        return getAccount(addr)
2228            .capabilities.get<&TradableLiquidityPool>(self.getLiquidityPoolPublicPath())
2229            .borrow()
2230    }
2231
2232    /// Get the prefix for the storage paths
2233    ///
2234    access(all)
2235    view fun getPathPrefix(): String {
2236        return "FixesTradablePool_".concat(self.account.address.toString()).concat("_")
2237    }
2238
2239    /// Get the storage path for the Liquidity Pool
2240    ///
2241    access(all)
2242    view fun getLiquidityPoolStoragePath(): StoragePath {
2243        let prefix = self.getPathPrefix()
2244        return StoragePath(identifier: prefix.concat("LiquidityPool"))!
2245    }
2246
2247    /// Get the public path for the Liquidity Pool
2248    ///
2249    access(all)
2250    view fun getLiquidityPoolPublicPath(): PublicPath {
2251        let prefix = self.getPathPrefix()
2252        return PublicPath(identifier: prefix.concat("LiquidityPool"))!
2253    }
2254
2255    /// Get the storage path for the Trading Center
2256    ///
2257    access(all)
2258    view fun getTradingCenterStoragePath(): StoragePath {
2259        let prefix = self.getPathPrefix()
2260        return StoragePath(identifier: prefix.concat("TradingCenter"))!
2261    }
2262
2263    /// Get the public path for the Trading Center
2264    ///
2265    access(all)
2266    view fun getTradingCenterPublicPath(): PublicPath {
2267        let prefix = self.getPathPrefix()
2268        return PublicPath(identifier: prefix.concat("TradingCenter"))!
2269    }
2270
2271    /// Create the trading information center
2272    ///
2273    access(all)
2274    fun createCenter(): @TradingCenter {
2275        return <- create TradingCenter()
2276    }
2277
2278    /// Borrow the trading information center
2279    ///
2280    access(all)
2281    view fun borrowTradingCenter(): &TradingCenter {
2282        return self.account
2283            .capabilities.get<&TradingCenter>(self.getTradingCenterPublicPath())
2284            .borrow()
2285            ?? panic("The Trading Center is missing")
2286    }
2287
2288    init() {
2289        let storagePath = self.getTradingCenterStoragePath()
2290        self.account.storage.save(<- self.createCenter(), to: storagePath)
2291        let cap = self.account
2292            .capabilities.storage.issue<&TradingCenter>(storagePath)
2293        self.account.capabilities.publish(cap, at: self.getTradingCenterPublicPath())
2294    }
2295}
2296