Smart Contract
SwapPair
A.c353b9d685ec427d.SwapPair
1/**
2
3# SwapPair
4
5# Author: Increment Labs
6
7*/
8
9import DelegatorManager from 0xd6f80565193ad727
10import FungibleToken from 0xf233dcee88fe0abe
11import SwapInterfaces from 0xb78ef7afa52ff906
12import SwapConfig from 0xb78ef7afa52ff906
13import SwapError from 0xb78ef7afa52ff906
14import SwapFactory from 0xb063c16cac85dbd1
15
16
17access(all) contract SwapPair: FungibleToken {
18 /// Total supply of pair lpTokens in existence
19 access(all) var totalSupply: UFix64
20
21 /// Two vaults for the trading pair.
22 access(self) let token0Vault: @{FungibleToken.Vault}
23 access(self) let token1Vault: @{FungibleToken.Vault}
24 access(all) let token0VaultType: Type
25 access(all) let token1VaultType: Type
26 access(all) let token0Key: String
27 access(all) let token1Key: String
28
29 /// TWAP: last cumulative price
30 access(all) var blockTimestampLast: UFix64
31 access(all) var price0CumulativeLastScaled: UInt256
32 access(all) var price1CumulativeLastScaled: UInt256
33
34 /// Transaction lock
35 access(self) var lock: Bool
36
37 /// √(reserve0 * reserve1) for volatile pool, or √√[(r0^3 * r1 + r0 * r1^3) / 2] for stable pool, as of immediately after the most recent liquidity event
38 access(all) var rootKLast: UFix64
39
40 /// Reserved parameter fields: {ParamName: Value}
41 /// Used fields:
42 /// |__ 1. "isStableSwap" -> Bool
43 access(self) let _reservedFields: {String: AnyStruct}
44
45 /// Event that is emitted when the contract is created
46 access(all) event TokensInitialized(initialSupply: UFix64)
47 /// Event that is emitted when lp tokens are minted
48 access(all) event TokensMinted(amount: UFix64)
49 /// Event that is emitted when lp tokens are destroyed
50 access(all) event TokensBurned(amount: UFix64)
51 /// Event that is emitted when liquidity is added
52 access(all) event LiquidityAdded(amount0: UFix64, amount1: UFix64, reserve0After: UFix64, reserve1After: UFix64, amount0Type: String, amount1Type: String)
53 /// Event that is emitted when liquidity is removed
54 access(all) event LiquidityRemoved(amount0: UFix64, amount1: UFix64, reserve0After: UFix64, reserve1After: UFix64, amount0Type: String, amount1Type: String)
55 /// Event that is emitted when a swap trade happenes to this trading pair
56 access(all) event Swap(amount0In: UFix64, amount1In: UFix64, amount0Out: UFix64, amount1Out: UFix64, reserve0After: UFix64, reserve1After: UFix64, amount0Type: String, amount1Type: String)
57 /// Event that is emitted when a flashloan is originated from this SwapPair pool
58 access(all) event Flashloan(executor: Address, executorType: Type, originator: Address, requestedTokenType: String, requestedAmount: UFix64, reserve0After: UFix64, reserve1After: UFix64)
59
60 /// Lptoken Vault
61 ///
62 access(all) resource Vault: FungibleToken.Vault {
63 /// Holds the balance of a users tokens
64 access(all) var balance: UFix64
65
66 /// Initialize the balance at resource creation time
67 init(balance: UFix64) {
68 self.balance = balance
69 }
70
71 /// Called when this SwapPair's LpToken is burned via the `Burner.burn()` method
72 access(contract) fun burnCallback() {
73 if self.balance > 0.0 {
74 SwapPair.totalSupply = SwapPair.totalSupply - self.balance
75 }
76 self.balance = 0.0
77 }
78
79 /// getSupportedVaultTypes optionally returns a list of vault types that this receiver accepts
80 access(all) view fun getSupportedVaultTypes(): {Type: Bool} {
81 return {self.getType(): true}
82 }
83
84 access(all) view fun isSupportedVaultType(type: Type): Bool {
85 if (type == self.getType()) { return true } else { return false }
86 }
87
88 /// Asks if the amount can be withdrawn from this vault
89 access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool {
90 return amount <= self.balance
91 }
92
93 /// Added simply to conform to FT-V2 interface.
94 ///
95 /// SwapPair doesn't support MetadataViews api as this is not just a normal FT, and there could be multiple types of lpTokens
96 /// for different SwapPairs, so it is stored in LpTokenCollection instead (not directly associated with a StoragePath).
97 access(all) view fun getViews(): [Type] { return [] }
98 access(all) fun resolveView(_ view: Type): AnyStruct? { return nil }
99
100 /// withdraw
101 ///
102 /// Function that takes an integer amount as an argument
103 /// and withdraws that amount from the Vault.
104 /// It creates a new temporary Vault that is used to hold
105 /// the money that is being transferred. It returns the newly
106 /// created Vault to the context that called so it can be deposited
107 /// elsewhere.
108 ///
109 access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} {
110 self.balance = self.balance - amount
111 return <-create Vault(balance: amount)
112 }
113
114 /// deposit
115 ///
116 /// Function that takes a Vault object as an argument and adds
117 /// its balance to the balance of the owners Vault.
118 /// It is allowed to destroy the sent Vault because the Vault
119 /// was a temporary holder of the tokens. The Vault's balance has
120 /// been consumed and therefore can be destroyed.
121 ///
122 access(all) fun deposit(from: @{FungibleToken.Vault}) {
123 let vault <- from as! @SwapPair.Vault
124 self.balance = self.balance + vault.balance
125 vault.balance = 0.0
126 destroy vault
127 }
128
129 access(all) fun createEmptyVault(): @{FungibleToken.Vault} {
130 return <-create Vault(balance: 0.0)
131 }
132 }
133
134 access(all) struct PairInfo: SwapInterfaces.PairInfo {
135 access(all) let token0Key: String
136 access(all) let token1Key: String
137 access(all) let token0Reserve: UFix64
138 access(all) let token1Reserve: UFix64
139 access(all) let pairAddr: Address
140 access(all) let lpTokenSupply: UFix64
141 access(all) let swapFeeInBps: UInt64
142 access(all) let isStableswap: Bool
143 access(all) let stableCurveP: UFix64
144
145 view init(
146 _ token0Key: String,
147 _ token1Key: String,
148 _ token0Reserve: UFix64,
149 _ token1Reserve: UFix64,
150 _ pairAddr: Address,
151 _ lpTokenSupply: UFix64,
152 _ swapFeeInBps: UInt64,
153 _ isStableswap: Bool,
154 _ stableCurveP: UFix64
155 ) {
156 self.token0Key = token0Key
157 self.token1Key = token1Key
158 self.token0Reserve = token0Reserve
159 self.token1Reserve = token1Reserve
160 self.pairAddr = pairAddr
161 self.lpTokenSupply = lpTokenSupply
162 self.swapFeeInBps = swapFeeInBps
163 self.isStableswap = isStableswap
164 self.stableCurveP = stableCurveP
165 }
166 }
167
168 /// Added simply to conform to FT-V2 interface.
169 ///
170 /// SwapPair doesn't support MetadataViews api as this is not just a normal FT, and there could be multiple types of lpTokens
171 /// for different SwapPairs, so it is stored in LpTokenCollection instead (not directly associated with a StoragePath).
172 access(all) view fun getContractViews(resourceType: Type?): [Type] { return [] }
173 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { return nil }
174
175 /// createEmptyVault
176 //
177 /// Function that creates a new Vault with a balance of zero
178 /// and returns it to the calling context. A user must call this function
179 /// and store the returned Vault in their storage in order to allow their
180 /// account to be able to receive deposits of this token type.
181 ///
182 access(all) fun createEmptyVault(vaultType: Type): @SwapPair.Vault {
183 return <-create Vault(balance: 0.0)
184 }
185
186 /// Permanently lock the first small amount lpTokens
187 access(self) fun donateInitialMinimumLpToken(donateLpBalance: UFix64) {
188 self.totalSupply = self.totalSupply + donateLpBalance
189 emit TokensMinted(amount: donateLpBalance)
190 }
191
192 /// Mint lpTokens
193 access(self) fun mintLpToken(amount: UFix64): @SwapPair.Vault {
194 self.totalSupply = self.totalSupply + amount
195 emit TokensMinted(amount: amount)
196 return <- create Vault(balance: amount)
197 }
198
199 /// Burn lpTokens
200 access(self) fun burnLpToken(from: @SwapPair.Vault) {
201 let amount = from.balance
202 self.totalSupply = self.totalSupply - amount
203 destroy from
204 emit TokensBurned(amount: amount)
205 }
206
207 /// Add liquidity
208 ///
209 access(all) fun addLiquidity(tokenAVault: @{FungibleToken.Vault}, tokenBVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
210 pre {
211 tokenAVault.balance > 0.0 && tokenBVault.balance > 0.0:
212 SwapError.ErrorEncode(
213 msg: "SwapPair: added zero liquidity",
214 err: SwapError.ErrorCode.ADD_ZERO_LIQUIDITY
215 )
216 (tokenAVault.isInstance(self.token0VaultType) && tokenBVault.isInstance(self.token1VaultType)) ||
217 (tokenBVault.isInstance(self.token0VaultType) && tokenAVault.isInstance(self.token1VaultType)):
218 SwapError.ErrorEncode(
219 msg: "SwapPair: added incompatible liquidity pair vaults",
220 err: SwapError.ErrorCode.INVALID_PARAMETERS
221 )
222 self.lock == false: SwapError.ErrorEncode(msg: "SwapPair: Reentrant", err: SwapError.ErrorCode.REENTRANT)
223 }
224 post {
225 self.lock == false: SwapError.ErrorEncode(msg: "SwapPair: Reentrant", err: SwapError.ErrorCode.REENTRANT)
226 }
227 self.lock = true
228
229 let reserve0LastScaled = SwapConfig.UFix64ToScaledUInt256(self.token0Vault.balance)
230 let reserve1LastScaled = SwapConfig.UFix64ToScaledUInt256(self.token1Vault.balance)
231
232 /// Update twap at the first transaction in one block with the last block balance
233 self._update(reserve0Last: self.token0Vault.balance, reserve1Last: self.token1Vault.balance)
234 /// Mint fee
235 let feeOn = self._mintFee(reserve0: self.token0Vault.balance, reserve1: self.token1Vault.balance)
236
237 var liquidity = 0.0
238 var amount0Added = 0.0
239 var amount1Added = 0.0
240 if (self.totalSupply == 0.0) {
241 var donateLpBalance = 0.0
242 if self.isStableSwap() {
243 donateLpBalance = 0.0001 // 1e-4
244 } else {
245 donateLpBalance = 0.000001 // 1e-6
246 }
247 // When adding initial liquidity, the balance should not be below certain minimum amount
248 assert(tokenAVault.balance > donateLpBalance && tokenBVault.balance > donateLpBalance, message:
249 SwapError.ErrorEncode(
250 msg: "SwapPair: less than initial minimum liquidity",
251 err: SwapError.ErrorCode.BELOW_MINIMUM_INITIAL_LIQUIDITY
252 )
253 )
254 /// Add initial liquidity
255 if (tokenAVault.isInstance(self.token0VaultType)) {
256 amount0Added = tokenAVault.balance
257 amount1Added = tokenBVault.balance
258 self.token0Vault.deposit(from: <-tokenAVault)
259 self.token1Vault.deposit(from: <-tokenBVault)
260 } else {
261 amount0Added = tokenBVault.balance
262 amount1Added = tokenAVault.balance
263 self.token0Vault.deposit(from: <-tokenBVault)
264 self.token1Vault.deposit(from: <-tokenAVault)
265 }
266 /// Mint initial liquidity token and donate initial minimum liquidity token
267 let initialLpAmount = self._rootK(balance0: self.token0Vault.balance, balance1: self.token1Vault.balance)
268 self.donateInitialMinimumLpToken(donateLpBalance: donateLpBalance)
269
270 liquidity = initialLpAmount - donateLpBalance
271 } else {
272 var lptokenMintAmount0Scaled: UInt256 = 0
273 var lptokenMintAmount1Scaled: UInt256 = 0
274 /// Use UFIx64ToUInt256 in division & multiply to solve precision issues
275 let inAmountAScaled = SwapConfig.UFix64ToScaledUInt256(tokenAVault.balance)
276 let inAmountBScaled = SwapConfig.UFix64ToScaledUInt256(tokenBVault.balance)
277
278 let totalSupplyScaled = SwapConfig.UFix64ToScaledUInt256(self.totalSupply)
279
280 if (tokenAVault.isInstance(self.token0VaultType)) {
281 lptokenMintAmount0Scaled = inAmountAScaled * totalSupplyScaled / reserve0LastScaled
282 lptokenMintAmount1Scaled = inAmountBScaled * totalSupplyScaled / reserve1LastScaled
283 amount0Added = tokenAVault.balance
284 amount1Added = tokenBVault.balance
285 self.token0Vault.deposit(from: <-tokenAVault)
286 self.token1Vault.deposit(from: <-tokenBVault)
287 } else {
288 lptokenMintAmount0Scaled = inAmountBScaled * totalSupplyScaled / reserve0LastScaled
289 lptokenMintAmount1Scaled = inAmountAScaled * totalSupplyScaled / reserve1LastScaled
290 amount0Added = tokenBVault.balance
291 amount1Added = tokenAVault.balance
292 self.token0Vault.deposit(from: <-tokenBVault)
293 self.token1Vault.deposit(from: <-tokenAVault)
294 }
295
296 /// Note: User should add proportional liquidity as any extra is added into pool.
297 let mintLptokenAmountScaled = lptokenMintAmount0Scaled < lptokenMintAmount1Scaled ? lptokenMintAmount0Scaled : lptokenMintAmount1Scaled
298
299 /// Mint liquidity token pro rata
300 liquidity = SwapConfig.ScaledUInt256ToUFix64(mintLptokenAmountScaled)
301 }
302 /// Mint lpTokens
303 let lpTokenVault <-self.mintLpToken(amount: liquidity)
304 emit LiquidityAdded(
305 amount0: amount0Added, amount1: amount1Added,
306 reserve0After: self.token0Vault.balance, reserve1After: self.token1Vault.balance,
307 amount0Type: self.token0Key, amount1Type: self.token1Key
308 )
309
310 if feeOn {
311 self.rootKLast = self._rootK(balance0: self.token0Vault.balance, balance1: self.token1Vault.balance)
312 }
313
314 self.lock = false
315 return <-lpTokenVault
316 }
317
318 /// Remove Liquidity
319 ///
320 /// @Return: @[FungibleToken.Vault; 2]
321 ///
322 access(all) fun removeLiquidity(lpTokenVault: @{FungibleToken.Vault}) : @[{FungibleToken.Vault}] {
323 pre {
324 lpTokenVault.balance > 0.0:
325 SwapError.ErrorEncode(
326 msg: "SwapPair: removed zero liquidity",
327 err: SwapError.ErrorCode.INVALID_PARAMETERS
328 )
329 lpTokenVault.isInstance(Type<@SwapPair.Vault>()):
330 SwapError.ErrorEncode(
331 msg: "SwapPair: input vault type mismatch with lpTokenVault type",
332 err: SwapError.ErrorCode.MISMATCH_LPTOKEN_VAULT
333 )
334 self.lock == false: SwapError.ErrorEncode(msg: "SwapPair: Reentrant", err: SwapError.ErrorCode.REENTRANT)
335 }
336 post {
337 self.lock == false: SwapError.ErrorEncode(msg: "SwapPair: Reentrant", err: SwapError.ErrorCode.REENTRANT)
338 }
339 self.lock = true
340
341 let reserve0LastScaled = SwapConfig.UFix64ToScaledUInt256(self.token0Vault.balance)
342 let reserve1LastScaled = SwapConfig.UFix64ToScaledUInt256(self.token1Vault.balance)
343
344 /// Update twap
345 self._update(reserve0Last: self.token0Vault.balance, reserve1Last: self.token1Vault.balance)
346 /// Mint fee
347 let feeOn = self._mintFee(reserve0: self.token0Vault.balance, reserve1: self.token1Vault.balance)
348
349 /// Use UFIx64ToUInt256 in division & multiply to solve precision issues
350 let removeAmountScaled = SwapConfig.UFix64ToScaledUInt256(lpTokenVault.balance)
351 let totalSupplyScaled = SwapConfig.UFix64ToScaledUInt256(self.totalSupply)
352
353 let token0AmountScaled = removeAmountScaled * reserve0LastScaled / totalSupplyScaled
354 let token1AmountScaled = removeAmountScaled * reserve1LastScaled / totalSupplyScaled
355 let token0Amount = SwapConfig.ScaledUInt256ToUFix64(token0AmountScaled)
356 let token1Amount = SwapConfig.ScaledUInt256ToUFix64(token1AmountScaled)
357
358 let withdrawnToken0 <- self.token0Vault.withdraw(amount: token0Amount)
359 let withdrawnToken1 <- self.token1Vault.withdraw(amount: token1Amount)
360
361 /// Burn lpTokens
362 self.burnLpToken(from: <- (lpTokenVault as! @SwapPair.Vault))
363 emit LiquidityRemoved(
364 amount0: withdrawnToken0.balance, amount1: withdrawnToken1.balance,
365 reserve0After: self.token0Vault.balance, reserve1After: self.token1Vault.balance,
366 amount0Type: self.token0Key, amount1Type: self.token1Key
367 )
368
369 if feeOn {
370 self.rootKLast = self._rootK(balance0: self.token0Vault.balance, balance1: self.token1Vault.balance)
371 }
372
373 self.lock = false
374 return <- [<-withdrawnToken0, <-withdrawnToken1]
375 }
376
377 /// Swap
378 ///
379 access(all) fun swap(vaultIn: @{FungibleToken.Vault}, exactAmountOut: UFix64?): @{FungibleToken.Vault} {
380 pre {
381 vaultIn.balance > 0.0: SwapError.ErrorEncode(msg: "SwapPair: zero in balance", err: SwapError.ErrorCode.INVALID_PARAMETERS)
382 vaultIn.isInstance(self.token0VaultType) || vaultIn.isInstance(self.token1VaultType):
383 SwapError.ErrorEncode(
384 msg: "SwapPair: incompatible in token vault",
385 err: SwapError.ErrorCode.INVALID_PARAMETERS
386 )
387 self.lock == false: SwapError.ErrorEncode(msg: "SwapPair: Reentrant", err: SwapError.ErrorCode.REENTRANT)
388 }
389 post {
390 self.lock == false: SwapError.ErrorEncode(msg: "SwapPair: Reentrant", err: SwapError.ErrorCode.REENTRANT)
391 }
392 self.lock = true
393
394 self._update(reserve0Last: self.token0Vault.balance, reserve1Last: self.token1Vault.balance)
395
396 let amountIn = vaultIn.balance
397 var amountOut = 0.0
398 /// Calculate the swap result
399 if (vaultIn.isInstance(self.token0VaultType)) {
400 if self.isStableSwap() {
401 amountOut = SwapConfig.getAmountOutStable(amountIn: amountIn, reserveIn: self.token0Vault.balance, reserveOut: self.token1Vault.balance, p: self.getStableCurveP(), swapFeeRateBps: self.getSwapFeeBps())
402 } else {
403 amountOut = SwapConfig.getAmountOutVolatile(amountIn: amountIn, reserveIn: self.token0Vault.balance, reserveOut: self.token1Vault.balance, swapFeeRateBps: self.getSwapFeeBps())
404 }
405 } else {
406 if self.isStableSwap() {
407 amountOut = SwapConfig.getAmountOutStable(amountIn: amountIn, reserveIn: self.token1Vault.balance, reserveOut: self.token0Vault.balance, p: 1.0/self.getStableCurveP(), swapFeeRateBps: self.getSwapFeeBps())
408 } else {
409 amountOut = SwapConfig.getAmountOutVolatile(amountIn: amountIn, reserveIn: self.token1Vault.balance, reserveOut: self.token0Vault.balance, swapFeeRateBps: self.getSwapFeeBps())
410 }
411 }
412 /// Check and swap exact output amount if specified in argument
413 if exactAmountOut != nil {
414 assert(amountOut >= exactAmountOut!, message:
415 SwapError.ErrorEncode(
416 msg: "SwapPair: INSUFFICIENT_OUTPUT_AMOUNT",
417 err: SwapError.ErrorCode.INSUFFICIENT_OUTPUT_AMOUNT
418 )
419 )
420 amountOut = exactAmountOut!
421 }
422
423 if (vaultIn.isInstance(self.token0VaultType)) {
424 self.token0Vault.deposit(from: <-vaultIn)
425 emit Swap(
426 amount0In: amountIn, amount1In: 0.0,
427 amount0Out: 0.0, amount1Out: amountOut,
428 reserve0After: self.token0Vault.balance, reserve1After: self.token1Vault.balance - amountOut,
429 amount0Type: self.token0Key, amount1Type: self.token1Key
430 )
431 self.lock = false
432 return <- self.token1Vault.withdraw(amount: amountOut)
433 } else {
434 self.token1Vault.deposit(from: <-vaultIn)
435 emit Swap(
436 amount0In: 0.0, amount1In: amountIn,
437 amount0Out: amountOut, amount1Out: 0.0,
438 reserve0After: self.token0Vault.balance - amountOut, reserve1After: self.token1Vault.balance,
439 amount0Type: self.token0Key, amount1Type: self.token1Key
440 )
441 self.lock = false
442 return <- self.token0Vault.withdraw(amount: amountOut)
443 }
444 }
445
446 /// An executor contract can request to use the whole liquidity of current SwapPair and perform custom operations (like arbitrage, liquidation, et al.), as long as:
447 /// 1. executor implements FlashLoanExecutor resource interface and sets up corresponding resource to receive & process requested tokens, and
448 /// 2. executor repays back requested amount + fees (dominated by 'flashloanRateBps x amount'), and
449 /// all in one atomic function call.
450 /// @params: User-definited extra data passed to executor for further auth/check/decode
451 ///
452 access(all) fun flashloan(executor: &{SwapInterfaces.FlashLoanExecutor}, requestedTokenVaultType: Type, requestedAmount: UFix64, params: {String: AnyStruct}) {
453 pre {
454 requestedTokenVaultType == self.token0VaultType || requestedTokenVaultType == self.token1VaultType:
455 SwapError.ErrorEncode(
456 msg: "SwapPair: flashloan invalid requested token type",
457 err: SwapError.ErrorCode.INVALID_PARAMETERS
458 )
459 (requestedTokenVaultType == self.token0VaultType && requestedAmount > 0.0 && requestedAmount < self.token0Vault.balance) ||
460 (requestedTokenVaultType == self.token1VaultType && requestedAmount > 0.0 && requestedAmount < self.token1Vault.balance) :
461 SwapError.ErrorEncode(
462 msg: "SwapPair: flashloan invalid requested amount",
463 err: SwapError.ErrorCode.INVALID_PARAMETERS
464 )
465 self.lock == false: SwapError.ErrorEncode(msg: "SwapPair: Reentrant", err: SwapError.ErrorCode.REENTRANT)
466 }
467 post {
468 self.lock == false: SwapError.ErrorEncode(msg: "SwapPair: Reentrant", err: SwapError.ErrorCode.REENTRANT)
469 }
470 self.lock = true
471
472 self._update(reserve0Last: self.token0Vault.balance, reserve1Last: self.token1Vault.balance)
473
474 var tokenOut: @{FungibleToken.Vault}? <- nil
475 if (requestedTokenVaultType == self.token0VaultType) {
476 tokenOut <-! self.token0Vault.withdraw(amount: requestedAmount)
477 } else {
478 tokenOut <-! self.token1Vault.withdraw(amount: requestedAmount)
479 }
480 // Send loans and invoke custom executor
481 let tokenIn <- executor.executeAndRepay(loanedToken: <- tokenOut!, params: params)
482 assert(tokenIn.isInstance(requestedTokenVaultType), message:
483 SwapError.ErrorEncode(
484 msg: "SwapPair: flashloan repaid incompatible token",
485 err: SwapError.ErrorCode.INVALID_PARAMETERS
486 )
487 )
488 assert(tokenIn.balance >= requestedAmount * (1.0 + UFix64(SwapFactory.getFlashloanRateBps()) / 10000.0), message:
489 SwapError.ErrorEncode(
490 msg: "SwapPair: flashloan insufficient repayment",
491 err: SwapError.ErrorCode.INVALID_PARAMETERS
492 )
493 )
494 if (requestedTokenVaultType == self.token0VaultType) {
495 self.token0Vault.deposit(from: <- tokenIn)
496 } else {
497 self.token1Vault.deposit(from: <- tokenIn)
498 }
499
500 emit Flashloan(
501 executor: executor.owner!.address,
502 executorType: executor.getType(),
503 originator: self.account.address,
504 requestedTokenType: requestedTokenVaultType == self.token0VaultType ? self.token0Key : self.token1Key,
505 requestedAmount: requestedAmount,
506 reserve0After: self.token0Vault.balance,
507 reserve1After: self.token1Vault.balance
508 )
509
510 self.lock = false
511 }
512
513 /// Update cumulative price on the first call per block
514 access(self) fun _update(reserve0Last: UFix64, reserve1Last: UFix64) {
515 let blockTimestamp = getCurrentBlock().timestamp
516 let timeElapsed = blockTimestamp - self.blockTimestampLast
517 if (timeElapsed > 0.0 && reserve0Last != 0.0 && reserve1Last != 0.0) {
518 let timeElapsedScaled = SwapConfig.UFix64ToScaledUInt256(timeElapsed)
519 let stableswap_p = self.getStableCurveP()
520 let price0 = self.isStableSwap() ?
521 SwapConfig.quoteStable(amountA: 1.0, reserveA: reserve0Last, reserveB: reserve1Last, p: stableswap_p) :
522 SwapConfig.quote(amountA: 1.0, reserveA: reserve0Last, reserveB: reserve1Last)
523 var price1 = 0.0
524 if (price0 == 0.0) {
525 price1 = self.isStableSwap() ?
526 SwapConfig.quoteStable(amountA: 1.0, reserveA: reserve1Last, reserveB: reserve0Last, p: 1.0 / stableswap_p) :
527 SwapConfig.quote(amountA: 1.0, reserveA: reserve1Last, reserveB: reserve0Last)
528 } else {
529 price1 = 1.0 / price0
530 }
531
532 self.price0CumulativeLastScaled = SwapConfig.overflowAddUInt256(
533 self.price0CumulativeLastScaled,
534 SwapConfig.UFix64ToScaledUInt256(price0) * timeElapsedScaled / SwapConfig.scaleFactor
535 )
536 self.price1CumulativeLastScaled = SwapConfig.overflowAddUInt256(
537 self.price1CumulativeLastScaled,
538 SwapConfig.UFix64ToScaledUInt256(price1) * timeElapsedScaled / SwapConfig.scaleFactor
539 )
540 }
541 self.blockTimestampLast = blockTimestamp
542 }
543
544 /// If feeTo is set, mint 1/6th (the default cut) of the growth in sqrt(k) which is only generated by trading behavior.
545 /// Instead of collecting protocol fees at the time of each trade, accumulated fees are collected only
546 /// when liquidity is deposited or withdrawn.
547 ///
548 access(self) fun _mintFee(reserve0: UFix64, reserve1: UFix64): Bool {
549 let rootKLast = self.rootKLast
550 if SwapFactory.feeTo == nil {
551 if rootKLast > 0.0 {
552 self.rootKLast = 0.0
553 }
554 return false
555 }
556 if rootKLast > 0.0 {
557 let rootK = self._rootK(balance0: reserve0, balance1: reserve1)
558 if rootK > rootKLast {
559 let numerator = self.totalSupply * (rootK - rootKLast)
560 let denominator = (1.0 / SwapFactory.getProtocolFeeCut() - 1.0) * rootK + rootKLast
561 let liquidity = numerator / denominator
562 if liquidity > 0.0 {
563 let lpTokenVault <-self.mintLpToken(amount: liquidity)
564 let lpTokenCollectionCap = getAccount(SwapFactory.feeTo!).capabilities.get<&{SwapInterfaces.LpTokenCollectionPublic}>(SwapConfig.LpTokenCollectionPublicPath)
565 assert(lpTokenCollectionCap.check(), message:
566 SwapError.ErrorEncode(
567 msg: "SwapPair: Cannot borrow reference to LpTokenCollection resource in feeTo account",
568 err: SwapError.ErrorCode.LOST_PUBLIC_CAPABILITY
569 )
570 )
571 lpTokenCollectionCap.borrow()!.deposit(pairAddr: self.account.address, lpTokenVault: <-lpTokenVault)
572 }
573 }
574 }
575 return true
576 }
577
578 access(all) view fun _rootK(balance0: UFix64, balance1: UFix64): UFix64 {
579 let e18: UInt256 = SwapConfig.scaleFactor
580 let balance0Scaled = SwapConfig.UFix64ToScaledUInt256(balance0)
581 let balance1Scaled = SwapConfig.UFix64ToScaledUInt256(balance1)
582 if self.isStableSwap() {
583 let _p_scaled: UInt256 = SwapConfig.UFix64ToScaledUInt256(self.getStableCurveP())
584 let _k_scaled: UInt256 = SwapConfig.k_stable_p(balance0Scaled, balance1Scaled, _p_scaled)
585 return SwapConfig.ScaledUInt256ToUFix64(SwapConfig.sqrt(SwapConfig.sqrt(_k_scaled / 2)))
586 } else {
587 return SwapConfig.ScaledUInt256ToUFix64(SwapConfig.sqrt(balance0Scaled * balance1Scaled / e18))
588 }
589 }
590
591 access(all) view fun isStableSwap(): Bool {
592 return (self._reservedFields["isStableSwap"] as! Bool?) ?? false
593 }
594
595 access(all) view fun getSwapFeeBps(): UInt64 {
596 return SwapFactory.getSwapFeeRateBps(stableMode: self.isStableSwap())
597 }
598
599 /// P for stFlow <> Flow pair
600 access(all) view fun getStableCurveP(): UFix64 {
601 if (self.token0Key == "A.1654653399040a61.FlowToken" && self.token1Key == "A.d6f80565193ad727.stFlowToken") {
602 let price_flow2Stflow = DelegatorManager.borrowCurrentQuoteEpochSnapshot().scaledQuoteFlowStFlow
603 return SwapConfig.ScaledUInt256ToUFix64(price_flow2Stflow)
604 } else if (self.token1Key == "A.1654653399040a61.FlowToken" && self.token0Key == "A.d6f80565193ad727.stFlowToken") {
605 let price_stflow2Flow = DelegatorManager.borrowCurrentQuoteEpochSnapshot().scaledQuoteStFlowFlow
606 return SwapConfig.ScaledUInt256ToUFix64(price_stflow2Flow)
607 } else {
608 return 1.0
609 }
610 }
611
612 /// Public interfaces
613 ///
614 access(all) resource PairPublic: SwapInterfaces.PairPublic {
615 access(all) fun swap(vaultIn: @{FungibleToken.Vault}, exactAmountOut: UFix64?): @{FungibleToken.Vault} {
616 return <- SwapPair.swap(vaultIn: <-vaultIn, exactAmountOut: exactAmountOut)
617 }
618
619 access(all) fun flashloan(executor: &{SwapInterfaces.FlashLoanExecutor}, requestedTokenVaultType: Type, requestedAmount: UFix64, params: {String: AnyStruct}) {
620 SwapPair.flashloan(executor: executor, requestedTokenVaultType: requestedTokenVaultType, requestedAmount: requestedAmount, params: params)
621 }
622
623 access(all) fun removeLiquidity(lpTokenVault: @{FungibleToken.Vault}) : @[{FungibleToken.Vault}] {
624 return <- SwapPair.removeLiquidity(lpTokenVault: <- lpTokenVault)
625 }
626
627 access(all) fun addLiquidity(tokenAVault: @{FungibleToken.Vault}, tokenBVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
628 return <- SwapPair.addLiquidity(tokenAVault: <- tokenAVault, tokenBVault: <- tokenBVault)
629 }
630
631 access(all) view fun getAmountIn(amountOut: UFix64, tokenOutKey: String): UFix64 {
632 if SwapPair.isStableSwap() {
633 if tokenOutKey == SwapPair.token1Key {
634 return SwapConfig.getAmountInStable(amountOut: amountOut, reserveIn: SwapPair.token0Vault.balance, reserveOut: SwapPair.token1Vault.balance, p: SwapPair.getStableCurveP(), swapFeeRateBps: SwapPair.getSwapFeeBps())
635 } else {
636 return SwapConfig.getAmountInStable(amountOut: amountOut, reserveIn: SwapPair.token1Vault.balance, reserveOut: SwapPair.token0Vault.balance, p: 1.0/SwapPair.getStableCurveP(), swapFeeRateBps: SwapPair.getSwapFeeBps())
637 }
638 } else {
639 if tokenOutKey == SwapPair.token1Key {
640 return SwapConfig.getAmountInVolatile(amountOut: amountOut, reserveIn: SwapPair.token0Vault.balance, reserveOut: SwapPair.token1Vault.balance, swapFeeRateBps: SwapPair.getSwapFeeBps())
641 } else {
642 return SwapConfig.getAmountInVolatile(amountOut: amountOut, reserveIn: SwapPair.token1Vault.balance, reserveOut: SwapPair.token0Vault.balance, swapFeeRateBps: SwapPair.getSwapFeeBps())
643 }
644 }
645 }
646 access(all) view fun getAmountOut(amountIn: UFix64, tokenInKey: String): UFix64 {
647 if SwapPair.isStableSwap() {
648 if tokenInKey == SwapPair.token0Key {
649 return SwapConfig.getAmountOutStable(amountIn: amountIn, reserveIn: SwapPair.token0Vault.balance, reserveOut: SwapPair.token1Vault.balance, p: SwapPair.getStableCurveP(), swapFeeRateBps: SwapPair.getSwapFeeBps())
650 } else {
651 return SwapConfig.getAmountOutStable(amountIn: amountIn, reserveIn: SwapPair.token1Vault.balance, reserveOut: SwapPair.token0Vault.balance, p: 1.0/SwapPair.getStableCurveP(), swapFeeRateBps: SwapPair.getSwapFeeBps())
652 }
653 } else {
654 if tokenInKey == SwapPair.token0Key {
655 return SwapConfig.getAmountOutVolatile(amountIn: amountIn, reserveIn: SwapPair.token0Vault.balance, reserveOut: SwapPair.token1Vault.balance, swapFeeRateBps: SwapPair.getSwapFeeBps())
656 } else {
657 return SwapConfig.getAmountOutVolatile(amountIn: amountIn, reserveIn: SwapPair.token1Vault.balance, reserveOut: SwapPair.token0Vault.balance, swapFeeRateBps: SwapPair.getSwapFeeBps())
658 }
659 }
660 }
661 access(all) view fun getPrice0CumulativeLastScaled(): UInt256 {
662 return SwapPair.price0CumulativeLastScaled
663 }
664 access(all) view fun getPrice1CumulativeLastScaled(): UInt256 {
665 return SwapPair.price1CumulativeLastScaled
666 }
667 access(all) view fun getBlockTimestampLast(): UFix64 {
668 return SwapPair.blockTimestampLast
669 }
670
671 access(all) view fun getPairInfo(): [AnyStruct] {
672 return [
673 SwapPair.token0Key, // 0
674 SwapPair.token1Key,
675 SwapPair.token0Vault.balance,
676 SwapPair.token1Vault.balance,
677 SwapPair.account.address,
678 SwapPair.totalSupply, // 5
679 SwapPair.getSwapFeeBps(),
680 SwapPair.isStableSwap(),
681 SwapPair.getStableCurveP()
682 ]
683 }
684
685 access(all) view fun getPairInfoStruct(): PairInfo {
686 return PairInfo(
687 SwapPair.token0Key,
688 SwapPair.token1Key,
689 SwapPair.token0Vault.balance,
690 SwapPair.token1Vault.balance,
691 SwapPair.account.address,
692 SwapPair.totalSupply,
693 SwapPair.getSwapFeeBps(),
694 SwapPair.isStableSwap(),
695 SwapPair.getStableCurveP()
696 )
697 }
698
699 access(all) view fun getLpTokenVaultType(): Type {
700 return Type<@SwapPair.Vault>()
701 }
702
703 access(all) view fun isStableSwap(): Bool {
704 return SwapPair.isStableSwap()
705 }
706
707 access(all) view fun getStableCurveP(): UFix64 {
708 return SwapPair.getStableCurveP()
709 }
710 }
711
712 init(token0Vault: @{FungibleToken.Vault}, token1Vault: @{FungibleToken.Vault}, stableMode: Bool) {
713 self.totalSupply = 0.0
714
715 self.token0VaultType = token0Vault.getType()
716 self.token1VaultType = token1Vault.getType()
717 self.token0Vault <- token0Vault
718 self.token1Vault <- token1Vault
719
720 self.lock = false
721
722 self.blockTimestampLast = getCurrentBlock().timestamp
723 self.price0CumulativeLastScaled = 0
724 self.price1CumulativeLastScaled = 0
725
726 self.rootKLast = 0.0
727 self._reservedFields = {}
728 self._reservedFields["isStableSwap"] = stableMode
729
730 self.token0Key = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: self.token0VaultType.identifier)
731 self.token1Key = SwapConfig.SliceTokenTypeIdentifierFromVaultType(vaultTypeIdentifier: self.token1VaultType.identifier)
732
733 /// Setup PairPublic interface capability
734 destroy <-self.account.storage.load<@AnyResource>(from: /storage/pair_public)
735 self.account.storage.save(<-create PairPublic(), to: /storage/pair_public)
736 let pairPublicCap = self.account.capabilities.storage.issue<&{SwapInterfaces.PairPublic}>(/storage/pair_public)
737 self.account.capabilities.publish(pairPublicCap, at: SwapConfig.PairPublicPath)
738
739 emit TokensInitialized(initialSupply: self.totalSupply)
740 }
741}
742