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}