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

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