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