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}