Smart Contract
FlowSwapPair
A.c6c77b9f5c7a378f.FlowSwapPair
1import Burner from 0xf233dcee88fe0abe
2import FungibleToken from 0xf233dcee88fe0abe
3import FlowToken from 0x1654653399040a61
4import TeleportedTetherToken from 0xcfdd90d4a00f7b5b
5import MetadataViews from 0x1d7e57aa55817448
6import FungibleTokenMetadataViews from 0xf233dcee88fe0abe
7
8// Exchange pair between FlowToken and TeleportedTetherToken
9// Token1: FlowToken
10// Token2: TeleportedTetherToken
11access(all)
12contract FlowSwapPair: FungibleToken {
13 // Frozen flag controlled by Admin
14 access(all) var isFrozen: Bool
15
16 // Total supply of FlowSwapExchange liquidity token in existence
17 access(all) var totalSupply: UFix64
18
19 // Fee charged when performing token swap
20 access(all) var feePercentage: UFix64
21
22 // Controls FlowToken vault
23 access(contract) let token1Vault: @FlowToken.Vault
24
25 // Controls TeleportedTetherToken vault
26 access(contract) let token2Vault: @TeleportedTetherToken.Vault
27
28 // Defines token vault storage path
29 access(all) let TokenStoragePath: StoragePath
30
31 // Defines token vault public balance path
32 access(all) let TokenPublicBalancePath: PublicPath
33
34 // Defines token vault public receiver path
35 access(all) let TokenPublicReceiverPath: PublicPath
36
37 // Event that is emitted when the contract is created
38 access(all) event TokensInitialized(initialSupply: UFix64)
39
40 // Event that is emitted when tokens are withdrawn from a Vault
41 access(all) event TokensWithdrawn(amount: UFix64, from: Address?)
42
43 // Event that is emitted when tokens are deposited to a Vault
44 access(all) event TokensDeposited(amount: UFix64, to: Address?)
45
46 // Event that is emitted when new tokens are minted
47 access(all) event TokensMinted(amount: UFix64)
48
49 // Event that is emitted when tokens are destroyed
50 access(all) event TokensBurned(amount: UFix64)
51
52 // Event that is emitted when trading fee is updated
53 access(all) event FeeUpdated(feePercentage: UFix64)
54
55 // Event that is emitted when a swap happens
56 // Side 1: from token1 to token2
57 // Side 2: from token2 to token1
58 access(all) event Trade(token1Amount: UFix64, token2Amount: UFix64, side: UInt8)
59
60 /// Gets a list of the metadata views that this contract supports
61 access(all) view fun getContractViews(resourceType: Type?): [Type] {
62 return [Type<FungibleTokenMetadataViews.FTView>(),
63 Type<FungibleTokenMetadataViews.FTDisplay>(),
64 Type<FungibleTokenMetadataViews.FTVaultData>(),
65 Type<FungibleTokenMetadataViews.TotalSupply>()]
66 }
67
68 /// Get a Metadata View
69 ///
70 /// @param view: The Type of the desired view.
71 /// @return A structure representing the requested view.
72 ///
73 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
74 switch viewType {
75 case Type<FungibleTokenMetadataViews.FTView>():
76 return FungibleTokenMetadataViews.FTView(
77 ftDisplay: self.resolveContractView(resourceType: nil, viewType: Type<FungibleTokenMetadataViews.FTDisplay>()) as! FungibleTokenMetadataViews.FTDisplay?,
78 ftVaultData: self.resolveContractView(resourceType: nil, viewType: Type<FungibleTokenMetadataViews.FTVaultData>()) as! FungibleTokenMetadataViews.FTVaultData?
79 )
80 case Type<FungibleTokenMetadataViews.FTDisplay>():
81 let media = MetadataViews.Media(
82 file: MetadataViews.HTTPFile(
83 url: "https://swap.blocto.app/favicon-144x144.png"
84 ),
85 mediaType: "image/png"
86 )
87 let medias = MetadataViews.Medias([media])
88 return FungibleTokenMetadataViews.FTDisplay(
89 name: "FLOW/tUSDT Swap LP Token",
90 symbol: "FLOWUSDT",
91 description: "BloctoSwap liquidity provider token for the FLOW/tUSDT swap pair.",
92 externalURL: MetadataViews.ExternalURL("https://swap.blocto.app"),
93 logos: medias,
94 socials: {
95 "twitter": MetadataViews.ExternalURL("https://x.com/bloctoapp")
96 }
97 )
98 case Type<FungibleTokenMetadataViews.FTVaultData>():
99 let vaultRef = FlowSwapPair.account.storage.borrow<auth(FungibleToken.Withdraw) &FlowSwapPair.Vault>(from: /storage/flowUsdtFspLpVault)
100 ?? panic("Could not borrow reference to the contract's Vault!")
101 return FungibleTokenMetadataViews.FTVaultData(
102 storagePath: /storage/flowUsdtFspLpVault,
103 receiverPath: /public/flowUsdtFspLpReceiver,
104 metadataPath: /public/flowUsdtFspLpBalance,
105 receiverLinkedType: Type<&{FungibleToken.Receiver, FungibleToken.Vault}>(),
106 metadataLinkedType: Type<&{FungibleToken.Balance, FungibleToken.Vault}>(),
107 createEmptyVaultFunction: (fun (): @{FungibleToken.Vault} {
108 return <-vaultRef.createEmptyVault()
109 })
110 )
111 case Type<FungibleTokenMetadataViews.TotalSupply>():
112 return FungibleTokenMetadataViews.TotalSupply(totalSupply: FlowSwapPair.totalSupply)
113 }
114 return nil
115 }
116
117 // Vault
118 //
119 // Each user stores an instance of only the Vault in their storage
120 // The functions in the Vault and governed by the pre and post conditions
121 // in FlowSwapExchange when they are called.
122 // The checks happen at runtime whenever a function is called.
123 //
124 // Resources can only be created in the context of the contract that they
125 // are defined in, so there is no way for a malicious user to create Vaults
126 // out of thin air. A special Minter resource needs to be defined to mint
127 // new tokens.
128 //
129 access(all) resource Vault: FungibleToken.Vault {
130
131 // holds the balance of a users tokens
132 access(all) var balance: UFix64
133
134 // initialize the balance at resource creation time
135 init(balance: UFix64) {
136 self.balance = balance
137 }
138
139 // Called when a fungible token is burned via the `Burner.burn()` method
140 access(contract) fun burnCallback() {
141 if self.balance > 0.0 {
142 FlowSwapPair.totalSupply = FlowSwapPair.totalSupply - self.balance
143 }
144 self.balance = 0.0
145 }
146
147 // getSupportedVaultTypes optionally returns a list of vault types that this receiver accepts
148 access(all) view fun getSupportedVaultTypes(): {Type: Bool} {
149 return {self.getType(): true}
150 }
151
152 access(all) view fun isSupportedVaultType(type: Type): Bool {
153 if (type == self.getType()) { return true } else { return false }
154 }
155
156 access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool{
157 return self.balance >= amount
158 }
159
160 // withdraw
161 //
162 // Function that takes an integer amount as an argument
163 // and withdraws that amount from the Vault.
164 // It creates a new temporary Vault that is used to hold
165 // the money that is being transferred. It returns the newly
166 // created Vault to the context that called so it can be deposited
167 // elsewhere.
168 //
169 access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} {
170 self.balance = self.balance - amount
171 emit TokensWithdrawn(amount: amount, from: self.owner?.address)
172 return <-create Vault(balance: amount)
173 }
174
175 // deposit
176 //
177 // Function that takes a Vault object as an argument and adds
178 // its balance to the balance of the owners Vault.
179 // It is allowed to destroy the sent Vault because the Vault
180 // was a temporary holder of the tokens. The Vault's balance has
181 // been consumed and therefore can be destroyed.
182 access(all) fun deposit(from: @{FungibleToken.Vault}) {
183 let vault <- from as! @FlowSwapPair.Vault
184 self.balance = self.balance + vault.balance
185 emit TokensDeposited(amount: vault.balance, to: self.owner?.address)
186 vault.balance = 0.0
187 destroy vault
188 }
189
190 // Get all the Metadata Views implemented
191 //
192 // @return An array of Types defining the implemented views. This value will be used by
193 // developers to know which parameter to pass to the resolveView() method.
194 //
195 access(all) view fun getViews(): [Type]{
196 return FlowSwapPair.getContractViews(resourceType: nil)
197 }
198
199 // Get a Metadata View
200 //
201 // @param view: The Type of the desired view.
202 // @return A structure representing the requested view.
203 //
204 access(all) fun resolveView(_ view: Type): AnyStruct? {
205 return FlowSwapPair.resolveContractView(resourceType: nil, viewType: view)
206 }
207
208 access(all) fun createEmptyVault(): @{FungibleToken.Vault}{
209 return <-create Vault(balance: 0.0)
210 }
211 }
212
213 // createEmptyVault
214 //
215 // Function that creates a new Vault with a balance of zero
216 // and returns it to the calling context. A user must call this function
217 // and store the returned Vault in their storage in order to allow their
218 // account to be able to receive deposits of this token type.
219 //
220 access(all) fun createEmptyVault(vaultType: Type): @FlowSwapPair.Vault {
221 return <-create Vault(balance: 0.0)
222 }
223
224 access(all) resource TokenBundle {
225 access(all) var token1: @FlowToken.Vault
226 access(all) var token2: @TeleportedTetherToken.Vault
227
228 // initialize the vault bundle
229 init(fromToken1: @FlowToken.Vault, fromToken2: @TeleportedTetherToken.Vault) {
230 self.token1 <- fromToken1
231 self.token2 <- fromToken2
232 }
233
234 access(all) fun depositToken1(from: @FlowToken.Vault) {
235 self.token1.deposit(from: <- from)
236 }
237
238 access(all) fun depositToken2(from: @TeleportedTetherToken.Vault) {
239 self.token2.deposit(from: <- from)
240 }
241
242 access(all) fun withdrawToken1(): @FlowToken.Vault {
243 var vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
244 vault <-> self.token1
245 return <- vault
246 }
247
248 access(all) fun withdrawToken2(): @TeleportedTetherToken.Vault {
249 var vault <- TeleportedTetherToken.createEmptyVault(vaultType: Type<@TeleportedTetherToken.Vault>())
250 vault <-> self.token2
251 return <- vault
252 }
253 }
254
255 // createEmptyBundle
256 //
257 access(all) fun createEmptyTokenBundle(): @FlowSwapPair.TokenBundle {
258 return <- create TokenBundle(
259 fromToken1: <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()),
260 fromToken2: <- TeleportedTetherToken.createEmptyVault(vaultType: Type<@TeleportedTetherToken.Vault>())
261 )
262 }
263
264 // createTokenBundle
265 //
266 access(all) fun createTokenBundle(fromToken1: @FlowToken.Vault, fromToken2: @TeleportedTetherToken.Vault): @FlowSwapPair.TokenBundle {
267 return <- create TokenBundle(fromToken1: <- fromToken1, fromToken2: <- fromToken2)
268 }
269
270 // mintTokens
271 //
272 // Function that mints new tokens, adds them to the total supply,
273 // and returns them to the calling context.
274 //
275 access(contract) fun mintTokens(amount: UFix64): @FlowSwapPair.Vault {
276 pre {
277 amount > 0.0: "Amount minted must be greater than zero"
278 }
279 FlowSwapPair.totalSupply = FlowSwapPair.totalSupply + amount
280 emit TokensMinted(amount: amount)
281 return <-create Vault(balance: amount)
282 }
283
284 // burnTokens
285 //
286 // Function that destroys a Vault instance, effectively burning the tokens.
287 //
288 // Note: the burned tokens are automatically subtracted from the
289 // total supply in the Vault destructor.
290 //
291 access(contract) fun burnTokens(from: @FlowSwapPair.Vault) {
292 let vault <- from
293 let amount = vault.balance
294 Burner.burn(<- vault)
295 emit TokensBurned(amount: amount)
296 }
297
298 access(all) resource SwapProxy {}
299
300 access(all) resource Admin {
301 access(all) fun freeze() {
302 FlowSwapPair.isFrozen = true
303 }
304
305 access(all) fun unfreeze() {
306 FlowSwapPair.isFrozen = false
307 }
308
309 access(all) fun addInitialLiquidity(from: @FlowSwapPair.TokenBundle): @FlowSwapPair.Vault {
310 pre {
311 FlowSwapPair.totalSupply == 0.0: "Pair already initialized"
312 }
313
314 let token1Vault <- from.withdrawToken1()
315 let token2Vault <- from.withdrawToken2()
316
317 assert(token1Vault.balance > 0.0, message: "Empty token1 vault")
318 assert(token2Vault.balance > 0.0, message: "Empty token2 vault")
319
320 FlowSwapPair.token1Vault.deposit(from: <- token1Vault)
321 FlowSwapPair.token2Vault.deposit(from: <- token2Vault)
322
323 destroy from
324
325 // Create initial tokens
326 return <- FlowSwapPair.mintTokens(amount: 1.0)
327 }
328
329 access(all) fun updateFeePercentage(feePercentage: UFix64) {
330 FlowSwapPair.feePercentage = feePercentage
331
332 emit FeeUpdated(feePercentage: feePercentage)
333 }
334 }
335
336 access(all) struct PoolAmounts {
337 access(all) let token1Amount: UFix64
338 access(all) let token2Amount: UFix64
339
340 init(token1Amount: UFix64, token2Amount: UFix64) {
341 self.token1Amount = token1Amount
342 self.token2Amount = token2Amount
343 }
344 }
345
346 access(all) fun getFeePercentage(): UFix64 {
347 return self.feePercentage
348 }
349
350 // Check current pool amounts
351 access(all) fun getPoolAmounts(): PoolAmounts {
352 return PoolAmounts(token1Amount: FlowSwapPair.token1Vault.balance, token2Amount: FlowSwapPair.token2Vault.balance)
353 }
354
355 // Get quote for Token1 (given) -> Token2
356 access(all) fun quoteSwapExactToken1ForToken2(amount: UFix64): UFix64 {
357 let poolAmounts = self.getPoolAmounts()
358
359 // token1Amount * token2Amount = token1Amount' * token2Amount' = (token1Amount + amount) * (token2Amount - quote)
360 let quote = poolAmounts.token2Amount * amount / (poolAmounts.token1Amount + amount);
361
362 return quote
363 }
364
365 // Get quote for Token1 -> Token2 (given)
366 access(all) fun quoteSwapToken1ForExactToken2(amount: UFix64): UFix64 {
367 let poolAmounts = self.getPoolAmounts()
368
369 assert(poolAmounts.token2Amount > amount, message: "Not enough Token2 in the pool")
370
371 // token1Amount * token2Amount = token1Amount' * token2Amount' = (token1Amount + quote) * (token2Amount - amount)
372 let quote = poolAmounts.token1Amount * amount / (poolAmounts.token2Amount - amount);
373
374 return quote
375 }
376
377 // Get quote for Token2 (given) -> Token1
378 access(all) fun quoteSwapExactToken2ForToken1(amount: UFix64): UFix64 {
379 let poolAmounts = self.getPoolAmounts()
380
381 // token1Amount * token2Amount = token1Amount' * token2Amount' = (token2Amount + amount) * (token1Amount - quote)
382 let quote = poolAmounts.token1Amount * amount / (poolAmounts.token2Amount + amount);
383
384 return quote
385 }
386
387 // Get quote for Token2 -> Token1 (given)
388 access(all) fun quoteSwapToken2ForExactToken1(amount: UFix64): UFix64 {
389 let poolAmounts = self.getPoolAmounts()
390
391 assert(poolAmounts.token1Amount > amount, message: "Not enough Token1 in the pool")
392
393 // token1Amount * token2Amount = token1Amount' * token2Amount' = (token2Amount + quote) * (token1Amount - amount)
394 let quote = poolAmounts.token2Amount * amount / (poolAmounts.token1Amount - amount);
395
396 return quote
397 }
398
399 // Swaps Token1 (FLOW) -> Token2 (tUSDT)
400 access(all) fun swapToken1ForToken2(from: @FlowToken.Vault): @TeleportedTetherToken.Vault {
401 pre {
402 !FlowSwapPair.isFrozen: "FlowSwapPair is frozen"
403 from.balance > 0.0: "Empty token vault"
404 }
405
406 // Calculate amount from pricing curve
407 // A fee portion is taken from the final amount
408 let token1Amount = from.balance * (1.0 - self.feePercentage)
409 let token2Amount = self.quoteSwapExactToken1ForToken2(amount: token1Amount)
410
411 assert(token2Amount > 0.0, message: "Exchanged amount too small")
412
413 self.token1Vault.deposit(from: <- from)
414 emit Trade(token1Amount: token1Amount, token2Amount: token2Amount, side: 1)
415
416 return <- (self.token2Vault.withdraw(amount: token2Amount) as! @TeleportedTetherToken.Vault)
417 }
418
419 // Swap Token2 (tUSDT) -> Token1 (FLOW)
420 access(all) fun swapToken2ForToken1(from: @TeleportedTetherToken.Vault): @FlowToken.Vault {
421 pre {
422 !FlowSwapPair.isFrozen: "FlowSwapPair is frozen"
423 from.balance > 0.0: "Empty token vault"
424 }
425
426 // Calculate amount from pricing curve
427 // A fee portion is taken from the final amount
428 let token2Amount = from.balance * (1.0 - self.feePercentage)
429 let token1Amount = self.quoteSwapExactToken2ForToken1(amount: token2Amount)
430
431 assert(token1Amount > 0.0, message: "Exchanged amount too small")
432
433 self.token2Vault.deposit(from: <- from)
434 emit Trade(token1Amount: token1Amount, token2Amount: token2Amount, side: 2)
435
436 return <- (self.token1Vault.withdraw(amount: token1Amount) as! @FlowToken.Vault)
437 }
438
439 // Used to add liquidity without minting new liquidity token
440 access(all) fun donateLiquidity(from: @FlowSwapPair.TokenBundle) {
441 let token1Vault <- from.withdrawToken1()
442 let token2Vault <- from.withdrawToken2()
443
444 FlowSwapPair.token1Vault.deposit(from: <- token1Vault)
445 FlowSwapPair.token2Vault.deposit(from: <- token2Vault)
446
447 destroy from
448 }
449
450 access(all) fun addLiquidity(from: @FlowSwapPair.TokenBundle): @FlowSwapPair.Vault {
451 pre {
452 self.totalSupply > 0.0: "Pair must be initialized by admin first"
453 }
454
455 let token1Vault <- from.withdrawToken1()
456 let token2Vault <- from.withdrawToken2()
457
458 assert(token1Vault.balance > 0.0, message: "Empty token1 vault")
459 assert(token2Vault.balance > 0.0, message: "Empty token2 vault")
460
461 // shift decimal 4 places to avoid truncation error
462 let token1Percentage: UFix64 = (token1Vault.balance * 10000.0) / FlowSwapPair.token1Vault.balance
463 let token2Percentage: UFix64 = (token2Vault.balance * 10000.0) / FlowSwapPair.token2Vault.balance
464
465 // final liquidity token minted is the smaller between token1Liquidity and token2Liquidity
466 // to maximize profit, user should add liquidity propotional to current liquidity
467 let liquidityPercentage = token1Percentage < token2Percentage ? token1Percentage : token2Percentage;
468
469 assert(liquidityPercentage > 0.0, message: "Liquidity too small")
470
471 FlowSwapPair.token1Vault.deposit(from: <- token1Vault)
472 FlowSwapPair.token2Vault.deposit(from: <- token2Vault)
473
474 let liquidityTokenVault <- FlowSwapPair.mintTokens(amount: (FlowSwapPair.totalSupply * liquidityPercentage) / 10000.0)
475
476 destroy from
477 return <- liquidityTokenVault
478 }
479
480 access(all) fun removeLiquidity(from: @FlowSwapPair.Vault): @FlowSwapPair.TokenBundle {
481 pre {
482 from.balance > 0.0: "Empty liquidity token vault"
483 from.balance < FlowSwapPair.totalSupply: "Cannot remove all liquidity"
484 }
485
486 // shift decimal 4 places to avoid truncation error
487 let liquidityPercentage = (from.balance * 10000.0) / FlowSwapPair.totalSupply
488
489 assert(liquidityPercentage > 0.0, message: "Liquidity too small")
490
491 // Burn liquidity tokens and withdraw
492 FlowSwapPair.burnTokens(from: <- from)
493
494 let token1Vault <- FlowSwapPair.token1Vault.withdraw(amount: (FlowSwapPair.token1Vault.balance * liquidityPercentage) / 10000.0) as! @FlowToken.Vault
495 let token2Vault <- FlowSwapPair.token2Vault.withdraw(amount: (FlowSwapPair.token2Vault.balance * liquidityPercentage) / 10000.0) as! @TeleportedTetherToken.Vault
496
497 let tokenBundle <- FlowSwapPair.createTokenBundle(fromToken1: <- token1Vault, fromToken2: <- token2Vault)
498 return <- tokenBundle
499 }
500
501 init() {
502 self.isFrozen = true // frozen until admin unfreezes
503 self.totalSupply = 0.0
504 self.feePercentage = 0.003 // 0.3%
505
506 self.TokenStoragePath = /storage/flowUsdtFspLpVault
507 self.TokenPublicBalancePath = /public/flowUsdtFspLpBalance
508 self.TokenPublicReceiverPath = /public/flowUsdtFspLpReceiver
509
510 // Create the Vault with the total supply of tokens and save it in storage
511 let vault <- create Vault(balance: self.totalSupply)
512
513 self.account.storage.save(<-vault, to: /storage/flowUsdtFspLpVault)
514
515 // Setup internal FlowToken vault
516 self.token1Vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
517
518 // Setup internal TeleportedTetherToken vault
519 self.token2Vault <- TeleportedTetherToken.createEmptyVault(vaultType: Type<@TeleportedTetherToken.Vault>())
520
521 let admin <- create Admin()
522 self.account.storage.save(<-admin, to: /storage/flowSwapPairAdmin)
523
524 // Emit an event that shows that the contract was initialized
525 emit TokensInitialized(initialSupply: self.totalSupply)
526 }
527}
528