Smart Contract
PierPair
A.609e10301860b683.PierPair
1import FungibleToken from 0xf233dcee88fe0abe
2import MultiFungibleToken from 0xa378eeb799df8387
3import PierLPToken from 0x609e10301860b683
4import IPierPair from 0x609e10301860b683
5import PierMath from 0xa378eeb799df8387
6import PierSwapSettings from 0x066a74dfb4da0306
7
8/**
9
10PierPair is the implementation of IPierPair.
11
12@author Metapier Foundation Ltd.
13
14 */
15pub contract PierPair: IPierPair {
16
17 // The initial liquidity that will be minted and locked
18 // by the pool. It's fixed to 1e-5.
19 pub let MINIMUM_LIQUIDITY: UFix64
20
21 pub event ContractInitialized()
22 pub event Swap(poolId: UInt64, amountIn: UFix64, amountOut: UFix64, swapAForB: Bool)
23 pub event Mint(poolId: UInt64, amountAIn: UFix64, amountBIn: UFix64)
24 pub event Burn(poolId: UInt64, amountLP: UFix64, amountAOut: UFix64, amountBOut: UFix64)
25
26 access(self) let lpTokenAdmin: @PierLPToken.Admin
27
28 pub resource Pool: IPierPair.IPool {
29 pub let poolId: UInt64
30 pub var kLast: UInt256
31
32 pub let tokenAType: Type
33 pub let tokenBType: Type
34
35 pub var lastBlockTimestamp: UFix64
36 pub var lastPriceACumulative: Word64
37 pub var lastPriceBCumulative: Word64
38
39 // Lock to prevent reentrancy attacks
40 access(self) var lock: Bool
41
42 access(self) let tokenAVault: @FungibleToken.Vault
43 access(self) let tokenBVault: @FungibleToken.Vault
44 access(self) let lpTokenMaster: @PierLPToken.TokenMaster
45
46 pub fun getReserves(): [UFix64; 2] {
47 return [self.tokenAVault.balance, self.tokenBVault.balance]
48 }
49
50 pub fun swap(fromVault: @FungibleToken.Vault, forAmount: UFix64): @FungibleToken.Vault {
51 pre {
52 !self.lock: "Metapier PierPair: Reentrant call"
53 }
54 post {
55 !self.lock: "Metapier PierPair: Lock not released"
56 }
57 self.lock = true
58
59 let reserveALast = self.tokenAVault.balance
60 let reserveBLast = self.tokenBVault.balance
61
62 let swapAForB = fromVault.isInstance(self.tokenAType)
63 var amountAIn = 0.0
64 var amountBIn = 0.0
65 var outputVault: @FungibleToken.Vault? <- nil
66
67 if swapAForB {
68 assert(reserveBLast > forAmount, message: "Metapier PierPair: Insufficient liquidity")
69 amountAIn = fromVault.balance
70 self.tokenAVault.deposit(from: <-fromVault)
71 outputVault <-!self.tokenBVault.withdraw(amount: forAmount)
72 } else {
73 assert(reserveALast > forAmount, message: "Metapier PierPair: Insufficient liquidity")
74 amountBIn = fromVault.balance
75 self.tokenBVault.deposit(from: <-fromVault)
76 outputVault <-!self.tokenAVault.withdraw(amount: forAmount)
77 }
78
79 let totalFeeCoefficient = UInt256(PierSwapSettings.getPoolTotalFeeCoefficient())
80
81 // adjustedBalanceA = balanceA * 1000 - amountAIn * TotalFeeCoefficient
82 let adjustedBalanceA = PierMath.UFix64ToRawUInt256(self.tokenAVault.balance) * 1000
83 - PierMath.UFix64ToRawUInt256(amountAIn) * totalFeeCoefficient
84
85 // adjustedBalanceB = balanceB * 1000 - amountBIn * TotalFeeCoefficient
86 let adjustedBalanceB = PierMath.UFix64ToRawUInt256(self.tokenBVault.balance) * 1000
87 - PierMath.UFix64ToRawUInt256(amountBIn) * totalFeeCoefficient
88
89 // prevK = reserveALast * reserveBLast * 1000^2
90 let prevK = PierMath.UFix64ToRawUInt256(reserveALast)
91 * PierMath.UFix64ToRawUInt256(reserveBLast)
92 * 1_000_000
93
94 assert(
95 adjustedBalanceA * adjustedBalanceB >= prevK,
96 message: "Metapier PierPair: K not maintained"
97 )
98
99 self.makeObservation(reserveA: reserveALast, reserveB: reserveBLast)
100
101 emit Swap(
102 poolId: self.poolId,
103 amountIn: swapAForB ? amountAIn : amountBIn,
104 amountOut: forAmount,
105 swapAForB: swapAForB
106 )
107
108 self.lock = false
109 return <-outputVault!
110 }
111
112 pub fun mint(vaultA: @FungibleToken.Vault, vaultB: @FungibleToken.Vault): @PierLPToken.Vault {
113 pre {
114 !self.lock: "Metapier PierPair: Reentrant call"
115 }
116 post {
117 !self.lock: "Metapier PierPair: Lock not released"
118 }
119 self.lock = true
120
121 let reserveALast = self.tokenAVault.balance
122 let reserveBLast = self.tokenBVault.balance
123
124 let amountA = vaultA.balance
125 let amountB = vaultB.balance
126
127 self.tokenAVault.deposit(from: <-vaultA)
128 self.tokenBVault.deposit(from: <-vaultB)
129
130 let isFeeOn = self.mintFee(reserveALast: reserveALast, reserveBLast: reserveBLast)
131
132 // note that totalSupply can update in mintFee
133 let totalSupply = PierMath.UFix64ToRawUInt256(PierLPToken.getTotalSupply(tokenId: self.poolId)!)
134 var liquidity = 0 as UInt256
135 if totalSupply == 0 {
136 // first liquidity for this pool
137 // liquidity = sqrt(amountA * amountB) - MINIMUM_LIQUIDITY
138 liquidity = PierMath.sqrt(PierMath.UFix64ToRawUInt256(amountA) * PierMath.UFix64ToRawUInt256(amountB))
139 - PierMath.UFix64ToRawUInt256(PierPair.MINIMUM_LIQUIDITY)
140
141 // permanently lock the first MINIMUM_LIQUIDITY tokens
142 let minimumLP <- self.lpTokenMaster.mintTokens(amount: PierPair.MINIMUM_LIQUIDITY)
143 destroy minimumLP
144 } else {
145 // liquidityA = amountA * totalSupply / reserveALast
146 let liquidityA = PierMath.UFix64ToRawUInt256(amountA) * totalSupply
147 / PierMath.UFix64ToRawUInt256(reserveALast)
148
149 // liquidityB = amountB * totalSupply / reserveBLast
150 let liquidityB = PierMath.UFix64ToRawUInt256(amountB) * totalSupply
151 / PierMath.UFix64ToRawUInt256(reserveBLast)
152
153 // liquidity = min(liquidityA, liquidityB)
154 liquidity = liquidityA > liquidityB ? liquidityB : liquidityA
155 }
156
157 assert(liquidity > 0, message: "Metapier PierPair: Cannot mint zero liquidity")
158 let lpTokenVault <-self.lpTokenMaster.mintTokens(amount: PierMath.rawUInt256ToUFix64(liquidity))
159
160 if isFeeOn {
161 // kLast = balanceA * balanceB
162 self.kLast = PierMath.UFix64ToRawUInt256(self.tokenAVault.balance)
163 * PierMath.UFix64ToRawUInt256(self.tokenBVault.balance)
164 }
165
166 self.makeObservation(reserveA: reserveALast, reserveB: reserveBLast)
167
168 emit Mint(poolId: self.poolId, amountAIn: amountA, amountBIn: amountB)
169
170 self.lock = false
171 return <-lpTokenVault
172 }
173
174 pub fun burn(lpTokenVault: @PierLPToken.Vault): @[FungibleToken.Vault; 2] {
175 pre {
176 !self.lock: "Metapier PierPair: Reentrant call"
177 }
178 post {
179 !self.lock: "Metapier PierPair: Lock not released"
180 }
181 self.lock = true
182
183 let reserveALast = self.tokenAVault.balance
184 let reserveBLast = self.tokenBVault.balance
185 let liquidity = lpTokenVault.balance
186 let balanceA = self.tokenAVault.balance
187 let balanceB = self.tokenBVault.balance
188
189 let isFeeOn = self.mintFee(reserveALast: reserveALast, reserveBLast: reserveBLast)
190
191 // note that totalSupply can update in mintFee
192 let totalSupply = PierMath.UFix64ToRawUInt256(PierLPToken.getTotalSupply(tokenId: self.poolId)!)
193 let liquidityUInt256 = PierMath.UFix64ToRawUInt256(liquidity)
194
195 // amountA = liquidity * balanceA / totalSupply
196 let amountA = liquidityUInt256 * PierMath.UFix64ToRawUInt256(balanceA) / totalSupply
197 // amountB = liquidity * balanceB / totalSupply
198 let amountB = liquidityUInt256 * PierMath.UFix64ToRawUInt256(balanceB) / totalSupply
199
200 assert(
201 amountA > 0 && amountB > 0,
202 message: "Metapier PierPair: Insufficient liquidity to burn"
203 )
204
205 // burn LP tokens
206 self.lpTokenMaster.burnTokens(vault: <-lpTokenVault)
207
208 let outputTokens: @[FungibleToken.Vault; 2] <-[
209 <-self.tokenAVault.withdraw(amount: PierMath.rawUInt256ToUFix64(amountA)),
210 <-self.tokenBVault.withdraw(amount: PierMath.rawUInt256ToUFix64(amountB))
211 ]
212
213 if isFeeOn {
214 // kLast = balanceA * balanceB
215 self.kLast = PierMath.UFix64ToRawUInt256(self.tokenAVault.balance)
216 * PierMath.UFix64ToRawUInt256(self.tokenBVault.balance)
217 }
218
219 self.makeObservation(reserveA: reserveALast, reserveB: reserveBLast)
220
221 emit Burn(
222 poolId: self.poolId,
223 amountLP: liquidity,
224 amountAOut: outputTokens[0].balance,
225 amountBOut: outputTokens[1].balance
226 )
227
228 self.lock = false
229 return <-outputTokens
230 }
231
232 // Updates the cumulative price information if this function
233 // is called for the first time in the current block.
234 access(self) fun makeObservation(reserveA: UFix64, reserveB: UFix64) {
235 let curTimestamp = getCurrentBlock().timestamp
236 let timeElapsed = curTimestamp - self.lastBlockTimestamp
237
238 if timeElapsed > 0.0 && reserveA != 0.0 && reserveB != 0.0 {
239 self.lastBlockTimestamp = curTimestamp
240 self.lastPriceACumulative = PierMath.computePriceCumulative(
241 lastPrice1Cumulative: self.lastPriceACumulative,
242 reserve1: reserveA,
243 reserve2: reserveB,
244 timeElapsed: timeElapsed
245 )
246 self.lastPriceBCumulative = PierMath.computePriceCumulative(
247 lastPrice1Cumulative: self.lastPriceBCumulative,
248 reserve1: reserveB,
249 reserve2: reserveA,
250 timeElapsed: timeElapsed
251 )
252 }
253 }
254
255 // Mints new LP tokens as protocol fees.
256 access(self) fun mintFee(reserveALast: UFix64, reserveBLast: UFix64): Bool {
257 let isFeeOn = PierSwapSettings.poolProtocolFee > 0.0
258 if isFeeOn {
259 if (self.kLast > 0) {
260 // rootK = sqrt(reserveALast * reserveBLast)
261 let rootK = PierMath.sqrt(PierMath.UFix64ToRawUInt256(reserveALast)
262 * PierMath.UFix64ToRawUInt256(reserveBLast))
263
264 // rootKLast = sqrt(kLast)
265 let rootKLast = PierMath.sqrt(self.kLast)
266
267 let totalSupply = PierMath.UFix64ToRawUInt256(PierLPToken.getTotalSupply(tokenId: self.poolId)!)
268 if (rootK > rootKLast) {
269 // numerator = totalSupply * (rootK - rootKLast)
270 let numerator = totalSupply * (rootK - rootKLast)
271
272 // denominator = rootK * ProtocolFeeCoefficient + rootKLast
273 let denominator = rootK * UInt256(PierSwapSettings.getPoolProtocolFeeCoefficient()) + rootKLast
274
275 // liquidity = numerator / denominator
276 let liquidity = PierMath.rawUInt256ToUFix64(numerator / denominator)
277 if (liquidity > 0.0) {
278 let protocolFee <-self.lpTokenMaster.mintTokens(amount: liquidity)
279 PierSwapSettings.depositProtocolFee(vault: <-protocolFee)
280 }
281 }
282 }
283 } else if (self.kLast > 0) {
284 self.kLast = 0
285 }
286
287 return isFeeOn
288 }
289
290 init(
291 vaultA: @FungibleToken.Vault,
292 vaultB: @FungibleToken.Vault,
293 lpTokenMaster: @PierLPToken.TokenMaster,
294 poolId: UInt64
295 ) {
296 pre {
297 vaultA.balance == 0.0: "MetaPier PierPair: Pool creation requires empty vaults"
298 vaultB.balance == 0.0: "MetaPier PierPair: Pool creation requires empty vaults"
299 !vaultA.isInstance(vaultB.getType()) && !vaultB.isInstance(vaultA.getType()):
300 "MetaPier PierPair: Pool creation requires vaults of different types"
301 }
302 self.poolId = poolId
303 self.kLast = 0
304
305 self.tokenAVault <- vaultA
306 self.tokenBVault <- vaultB
307 self.tokenAType = self.tokenAVault.getType()
308 self.tokenBType = self.tokenBVault.getType()
309
310 self.lastBlockTimestamp = getCurrentBlock().timestamp
311 self.lastPriceACumulative = 0
312 self.lastPriceBCumulative = 0
313
314 self.lpTokenMaster <- lpTokenMaster
315
316 self.lock = false
317 }
318
319 destroy() {
320 destroy self.tokenAVault
321 destroy self.tokenBVault
322 destroy self.lpTokenMaster
323 }
324 }
325
326 // Creates a new pool resource.
327 // This function is only accessible to code in the same account.
328 access(account) fun createPool(
329 vaultA: @FungibleToken.Vault,
330 vaultB: @FungibleToken.Vault,
331 poolId: UInt64
332 ): @Pool {
333 return <-create PierPair.Pool(
334 vaultA: <-vaultA,
335 vaultB: <-vaultB,
336 lpTokenMaster: <-self.lpTokenAdmin.initNewLPToken(tokenId: poolId),
337 poolId: poolId
338 )
339 }
340
341 init() {
342 self.MINIMUM_LIQUIDITY = 0.00001
343
344 // requires PierLPToken to be deployed to the same account
345 self.lpTokenAdmin <-self.account.load<@PierLPToken.Admin>(from: /storage/metapierLPTokenAdmin)
346 ?? panic("Metapier PierPair: Cannot load LP token admin")
347
348 emit ContractInitialized()
349 }
350}
351