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}