Smart Contract

BltUsdtSwapPair

A.fcb06a5ae5b21a2d.BltUsdtSwapPair

Deployed

5d ago
Feb 21, 2026, 01:58:53 PM UTC

Dependents

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