pool.gno
19.23 Kb · 600 lines
1package pool
2
3import (
4 "chain"
5 "errors"
6
7 "gno.land/r/gnoswap/common"
8 "gno.land/r/gnoswap/halt/v1"
9 pl "gno.land/r/gnoswap/pool"
10
11 "gno.land/p/gnoswap/gnsmath/v1"
12 i256 "gno.land/p/gnoswap/int256/v1"
13 u256 "gno.land/p/gnoswap/uint256/v1"
14 "gno.land/p/gnoswap/utils/v1"
15 ufmt "gno.land/p/nt/ufmt/v0"
16
17 prabc "gno.land/p/gnoswap/rbac/v1"
18 _ "gno.land/r/gnoswap/rbac/v1"
19
20 "gno.land/r/gnoswap/access/v1"
21)
22
23// Mint adds liquidity to a pool position.
24//
25// Increases liquidity for a position within specified tick range.
26// Calculates required token amounts based on current pool price.
27// Updates tick state and transfers tokens atomically.
28//
29// Parameters:
30// - _: Noncrossing implementation-call discriminator; pass 0.
31// - rlm: Current realm context forwarded unchanged by the pool proxy.
32// - token0Path: Registered token contract path for token0.
33// - token1Path: Registered token contract path for token1.
34// - fee: Fee tier; supported tiers are 100, 500, 3000, and 10000 (0.01%, 0.05%, 0.3%, and 1%).
35// - tickLower: Inclusive lower price-range tick; must be aligned to the pool tick spacing.
36// - tickUpper: Exclusive upper price-range tick; must be aligned to the pool tick spacing.
37// - liquidityAmount: Positive decimal string specifying liquidity to add; it is converted to uint256 and then int128.
38// - positionCaller: Nonzero position-contract address that supplies tokens for this mint.
39//
40// Returns:
41// - amount0: Token0 amount consumed, returned as a decimal string.
42// - amount1: Token1 amount consumed, returned as a decimal string.
43//
44// Requirements:
45// - Pool must exist for token pair and fee
46// - Liquidity amount must be positive
47// - Ticks must be valid and aligned to spacing
48//
49// Only callable by position contract.
50func (i *poolV1) Mint(
51 _ int,
52 rlm realm,
53 token0Path string,
54 token1Path string,
55 fee uint32,
56 tickLower int32,
57 tickUpper int32,
58 liquidityAmount string,
59 positionCaller address,
60) (string, string) {
61 access.AssertIsRlmCurrent(0, rlm)
62
63 i.assertPoolUnlocked()
64 halt.AssertIsNotHaltedPool()
65
66 caller := rlm.Previous().Address()
67 access.AssertIsPosition(caller)
68 access.AssertIsValidAddress(positionCaller)
69
70 i.lockPool(0, rlm)
71 defer i.unlockPool(0, rlm)
72
73 liquidity := u256.MustFromDecimal(liquidityAmount)
74 if liquidity.IsZero() {
75 panic(errors.New(errZeroLiquidity))
76 }
77
78 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
79
80 tickSpacing := pool.TickSpacing()
81 checkTickSpacing(tickLower, tickSpacing)
82 checkTickSpacing(tickUpper, tickSpacing)
83
84 liquidityDelta := gnsmath.SafeConvertToInt128(liquidity)
85 positionParam := newModifyPositionParams(positionCaller, tickLower, tickUpper, liquidityDelta)
86 observations := i.mustGetObservations(pool.PoolPath())
87 _, amount0, amount1, err := modifyPosition(pool, observations, positionParam)
88 if err != nil {
89 panic(err)
90 }
91
92 poolAddr := access.MustGetAddress(prabc.ROLE_POOL.String())
93
94 if amount0.Gt(u256.Zero()) {
95 i.safeTransferFrom(0, rlm, pool, positionCaller, poolAddr, pool.Token0Path(), amount0, true)
96 }
97
98 if amount1.Gt(u256.Zero()) {
99 i.safeTransferFrom(0, rlm, pool, positionCaller, poolAddr, pool.Token1Path(), amount1, false)
100 }
101
102 // Save pool state after modifyPosition may have updated liquidity
103 err = i.savePool(0, rlm, pool)
104 if err != nil {
105 panic(err)
106 }
107
108 return amount0.ToString(), amount1.ToString()
109}
110
111// Burn removes liquidity from a pool position and credits principal as tokens
112// owed to that pool-level position entry.
113//
114// The pool operation itself does not transfer tokens; a subsequent Collect call
115// pays the credited principal. Position.DecreaseLiquidity wraps Burn and the
116// fee-free Collect call within one atomic public operation.
117//
118// Parameters:
119// - _: Noncrossing implementation-call discriminator; pass 0.
120// - rlm: Current realm context forwarded unchanged by the pool proxy.
121// - token0Path: Registered token contract path for token0.
122// - token1Path: Registered token contract path for token1.
123// - fee: Fee tier identifying the pool.
124// - tickLower: Lower tick of the position's price range; must match pool spacing.
125// - tickUpper: Upper tick of the position's price range; must match pool spacing.
126// - liquidityAmount: Non-negative decimal liquidity amount, at most 2^127-1 after conversion to int128.
127// - positionCaller: Nonzero position-contract address associated with the pool position.
128//
129// Returns:
130// - amount0: Token0 principal credited to the pool position, as a decimal string.
131// - amount1: Token1 principal credited to the pool position, as a decimal string.
132//
133// Note: Tokens remain in pool until Collect is called.
134// Only callable by position contract.
135func (i *poolV1) Burn(
136 _ int,
137 rlm realm,
138 token0Path string,
139 token1Path string,
140 fee uint32,
141 tickLower int32,
142 tickUpper int32,
143 liquidityAmount string, // parsed as uint256, then checked against int128
144 positionCaller address,
145) (string, string) {
146 access.AssertIsRlmCurrent(0, rlm)
147
148 i.assertPoolUnlocked()
149 halt.AssertIsNotHaltedWithdraw()
150
151 caller := rlm.Previous().Address()
152 access.AssertIsPosition(caller)
153 access.AssertIsValidAddress(positionCaller)
154
155 i.lockPool(0, rlm)
156 defer i.unlockPool(0, rlm)
157
158 liqAmount := u256.MustFromDecimal(liquidityAmount)
159 liqAmountInt256 := gnsmath.SafeConvertToInt128(liqAmount)
160 liqDelta := i256.Zero().Neg(liqAmountInt256)
161
162 posParams := newModifyPositionParams(positionCaller, tickLower, tickUpper, liqDelta)
163 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
164 observations := i.mustGetObservations(pool.PoolPath())
165 position, amount0, amount1, err := modifyPosition(pool, observations, posParams)
166 if err != nil {
167 panic(err)
168 }
169
170 if amount0.Gt(u256.Zero()) || amount1.Gt(u256.Zero()) {
171 amount0 = toUint128(amount0)
172 amount1 = toUint128(amount1)
173
174 position.SetTokensOwed0(gnsmath.SafeAddInt64(position.TokensOwed0(), gnsmath.SafeConvertToInt64(amount0)))
175 position.SetTokensOwed1(gnsmath.SafeAddInt64(position.TokensOwed1(), gnsmath.SafeConvertToInt64(amount1)))
176 }
177
178 positionKey := getPositionKey(tickLower, tickUpper)
179
180 setPosition(pool, positionKey, position)
181
182 err = i.savePool(0, rlm, pool)
183 if err != nil {
184 panic(err)
185 }
186
187 // actual token transfer happens in Collect()
188 return amount0.ToString(), amount1.ToString()
189}
190
191// CollectSwapFee pays out accrued swap fees for a position.
192//
193// The withdrawal fee is deducted from the collected amount and settled to the
194// protocol fee realm; the remainder is transferred to recipient. Only accrued
195// swap fees belong on this path - principal returned by Burn is not fee
196// bearing and must go through Collect.
197//
198// Parameters:
199// - _: Noncrossing implementation-call discriminator; pass 0.
200// - rlm: Current realm context forwarded unchanged by the pool proxy.
201// - token0Path: Registered token contract path for token0.
202// - token1Path: Registered token contract path for token1.
203// - fee: Fee tier identifying the pool.
204// - recipient: Nonzero address receiving the post-fee token amounts.
205// - tickLower: Lower tick of the position's price range.
206// - tickUpper: Upper tick of the position's price range.
207// - amount0Requested: Non-negative decimal int64 amount of token0 requested; the int64 maximum requests all owed token0.
208// - amount1Requested: Non-negative decimal int64 amount of token1 requested; the int64 maximum requests all owed token1.
209//
210// Returns:
211// - amount0: Token0 amount collected before withdrawal-fee deduction, as a decimal string.
212// - amount1: Token1 amount collected before withdrawal-fee deduction, as a decimal string.
213// - fee0: Withdrawal fee withheld from token0, as a decimal string.
214// - fee1: Withdrawal fee withheld from token1, as a decimal string.
215//
216// Only callable by position contract.
217func (i *poolV1) CollectSwapFee(
218 _ int,
219 rlm realm,
220 token0Path string,
221 token1Path string,
222 fee uint32,
223 recipient address,
224 tickLower int32,
225 tickUpper int32,
226 amount0Requested string,
227 amount1Requested string,
228) (amount0, amount1, fee0, fee1 string) {
229 collected0, collected1, withheld0, withheld1 := i.collect(
230 0, rlm,
231 token0Path, token1Path, fee,
232 recipient,
233 tickLower, tickUpper,
234 amount0Requested, amount1Requested,
235 true,
236 )
237
238 return utils.FormatInt(collected0), utils.FormatInt(collected1),
239 utils.FormatInt(withheld0), utils.FormatInt(withheld1)
240}
241
242// Collect pays out tokens owed to a position, transferring the full amount to
243// recipient.
244//
245// No withdrawal fee is charged: this path carries principal credited by Burn,
246// which the protocol does not tax. Accrued swap fees belong on CollectSwapFee.
247//
248// Parameters:
249// - _: Noncrossing implementation-call discriminator; pass 0.
250// - rlm: Current realm context forwarded unchanged by the pool proxy.
251// - token0Path: Registered token contract path for token0.
252// - token1Path: Registered token contract path for token1.
253// - fee: Fee tier identifying the pool.
254// - recipient: Nonzero address receiving the owed token amounts.
255// - tickLower: Lower tick of the position's price range.
256// - tickUpper: Upper tick of the position's price range.
257// - amount0Requested: Non-negative decimal int64 amount of token0 requested; the int64 maximum requests all owed token0.
258// - amount1Requested: Non-negative decimal int64 amount of token1 requested; the int64 maximum requests all owed token1.
259//
260// Returns:
261// - amount0: Token0 principal transferred to recipient, as a decimal string.
262// - amount1: Token1 principal transferred to recipient, as a decimal string.
263//
264// Only callable by position contract.
265func (i *poolV1) Collect(
266 _ int,
267 rlm realm,
268 token0Path string,
269 token1Path string,
270 fee uint32,
271 recipient address,
272 tickLower int32,
273 tickUpper int32,
274 amount0Requested string,
275 amount1Requested string,
276) (amount0, amount1 string) {
277 collected0, collected1, _, _ := i.collect(
278 0, rlm,
279 token0Path, token1Path, fee,
280 recipient,
281 tickLower, tickUpper,
282 amount0Requested, amount1Requested,
283 false,
284 )
285
286 return utils.FormatInt(collected0), utils.FormatInt(collected1)
287}
288
289// collect settles the pool ledger for a position and pays the recipient.
290//
291// The payout is capped by the position's tokensOwed, never by a caller
292// supplied number, and the ledger is written before any token leaves the
293// realm. Both happen under a single pool lock, so the pool's accounting and
294// its token balance are never observably out of step.
295//
296// The collected amount is NOT capped by the pool's internal balance: a balance
297// short of the owed amount means the internal ledger has drifted, so the call
298// reverts rather than silently paying out less.
299//
300// applyWithdrawalFee selects the fee policy; pass false for a fee-free payout.
301// It is the entry point, not the configured rate, that decides whether the
302// protocol fee is settled: a rate of zero must still flush a backlog left
303// pending by an earlier halted settlement.
304func (i *poolV1) collect(
305 _ int,
306 rlm realm,
307 token0Path string,
308 token1Path string,
309 fee uint32,
310 recipient address,
311 tickLower int32,
312 tickUpper int32,
313 amount0Requested string,
314 amount1Requested string,
315 applyWithdrawalFee bool,
316) (amount0, amount1, fee0, fee1 int64) {
317 access.AssertIsRlmCurrent(0, rlm)
318
319 i.assertPoolUnlocked()
320 halt.AssertIsNotHaltedWithdraw()
321
322 caller := rlm.Previous().Address()
323 access.AssertIsPosition(caller)
324 access.AssertIsValidAddress(recipient)
325
326 i.lockPool(0, rlm)
327 defer i.unlockPool(0, rlm)
328
329 amount0Req := utils.SafeParseInt64(amount0Requested)
330 amount1Req := utils.SafeParseInt64(amount1Requested)
331
332 if amount0Req < 0 || amount1Req < 0 {
333 panic(errors.New(errInvalidInput))
334 }
335
336 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
337 // The pool position key encodes only the lower and upper ticks. It is scoped
338 // to this pool, so NFTs sharing a range use the same aggregate pool entry.
339 positionKey := getPositionKey(tickLower, tickUpper)
340 position, err := pool.GetPosition(positionKey)
341 if err != nil {
342 panic(newErrorWithDetail(
343 errDataNotFound,
344 ufmt.Sprintf("positionKey(%s) does not exist", positionKey),
345 ))
346 }
347
348 amount0 = minRequestedAmount(amount0Req, position.TokensOwed0())
349 amount1 = minRequestedAmount(amount1Req, position.TokensOwed1())
350
351 if amount0 > 0 {
352 tokenOwed0 := gnsmath.SafeSubInt64(position.TokensOwed0(), amount0)
353 token0Balance, err := updatePoolBalance(pool.BalanceToken0(), pool.BalanceToken1(), amount0, true)
354 if err != nil {
355 panic(err)
356 }
357
358 position.SetTokensOwed0(tokenOwed0)
359 pool.SetBalanceToken0(token0Balance)
360 }
361 if amount1 > 0 {
362 tokenOwed1 := gnsmath.SafeSubInt64(position.TokensOwed1(), amount1)
363 token1Balance, err := updatePoolBalance(pool.BalanceToken0(), pool.BalanceToken1(), amount1, false)
364 if err != nil {
365 panic(err)
366 }
367
368 position.SetTokensOwed1(tokenOwed1)
369 pool.SetBalanceToken1(token1Balance)
370 }
371
372 setPosition(pool, positionKey, position)
373
374 if err := i.savePool(0, rlm, pool); err != nil {
375 panic(err)
376 }
377
378 withdrawalFeeBPS := ZeroBps
379 if applyWithdrawalFee {
380 withdrawalFeeBPS = i.store.GetWithdrawalFeeBPS()
381 }
382
383 // Effects are persisted above; everything below leaves the realm.
384 fee0, amount0AfterFee := deductWithdrawalFee(amount0, withdrawalFeeBPS)
385 fee1, amount1AfterFee := deductWithdrawalFee(amount1, withdrawalFeeBPS)
386
387 if applyWithdrawalFee {
388 // Called even for a zero fee, and for a zero withdrawal fee rate, so that
389 // anything left pending for these tokens by an earlier halted settlement
390 // is flushed.
391 i.settleProtocolFee(0, rlm, token0Path, fee0)
392 i.settleProtocolFee(0, rlm, token1Path, fee1)
393 }
394
395 if amount0AfterFee > 0 {
396 common.SafeGRC20Transfer(0, rlm, token0Path, recipient, amount0AfterFee)
397 }
398 if amount1AfterFee > 0 {
399 common.SafeGRC20Transfer(0, rlm, token1Path, recipient, amount1AfterFee)
400 }
401
402 return amount0, amount1, fee0, fee1
403}
404
405// CollectProtocol collects accumulated protocol fees from swap operations.
406// Only callable by admin or governance.
407//
408// Parameters:
409// - _: Noncrossing implementation-call discriminator; pass 0.
410// - rlm: Current realm context forwarded unchanged by the pool proxy.
411// - token0Path: Registered token contract path for token0.
412// - token1Path: Registered token contract path for token1.
413// - fee: Fee tier identifying the pool.
414// - recipient: Nonzero address receiving the collected protocol fees.
415// - amount0Requested: Non-negative decimal amount of token0 protocol fees requested; capped at the available balance.
416// - amount1Requested: Non-negative decimal amount of token1 protocol fees requested; capped at the available balance.
417//
418// Returns:
419// - amount0: Token0 protocol fees transferred to recipient, as a decimal string.
420// - amount1: Token1 protocol fees transferred to recipient, as a decimal string.
421func (i *poolV1) CollectProtocol(
422 _ int,
423 rlm realm,
424 token0Path string,
425 token1Path string,
426 fee uint32,
427 recipient address,
428 amount0Requested string, // uint128
429 amount1Requested string, // uint128
430) (string, string) {
431 access.AssertIsRlmCurrent(0, rlm)
432
433 i.assertPoolUnlocked()
434 halt.AssertIsNotHaltedWithdraw()
435
436 previousRealm := rlm.Previous()
437 caller := previousRealm.Address()
438 access.AssertIsAdminOrGovernance(caller)
439
440 common.MustRegistered(token0Path, token1Path)
441
442 i.lockPool(0, rlm)
443 defer i.unlockPool(0, rlm)
444
445 amount0, amount1 := i.collectProtocol(
446 0,
447 rlm,
448 token0Path,
449 token1Path,
450 fee,
451 recipient,
452 amount0Requested,
453 amount1Requested,
454 )
455
456 chain.Emit(
457 "CollectProtocol",
458 "prevAddr", caller.String(),
459 "prevRealm", previousRealm.PkgPath(),
460 "token0Path", token0Path,
461 "token1Path", token1Path,
462 "fee", utils.FormatUint(fee),
463 "recipient", recipient.String(),
464 "internal_amount0", amount0,
465 "internal_amount1", amount1,
466 )
467
468 return amount0, amount1
469}
470
471// collectProtocol performs the actual protocol fee collection.
472// It ensures requested amounts don't exceed available protocol fees.
473// Returns amount0, amount1 as strings representing collected fees.
474func (i *poolV1) collectProtocol(
475 _ int,
476 rlm realm,
477 token0Path string,
478 token1Path string,
479 fee uint32,
480 recipient address,
481 amount0Requested string,
482 amount1Requested string,
483) (string, string) {
484 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
485
486 amount0Req := utils.SafeParseInt64(amount0Requested)
487 amount1Req := utils.SafeParseInt64(amount1Requested)
488
489 if amount0Req < 0 || amount1Req < 0 {
490 panic(errors.New(errInvalidInput))
491 }
492
493 amount0 := minRequestedAmount(amount0Req, pool.ProtocolFeesToken0())
494 amount1 := minRequestedAmount(amount1Req, pool.ProtocolFeesToken1())
495
496 amount0, amount1 = i.saveProtocolFees(pool, amount0, amount1)
497
498 newBalanceToken0, err := updatePoolBalance(pool.BalanceToken0(), pool.BalanceToken1(), amount0, true)
499 if err != nil {
500 panic(err)
501 }
502 pool.SetBalanceToken0(newBalanceToken0)
503
504 newBalanceToken1, err := updatePoolBalance(pool.BalanceToken0(), pool.BalanceToken1(), amount1, false)
505 if err != nil {
506 panic(err)
507 }
508 pool.SetBalanceToken1(newBalanceToken1)
509
510 err = i.savePool(0, rlm, pool)
511 if err != nil {
512 panic(err)
513 }
514
515 common.SafeGRC20Transfer(0, rlm, pool.Token0Path(), recipient, amount0)
516 common.SafeGRC20Transfer(0, rlm, pool.Token1Path(), recipient, amount1)
517
518 return utils.FormatInt(amount0), utils.FormatInt(amount1)
519}
520
521// saveProtocolFees updates the protocol fee balances after collection.
522// Returns amount0, amount1 representing the fees deducted from protocol reserves.
523func (i *poolV1) saveProtocolFees(pool *pl.Pool, amount0, amount1 int64) (int64, int64) {
524 if pool.ProtocolFeesToken0() < amount0 {
525 panic(errors.New(errUnderflow))
526 }
527 pool.SetProtocolFeesToken0(gnsmath.SafeSubInt64(pool.ProtocolFeesToken0(), amount0))
528
529 if pool.ProtocolFeesToken1() < amount1 {
530 panic(errors.New(errUnderflow))
531 }
532 pool.SetProtocolFeesToken1(gnsmath.SafeSubInt64(pool.ProtocolFeesToken1(), amount1))
533
534 return amount0, amount1
535}
536
537func minRequestedAmount(request, available int64) int64 {
538 if request > available {
539 return available
540 }
541
542 return request
543}
544
545// IncreaseObservationCardinalityNext schedules growth of a pool's circular observation buffer.
546//
547// Parameters:
548// - _: Noncrossing implementation-call discriminator; pass 0.
549// - rlm: Current realm context forwarded unchanged by the pool proxy.
550// - token0Path: Registered token contract path for token0.
551// - token1Path: Registered token contract path for token1.
552// - fee: Fee tier identifying the pool.
553// - cardinalityNext: Requested observation-buffer capacity; must not exceed the configured maximum.
554func (i *poolV1) IncreaseObservationCardinalityNext(
555 _ int,
556 rlm realm,
557 token0Path string,
558 token1Path string,
559 fee uint32,
560 cardinalityNext uint16,
561) {
562 access.AssertIsRlmCurrent(0, rlm)
563
564 i.assertPoolUnlocked()
565 halt.AssertIsNotHaltedPool()
566
567 pool := i.mustGetPoolBy(token0Path, token1Path, fee)
568 slot0Before := pool.Slot0()
569 observationCardinalityNextOld := slot0Before.ObservationCardinalityNext()
570
571 i.lockPool(0, rlm)
572 defer i.unlockPool(0, rlm)
573
574 observations := i.mustGetObservations(pool.PoolPath())
575 if cardinalityNext > maxObservationCardinality {
576 panic("observation cardinality next exceeds maximum")
577 }
578 observationCardinalityNextNew, err := grow(
579 observations,
580 slot0Before.ObservationCardinalityNext(),
581 cardinalityNext,
582 )
583 if err != nil {
584 panic(err)
585 }
586 slot0After := pool.Slot0()
587 slot0After.SetObservationCardinalityNext(observationCardinalityNextNew)
588 pool.SetSlot0(slot0After)
589
590 if observationCardinalityNextOld != observationCardinalityNextNew {
591 previousRealm := rlm.Previous()
592 chain.Emit(
593 "IncreaseObservationCardinalityNext",
594 "prevAddr", previousRealm.Address().String(),
595 "prevRealm", previousRealm.PkgPath(),
596 "poolPath", pool.PoolPath(),
597 "cardinalityNext", utils.FormatUint(observationCardinalityNextNew),
598 )
599 }
600}