Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}