Smart Contract

FlowSwapPair

A.c6c77b9f5c7a378f.FlowSwapPair

Deployed

1w ago
Feb 15, 2026, 04:35:32 PM UTC

Dependents

136 imports
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