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}