Smart Contract

PierPair

A.609e10301860b683.PierPair

Deployed

1w ago
Mar 06, 2026, 04:27:13 PM UTC

Dependents

0 imports
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