Smart Contract
StarlyUsdtSwapPair
A.6efab66df92c37e4.StarlyUsdtSwapPair
1import Burner from 0xf233dcee88fe0abe
2import FungibleToken from 0xf233dcee88fe0abe
3import StarlyToken from 0x142fa6570b62fd97
4import TeleportedTetherToken from 0xcfdd90d4a00f7b5b
5import MetadataViews from 0x1d7e57aa55817448
6import FungibleTokenMetadataViews from 0xf233dcee88fe0abe
7
8// Exchange pair between StarlyToken and TeleportedTetherToken
9// Token1: StarlyToken
10// Token2: TeleportedTetherToken
11access(all) contract StarlyUsdtSwapPair: FungibleToken {
12 // Frozen flag controlled by Admin
13 access(all) var isFrozen: Bool
14
15 // Total supply of StarlyUsdtSwapPair 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 StarlyToken vault
22 access(contract) let token1Vault: @StarlyToken.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: "STARLY/tUSDT Swap LP Token",
89 symbol: "STARLYUSDT",
90 description: "BloctoSwap liquidity provider token for the STARLY/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 = StarlyUsdtSwapPair.account.storage.borrow<auth(FungibleToken.Withdraw) &StarlyUsdtSwapPair.Vault>(from: /storage/StarlyUsdtSwapLpVault)
99 ?? panic("Could not borrow reference to the contract's Vault!")
100 return FungibleTokenMetadataViews.FTVaultData(
101 storagePath: /storage/StarlyUsdtSwapLpVault,
102 receiverPath: /public/StarlyUsdtSwapLpReceiver,
103 metadataPath: /public/StarlyUsdtSwapLpBalance,
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: StarlyUsdtSwapPair.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 StarlyUsdtSwapPair 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 StarlyUsdtSwapPair.totalSupply = StarlyUsdtSwapPair.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! @StarlyUsdtSwapPair.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 StarlyUsdtSwapPair.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 StarlyUsdtSwapPair.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): @StarlyUsdtSwapPair.Vault {
220 return <-create Vault(balance: 0.0)
221 }
222
223 access(all) resource TokenBundle {
224 access(all) var token1: @StarlyToken.Vault
225 access(all) var token2: @TeleportedTetherToken.Vault
226
227 // initialize the vault bundle
228 init(fromToken1: @StarlyToken.Vault, fromToken2: @TeleportedTetherToken.Vault) {
229 self.token1 <- fromToken1
230 self.token2 <- fromToken2
231 }
232
233 access(all) fun depositToken1(from: @StarlyToken.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(): @StarlyToken.Vault {
242 var vault <- StarlyToken.createEmptyVault(vaultType: Type<@StarlyToken.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(): @StarlyUsdtSwapPair.TokenBundle {
257 return <- create TokenBundle(
258 fromToken1: <- StarlyToken.createEmptyVault(vaultType: Type<@StarlyToken.Vault>()),
259 fromToken2: <- TeleportedTetherToken.createEmptyVault(vaultType: Type<@TeleportedTetherToken.Vault>())
260 )
261 }
262
263 // createTokenBundle
264 //
265 access(all) fun createTokenBundle(fromToken1: @StarlyToken.Vault, fromToken2: @TeleportedTetherToken.Vault): @StarlyUsdtSwapPair.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): @StarlyUsdtSwapPair.Vault {
275 pre {
276 amount > 0.0: "Amount minted must be greater than zero"
277 }
278 StarlyUsdtSwapPair.totalSupply = StarlyUsdtSwapPair.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: @StarlyUsdtSwapPair.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 StarlyUsdtSwapPair.isFrozen = true
302 }
303
304 access(all) fun unfreeze() {
305 StarlyUsdtSwapPair.isFrozen = false
306 }
307
308 access(all) fun addInitialLiquidity(from: @StarlyUsdtSwapPair.TokenBundle): @StarlyUsdtSwapPair.Vault {
309 pre {
310 StarlyUsdtSwapPair.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 StarlyUsdtSwapPair.token1Vault.deposit(from: <- token1Vault)
320 StarlyUsdtSwapPair.token2Vault.deposit(from: <- token2Vault)
321
322 destroy from
323
324 // Create initial tokens
325 return <- StarlyUsdtSwapPair.mintTokens(amount: 1.0)
326 }
327
328 access(all) fun updateFeePercentage(feePercentage: UFix64) {
329 StarlyUsdtSwapPair.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: StarlyUsdtSwapPair.token1Vault.balance, token2Amount: StarlyUsdtSwapPair.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 (STARLY) -> Token2 (tUSDT)
399 access(all) fun swapToken1ForToken2(from: @StarlyToken.Vault): @TeleportedTetherToken.Vault {
400 pre {
401 !StarlyUsdtSwapPair.isFrozen: "StarlyUsdtSwapPair 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 (STARLY)
419 access(all) fun swapToken2ForToken1(from: @TeleportedTetherToken.Vault): @StarlyToken.Vault {
420 pre {
421 !StarlyUsdtSwapPair.isFrozen: "StarlyUsdtSwapPair 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)
436 }
437
438 // Used to add liquidity without minting new liquidity token
439 access(all) fun donateLiquidity(from: @StarlyUsdtSwapPair.TokenBundle) {
440 let token1Vault <- from.withdrawToken1()
441 let token2Vault <- from.withdrawToken2()
442
443 StarlyUsdtSwapPair.token1Vault.deposit(from: <- token1Vault)
444 StarlyUsdtSwapPair.token2Vault.deposit(from: <- token2Vault)
445
446 destroy from
447 }
448
449 access(all) fun addLiquidity(from: @StarlyUsdtSwapPair.TokenBundle): @StarlyUsdtSwapPair.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) / StarlyUsdtSwapPair.token1Vault.balance
462 let token2Percentage: UFix64 = (token2Vault.balance * 10000.0) / StarlyUsdtSwapPair.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 StarlyUsdtSwapPair.token1Vault.deposit(from: <- token1Vault)
471 StarlyUsdtSwapPair.token2Vault.deposit(from: <- token2Vault)
472
473 let liquidityTokenVault <- StarlyUsdtSwapPair.mintTokens(amount: (StarlyUsdtSwapPair.totalSupply * liquidityPercentage) / 10000.0)
474
475 destroy from
476 return <- liquidityTokenVault
477 }
478
479 access(all) fun removeLiquidity(from: @StarlyUsdtSwapPair.Vault): @StarlyUsdtSwapPair.TokenBundle {
480 pre {
481 from.balance > 0.0: "Empty liquidity token vault"
482 from.balance < StarlyUsdtSwapPair.totalSupply: "Cannot remove all liquidity"
483 }
484
485 // shift decimal 4 places to avoid truncation error
486 let liquidityPercentage = (from.balance * 10000.0) / StarlyUsdtSwapPair.totalSupply
487
488 assert(liquidityPercentage > 0.0, message: "Liquidity too small")
489
490 // Burn liquidity tokens and withdraw
491 StarlyUsdtSwapPair.burnTokens(from: <- from)
492
493 let token1Vault <- StarlyUsdtSwapPair.token1Vault.withdraw(amount: (StarlyUsdtSwapPair.token1Vault.balance * liquidityPercentage) / 10000.0)
494 let token2Vault <- StarlyUsdtSwapPair.token2Vault.withdraw(amount: (StarlyUsdtSwapPair.token2Vault.balance * liquidityPercentage) / 10000.0) as! @TeleportedTetherToken.Vault
495
496 let tokenBundle <- StarlyUsdtSwapPair.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/StarlyUsdtSwapLpVault
506 self.TokenPublicBalancePath = /public/StarlyUsdtSwapLpBalance
507 self.TokenPublicReceiverPath = /public/StarlyUsdtSwapLpReceiver
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/StarlyUsdtSwapLpVault)
513
514 // Setup internal StarlyToken vault
515 self.token1Vault <- StarlyToken.createEmptyVault(vaultType: Type<@StarlyToken.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/StarlyUsdtSwapAdmin)
522
523 // Emit an event that shows that the contract was initialized
524 emit TokensInitialized(initialSupply: self.totalSupply)
525 }
526}
527