Smart Contract
FixesTradablePool
A.d2abb5dbf5e08666.FixesTradablePool
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