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