Search Apps Documentation Source Content File Folder Download Copy Actions Download

position.gno

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