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

position.gno

19.17 Kb · 555 lines
  1package position
  2
  3import (
  4	"chain"
  5
  6	"gno.land/p/gnoswap/gnsmath/v1"
  7	u256 "gno.land/p/gnoswap/uint256/v1"
  8	"gno.land/p/gnoswap/utils/v1"
  9	ufmt "gno.land/p/nt/ufmt/v0"
 10	"gno.land/r/gnoswap/access/v1"
 11	"gno.land/r/gnoswap/common"
 12	"gno.land/r/gnoswap/emission"
 13	"gno.land/r/gnoswap/halt/v1"
 14	pl "gno.land/r/gnoswap/pool"
 15	pos "gno.land/r/gnoswap/position"
 16	"gno.land/r/gnoswap/referral/v1"
 17	"gno.land/r/gnoswap/staker"
 18)
 19
 20// Mint creates a new liquidity position NFT.
 21//
 22// Parameters:
 23//   - _: Noncrossing implementation-call discriminator; pass 0.
 24//   - rlm: Current realm context forwarded unchanged by the position proxy.
 25//   - token0: token0 contract path for the pool
 26//   - token1: token1 contract path for the pool
 27//   - fee: pool fee tier used to select the pool
 28//   - tickLower: lower tick boundary of the position's range
 29//   - tickUpper: upper tick boundary of the position's range
 30//   - amount0Desired: desired token0 amount, supplied as a decimal string
 31//   - amount1Desired: desired token1 amount, supplied as a decimal string
 32//   - amount0Min: minimum acceptable token0 amount for slippage protection
 33//   - amount1Min: minimum acceptable token1 amount for slippage protection
 34//   - deadline: Unix timestamp after which the transaction is rejected
 35//   - mintTo: address that receives the newly minted position NFT
 36//   - referrer: referral value submitted for registration with the caller
 37//
 38// Returns:
 39//   - positionId: newly minted position NFT ID
 40//   - liquidity: liquidity amount minted for the position
 41//   - amount0: token0 amount actually deposited into the pool
 42//   - amount1: token1 amount actually deposited into the pool
 43//
 44// Note: Slippage protection via amount0Min/amount1Min.
 45func (p *positionV1) Mint(
 46	_ int,
 47	rlm realm,
 48	token0 string,
 49	token1 string,
 50	fee uint32,
 51	tickLower int32,
 52	tickUpper int32,
 53	amount0Desired string,
 54	amount1Desired string,
 55	amount0Min string,
 56	amount1Min string,
 57	deadline int64,
 58	mintTo address,
 59	referrer string,
 60) (uint64, string, string, string) {
 61	access.AssertIsRlmCurrent(0, rlm)
 62
 63	halt.AssertIsNotHaltedPosition()
 64	access.AssertIsValidAddress(mintTo)
 65
 66	previousRealm := rlm.Previous()
 67	caller := previousRealm.Address()
 68
 69	assertIsNotMintToStaker(mintTo)
 70	assertValidNumberString(amount0Desired)
 71	assertValidNumberString(amount1Desired)
 72	assertValidNumberString(amount0Min)
 73	assertValidNumberString(amount1Min)
 74
 75	// assert that the user has sent the correct amount of native coin
 76	common.AssertIsNotHandleNativeCoin()
 77	assertIsNotExpired(deadline)
 78
 79	actualReferrer := referral.TryRegister(cross(rlm), caller, referrer)
 80
 81	emission.MintAndDistributeGns(cross(rlm))
 82
 83	mintInput := MintInput{
 84		token0:         token0,
 85		token1:         token1,
 86		fee:            fee,
 87		tickLower:      tickLower,
 88		tickUpper:      tickUpper,
 89		amount0Desired: amount0Desired,
 90		amount1Desired: amount1Desired,
 91		amount0Min:     amount0Min,
 92		amount1Min:     amount1Min,
 93		deadline:       deadline,
 94		mintTo:         mintTo,
 95		caller:         caller,
 96	}
 97
 98	processedInput, err := p.processMintInput(mintInput)
 99	if err != nil {
100		panic(newErrorWithDetail(errInvalidInput, err.Error()))
101	}
102
103	// mint liquidity
104	params := newMintParams(processedInput, mintInput)
105	id, liquidity, amount0, amount1 := p.mint(0, rlm, params)
106
107	pool := mustGetPool(processedInput.poolPath)
108	positionLiquidity, err := p.GetPositionLiquidity(id)
109	if err != nil {
110		panic(err)
111	}
112	tickCumulative, secondsPerLiquidityCumulativeX128, observationTimestamp, err := currentPoolObservation(processedInput.poolPath)
113	if err != nil {
114		panic(err)
115	}
116
117	chain.Emit(
118		"Mint",
119		"prevAddr", caller.String(),
120		"prevRealm", previousRealm.PkgPath(),
121		"tickLower", utils.FormatInt(processedInput.tickLower),
122		"tickUpper", utils.FormatInt(processedInput.tickUpper),
123		"poolPath", processedInput.poolPath,
124		"mintTo", mintTo.String(),
125		"caller", caller.String(),
126		"lpPositionId", utils.FormatUint(id),
127		"liquidityDelta", liquidity.ToString(),
128		"amount0", amount0.ToString(),
129		"amount1", amount1.ToString(),
130		"sqrtPriceX96", pool.Slot0SqrtPriceX96().ToString(),
131		"positionLiquidity", positionLiquidity,
132		"poolLiquidity", pool.Liquidity().ToString(),
133		"token0Balance", utils.FormatInt(pool.BalanceToken0()),
134		"token1Balance", utils.FormatInt(pool.BalanceToken1()),
135		"tickCumulative", utils.FormatInt(tickCumulative),
136		"secondsPerLiquidityCumulativeX128", secondsPerLiquidityCumulativeX128,
137		"observationTimestamp", utils.FormatInt(observationTimestamp),
138		"referrer", actualReferrer,
139	)
140
141	return id, liquidity.ToString(), amount0.ToString(), amount1.ToString()
142}
143
144// IncreaseLiquidity increases liquidity of an existing position.
145//
146// Adds more liquidity to existing NFT position.
147// Maintains same price range as original position.
148// Calculates optimal token ratio for current price.
149//
150// Parameters:
151//   - _: Noncrossing implementation-call discriminator; pass 0.
152//   - rlm: Current realm context forwarded unchanged by the position proxy.
153//   - positionId: NFT token ID to increase
154//   - amount0DesiredStr: desired token0 amount, supplied as a decimal string
155//   - amount1DesiredStr: desired token1 amount, supplied as a decimal string
156//   - amount0MinStr: minimum acceptable token0 amount for slippage protection
157//   - amount1MinStr: minimum acceptable token1 amount for slippage protection
158//   - deadline: Unix timestamp after which the transaction is rejected
159//
160// Returns:
161//   - positionId: same NFT ID
162//   - liquidity: liquidity amount added (the delta, not total)
163//   - amount0: token0 amount actually deposited
164//   - amount1: token1 amount actually deposited
165//   - poolPath: pool identifier for the position
166//
167// Requirements:
168//   - Caller must own the position NFT
169//   - Sufficient token balances and approvals
170func (p *positionV1) IncreaseLiquidity(
171	_ int,
172	rlm realm,
173	positionId uint64,
174	amount0DesiredStr string,
175	amount1DesiredStr string,
176	amount0MinStr string,
177	amount1MinStr string,
178	deadline int64,
179) (uint64, string, string, string, string) {
180	access.AssertIsRlmCurrent(0, rlm)
181
182	halt.AssertIsNotHaltedPosition()
183
184	previousRealm := rlm.Previous()
185	caller := previousRealm.Address()
186	assertIsOwnerForToken(p, positionId, caller)
187
188	assertValidNumberString(amount0DesiredStr)
189	assertValidNumberString(amount1DesiredStr)
190	assertValidNumberString(amount0MinStr)
191	assertValidNumberString(amount1MinStr)
192	assertIsNotExpired(deadline)
193
194	emission.MintAndDistributeGns(cross(rlm))
195
196	position := p.mustGetPosition(positionId)
197	token0, token1, _ := splitOf(position.PoolKey())
198
199	common.AssertIsNotHandleNativeCoin()
200
201	err := validateTokenPath(token0, token1)
202	if err != nil {
203		panic(newErrorWithDetail(err.Error(), ufmt.Sprintf("token0(%s), token1(%s)", token0, token1)))
204	}
205
206	amount0Desired, amount1Desired, amount0Min, amount1Min := parseAmounts(amount0DesiredStr, amount1DesiredStr, amount0MinStr, amount1MinStr)
207	increaseLiquidityParams := IncreaseLiquidityParams{
208		positionId:     positionId,
209		amount0Desired: amount0Desired,
210		amount1Desired: amount1Desired,
211		amount0Min:     amount0Min,
212		amount1Min:     amount1Min,
213		deadline:       deadline,
214		caller:         caller,
215	}
216
217	_, liquidity, amount0, amount1, poolPath, err := p.increaseLiquidity(0, rlm, increaseLiquidityParams)
218	if err != nil {
219		panic(err)
220	}
221
222	pool := mustGetPool(poolPath)
223	positionLiquidity, err := p.GetPositionLiquidity(positionId)
224	if err != nil {
225		panic(err)
226	}
227	tickCumulative, secondsPerLiquidityCumulativeX128, observationTimestamp, err := currentPoolObservation(poolPath)
228	if err != nil {
229		panic(err)
230	}
231	chain.Emit(
232		"IncreaseLiquidity",
233		"prevAddr", previousRealm.Address().String(),
234		"prevRealm", previousRealm.PkgPath(),
235		"poolPath", poolPath,
236		"tickLower", utils.FormatInt(position.TickLower()),
237		"tickUpper", utils.FormatInt(position.TickUpper()),
238		"caller", caller.String(),
239		"lpPositionId", utils.FormatUint(positionId),
240		"liquidityDelta", liquidity.ToString(),
241		"amount0", amount0.ToString(),
242		"amount1", amount1.ToString(),
243		"sqrtPriceX96", pool.Slot0SqrtPriceX96().ToString(),
244		"positionLiquidity", positionLiquidity,
245		"poolLiquidity", pool.Liquidity().ToString(),
246		"token0Balance", utils.FormatInt(pool.BalanceToken0()),
247		"token1Balance", utils.FormatInt(pool.BalanceToken1()),
248		"tickCumulative", utils.FormatInt(tickCumulative),
249		"secondsPerLiquidityCumulativeX128", secondsPerLiquidityCumulativeX128,
250		"observationTimestamp", utils.FormatInt(observationTimestamp),
251	)
252
253	return positionId, liquidity.ToString(), amount0.ToString(), amount1.ToString(), poolPath
254}
255
256// DecreaseLiquidity decreases liquidity of an existing position.
257//
258// Removes liquidity but keeps NFT ownership. This is one atomic public
259// operation: accrued swap fees are collected first, then liquidity is burned,
260// and principal is collected through the pool's fee-free `Collect` path.
261//
262// Parameters:
263//   - _: Noncrossing implementation-call discriminator; pass 0.
264//   - rlm: Current realm context forwarded unchanged by the position proxy.
265//   - positionId: NFT token ID
266//   - liquidityStr: amount of liquidity to remove, supplied as a decimal string
267//   - amount0MinStr: minimum token0 principal to receive, for slippage protection
268//   - amount1MinStr: minimum token1 principal to receive, for slippage protection
269//   - deadline: Unix timestamp after which the transaction is rejected
270//
271// Returns:
272//   - positionId: same NFT ID
273//   - liquidity: amount of liquidity removed (the delta)
274//   - fee0: token0 fee amount returned net of the withdrawal fee
275//   - fee1: token1 fee amount returned net of the withdrawal fee
276//   - amount0: token0 principal collected without a withdrawal fee
277//   - amount1: token1 principal collected without a withdrawal fee
278//   - poolPath: pool identifier
279//
280// Amount-minimum checks apply to the principal actually collected.
281func (p *positionV1) DecreaseLiquidity(
282	_ int,
283	rlm realm,
284	positionId uint64,
285	liquidityStr string,
286	amount0MinStr string,
287	amount1MinStr string,
288	deadline int64,
289) (uint64, string, string, string, string, string, string) {
290	access.AssertIsRlmCurrent(0, rlm)
291
292	halt.AssertIsNotHaltedWithdraw()
293
294	previousRealm := rlm.Previous()
295	caller := previousRealm.Address()
296	assertIsOwnerForToken(p, positionId, caller)
297	assertIsNotExpired(deadline)
298	assertValidLiquidityAmount(liquidityStr)
299
300	emission.MintAndDistributeGns(cross(rlm))
301
302	amount0Min := u256.MustFromDecimal(amount0MinStr)
303	amount1Min := u256.MustFromDecimal(amount1MinStr)
304	decreaseLiquidityParams := DecreaseLiquidityParams{
305		positionId: positionId,
306		liquidity:  liquidityStr,
307		amount0Min: amount0Min,
308		amount1Min: amount1Min,
309		deadline:   deadline,
310		caller:     caller,
311	}
312
313	position := p.mustGetPosition(positionId)
314	tickLower := position.TickLower()
315	tickUpper := position.TickUpper()
316
317	positionId, liquidity, fee0, fee1, amount0, amount1, poolPath, err := p.decreaseLiquidity(0, rlm, decreaseLiquidityParams)
318	if err != nil {
319		panic(err)
320	}
321
322	pool := mustGetPool(poolPath)
323	positionLiquidity, err := p.GetPositionLiquidity(positionId)
324	if err != nil {
325		panic(err)
326	}
327	tickCumulative, secondsPerLiquidityCumulativeX128, observationTimestamp, err := currentPoolObservation(poolPath)
328	if err != nil {
329		panic(err)
330	}
331	chain.Emit(
332		"DecreaseLiquidity",
333		"prevAddr", previousRealm.Address().String(),
334		"prevRealm", previousRealm.PkgPath(),
335		"lpPositionId", utils.FormatUint(positionId),
336		"poolPath", poolPath,
337		"tickLower", utils.FormatInt(tickLower),
338		"tickUpper", utils.FormatInt(tickUpper),
339		"liquidityDelta", liquidity,
340		"feeAmount0", fee0,
341		"feeAmount1", fee1,
342		"amount0", amount0,
343		"amount1", amount1,
344		"sqrtPriceX96", pool.Slot0SqrtPriceX96().ToString(),
345		"positionLiquidity", positionLiquidity,
346		"poolLiquidity", pool.Liquidity().ToString(),
347		"token0Balance", utils.FormatInt(pool.BalanceToken0()),
348		"token1Balance", utils.FormatInt(pool.BalanceToken1()),
349		"tickCumulative", utils.FormatInt(tickCumulative),
350		"secondsPerLiquidityCumulativeX128", secondsPerLiquidityCumulativeX128,
351		"observationTimestamp", utils.FormatInt(observationTimestamp),
352	)
353
354	return positionId, liquidity, fee0, fee1, amount0, amount1, poolPath
355}
356
357// CollectFee collects swap fee from the position.
358//
359// Claims accumulated fees without removing liquidity.
360// Useful for active positions earning ongoing fees.
361// Applies the configured withdrawal fee to the collected swap fees.
362//
363// Parameters:
364//   - _: Noncrossing implementation-call discriminator; pass 0.
365//   - rlm: Current realm context forwarded unchanged by the position proxy.
366//   - positionId: NFT token ID whose accrued swap fees are collected
367//
368// Returns:
369//   - positionId: same NFT ID
370//   - tokensCollected0: token0 amount sent to caller after the withdrawal fee
371//   - tokensCollected1: token1 amount sent to caller after the withdrawal fee
372//   - poolPath: pool identifier
373//   - totalAmount0: raw token0 amount collected before the withdrawal fee
374//   - totalAmount1: raw token1 amount collected before the withdrawal fee
375//
376// Requirements:
377//   - Caller must own an unstaked NFT; a staked position uses its configured operator
378//   - If no fees are owed, the fee amounts returned are zero
379func (p *positionV1) CollectFee(_ int, rlm realm, positionId uint64) (uint64, string, string, string, string, string) {
380	access.AssertIsRlmCurrent(0, rlm)
381
382	halt.AssertIsNotHaltedWithdraw()
383
384	caller := rlm.Previous().Address()
385	assertIsOwnerOrOperatorForToken(p, positionId, caller)
386
387	emission.MintAndDistributeGns(cross(rlm))
388
389	return p.collectFee(0, rlm, positionId, caller)
390}
391
392// collectFee performs fee collection and withdrawal fee calculation.
393func (p *positionV1) collectFee(_ int, rlm realm, positionId uint64, caller address) (uint64, string, string, string, string, string) {
394	// verify position
395	position := p.mustGetPosition(positionId)
396	token0, token1, fee := splitOf(position.PoolKey())
397
398	pl.Burn(
399		cross(rlm),
400		token0,
401		token1,
402		fee,
403		position.TickLower(),
404		position.TickUpper(),
405		"0", // burn '0' liquidity to collect fee
406		caller,
407	)
408
409	currentFeeGrowth, err := p.getCurrentFeeGrowth(position, caller)
410	if err != nil {
411		panic(newErrorWithDetail(err.Error(), "failed to get current fee growth"))
412	}
413
414	tokensOwed0, tokensOwed1 := p.calculateFees(position, currentFeeGrowth)
415
416	position.SetFeeGrowthInside0LastX128(currentFeeGrowth.feeGrowthInside0LastX128.ToString())
417	position.SetFeeGrowthInside1LastX128(currentFeeGrowth.feeGrowthInside1LastX128.ToString())
418
419	// Collect the fee. The pool withholds the withdrawal fee and pays the
420	// remainder straight to the caller.
421	amount0, amount1, fee0Str, fee1Str := pl.CollectSwapFee(
422		cross(rlm),
423		token0, token1, fee,
424		caller,
425		position.TickLower(), position.TickUpper(),
426		utils.FormatInt(tokensOwed0), utils.FormatInt(tokensOwed1),
427	)
428	amount0Uint256 := u256.MustFromDecimal(amount0)
429	amount1Uint256 := u256.MustFromDecimal(amount1)
430	amount0Int64 := gnsmath.SafeConvertToInt64(amount0Uint256)
431	amount1Int64 := gnsmath.SafeConvertToInt64(amount1Uint256)
432
433	// sometimes there will be a few less uBase amount than expected due to rounding down in core, but we just subtract the full amount expected
434	// instead of the actual amount so we can burn the token
435	if tokensOwed0 < amount0Int64 {
436		panic(newErrorWithDetail(errUnderflow, "tokensOwed0 - amount0 underflow"))
437	}
438	position.SetTokensOwed0(gnsmath.SafeSubInt64(tokensOwed0, amount0Int64))
439
440	if tokensOwed1 < amount1Int64 {
441		panic(newErrorWithDetail(errUnderflow, "tokensOwed1 - amount1 underflow"))
442	}
443	position.SetTokensOwed1(gnsmath.SafeSubInt64(tokensOwed1, amount1Int64))
444	p.mustUpdatePosition(0, rlm, positionId, *position)
445
446	// The pool already paid these out; recomputing them here only feeds the
447	// events and the return value.
448	amount0WithoutFeeStr := utils.FormatInt(gnsmath.SafeSubInt64(amount0Int64, utils.SafeParseInt64(fee0Str)))
449	amount1WithoutFeeStr := utils.FormatInt(gnsmath.SafeSubInt64(amount1Int64, utils.SafeParseInt64(fee1Str)))
450
451	poolPath := position.PoolKey()
452
453	previousRealm := rlm.Previous()
454	chain.Emit(
455		"CollectSwapFee",
456		"prevAddr", previousRealm.Address().String(),
457		"prevRealm", previousRealm.PkgPath(),
458		"lpPositionId", utils.FormatUint(positionId),
459		"feeAmount0", amount0WithoutFeeStr,
460		"feeAmount1", amount1WithoutFeeStr,
461		"poolPath", poolPath,
462		"poolTier", utils.FormatUint(staker.GetPoolTier(poolPath)),
463		"feeGrowthInside0LastX128", position.FeeGrowthInside0LastX128(),
464		"feeGrowthInside1LastX128", position.FeeGrowthInside1LastX128(),
465	)
466
467	chain.Emit(
468		"WithdrawalFee",
469		"prevAddr", previousRealm.Address().String(),
470		"prevRealm", previousRealm.PkgPath(),
471		"lpTokenId", utils.FormatUint(positionId),
472		"poolPath", poolPath,
473		"feeAmount0", fee0Str,
474		"feeAmount1", fee1Str,
475		"amount0WithoutFee", amount0WithoutFeeStr,
476		"amount1WithoutFee", amount1WithoutFeeStr,
477	)
478
479	return positionId, amount0WithoutFeeStr, amount1WithoutFeeStr, position.PoolKey(), amount0, amount1
480}
481
482// SetPositionOperator sets an operator for a position.
483// Only staker can call this function.
484//
485// Parameters:
486//   - _: Noncrossing implementation-call discriminator; pass 0.
487//   - rlm: Current realm context forwarded unchanged by the position proxy.
488//   - id: position NFT ID whose operator is changed
489//   - operator: valid address to approve, or the empty address to remove the operator
490func (p *positionV1) SetPositionOperator(_ int, rlm realm, id uint64, operator address) {
491	access.AssertIsRlmCurrent(0, rlm)
492
493	previousRealm := rlm.Previous()
494	access.AssertIsStaker(previousRealm.Address())
495
496	assertValidOperatorAddress(operator)
497
498	position := p.mustGetPosition(id)
499	prevOperator := position.Operator()
500	position.SetOperator(operator)
501
502	p.mustUpdatePosition(0, rlm, id, *position)
503
504	chain.Emit(
505		"SetPositionOperator",
506		"prevAddr", previousRealm.Address().String(),
507		"prevRealm", previousRealm.PkgPath(),
508		"lpPositionId", utils.FormatUint(id),
509		"prevOperator", prevOperator.String(),
510		"newOperator", operator.String(),
511	)
512}
513
514// getCurrentFeeGrowth retrieves current fee growth values for a position.
515func (p *positionV1) getCurrentFeeGrowth(position *pos.Position, owner address) (FeeGrowthInside, error) {
516	positionKey := computePositionKey(position.TickLower(), position.TickUpper())
517	feeGrowthInside0LastX128, feeGrowthInside1LastX128, err := pl.GetPositionFeeGrowthInsideLastX128(position.PoolKey(), positionKey)
518	if err != nil {
519		return FeeGrowthInside{}, err
520	}
521
522	feeGrowthInside := FeeGrowthInside{
523		feeGrowthInside0LastX128: u256.MustFromDecimal(feeGrowthInside0LastX128),
524		feeGrowthInside1LastX128: u256.MustFromDecimal(feeGrowthInside1LastX128),
525	}
526
527	return feeGrowthInside, nil
528}
529
530// computePositionKey generates a compact deterministic key for a liquidity position.
531func computePositionKey(tickLower, tickUpper int32) string {
532	return pl.EncodePositionKey(tickLower, tickUpper)
533}
534
535// calculatePositionBalances computes token balances for a position at current price.
536// Returns calculated token0 and token1 balances based on position liquidity and price range.
537func calculatePositionBalances(position *pos.Position) (int64, int64) {
538	liquidity := u256.MustFromDecimal(position.Liquidity())
539	if liquidity.IsZero() {
540		return 0, 0
541	}
542
543	sqrtPriceX96, err := pl.GetSlot0SqrtPriceX96(position.PoolKey())
544	if err != nil {
545		panic(err)
546	}
547	token0Balance, token1Balance := gnsmath.GetAmountsForLiquidity(
548		u256.MustFromDecimal(sqrtPriceX96), // currentSqrtPriceX96
549		gnsmath.TickMathGetSqrtRatioAtTick(position.TickLower()),
550		gnsmath.TickMathGetSqrtRatioAtTick(position.TickUpper()),
551		liquidity,
552	)
553
554	return gnsmath.SafeConvertToInt64(token0Balance), gnsmath.SafeConvertToInt64(token1Balance)
555}