Search Apps Documentation Source Content File Folder Download Copy Actions Download

position.gno

19.84 Kb ยท 616 lines
  1package v1
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"encoding/base64"
  7	"strconv"
  8
  9	prabc "gno.land/p/gnoswap/rbac"
 10	u256 "gno.land/p/gnoswap/uint256"
 11	"gno.land/p/nt/ufmt"
 12
 13	"gno.land/r/gnoswap/access"
 14	"gno.land/r/gnoswap/common"
 15	"gno.land/r/gnoswap/emission"
 16	"gno.land/r/gnoswap/halt"
 17	pl "gno.land/r/gnoswap/pool"
 18	"gno.land/r/gnoswap/position"
 19	pf "gno.land/r/gnoswap/protocol_fee"
 20	"gno.land/r/gnoswap/referral"
 21)
 22
 23// Mint creates a new liquidity position NFT.
 24//
 25// Parameters:
 26//   - token0, token1: token contract paths
 27//   - fee: pool fee tier
 28//   - tickLower, tickUpper: price range boundaries
 29//   - amount0Desired, amount1Desired: desired token amounts
 30//   - amount0Min, amount1Min: minimum acceptable amounts
 31//   - deadline: transaction deadline
 32//   - mintTo: NFT recipient address
 33//   - caller: transaction initiator
 34//   - referrer: referral address
 35//
 36// Returns tokenId, liquidity, amount0, amount1.
 37// Only callable by users or staker contract.
 38// Note: Slippage protection via amount0Min/amount1Min.
 39func (p *positionV1) Mint(
 40	token0 string,
 41	token1 string,
 42	fee uint32,
 43	tickLower int32,
 44	tickUpper int32,
 45	amount0Desired string,
 46	amount1Desired string,
 47	amount0Min string,
 48	amount1Min string,
 49	deadline int64,
 50	mintTo address,
 51	caller address,
 52	referrer string,
 53) (uint64, string, string, string) {
 54	halt.AssertIsNotHaltedPosition()
 55
 56	previousRealm := runtime.PreviousRealm()
 57	prevRealmAddr := previousRealm.Address()
 58	if !previousRealm.IsUser() {
 59		access.AssertIsStaker(prevRealmAddr)
 60	} else {
 61		assertEqualsAddress(prevRealmAddr, mintTo)
 62		assertEqualsAddress(prevRealmAddr, caller)
 63	}
 64
 65	assertValidNumberString(amount0Desired)
 66	assertValidNumberString(amount1Desired)
 67	assertValidNumberString(amount0Min)
 68	assertValidNumberString(amount1Min)
 69
 70	// assert that the user has sent the correct amount of native coin
 71	assertIsValidUserCoinSendWithTokenPair(token0, token1, amount0Desired, amount1Desired)
 72	assertIsNotExpired(deadline)
 73
 74	success := referral.TryRegister(cross, caller, referrer)
 75	actualReferrer := referrer
 76	if !success {
 77		actualReferrer = referral.GetReferral(caller.String())
 78	}
 79
 80	emission.MintAndDistributeGns(cross)
 81
 82	mintInput := MintInput{
 83		token0:         token0,
 84		token1:         token1,
 85		fee:            fee,
 86		tickLower:      tickLower,
 87		tickUpper:      tickUpper,
 88		amount0Desired: amount0Desired,
 89		amount1Desired: amount1Desired,
 90		amount0Min:     amount0Min,
 91		amount1Min:     amount1Min,
 92		deadline:       deadline,
 93		mintTo:         mintTo,
 94		caller:         caller,
 95	}
 96	processedInput, err := p.processMintInput(mintInput)
 97	if err != nil {
 98		panic(newErrorWithDetail(errInvalidInput, err.Error()))
 99	}
100
101	// mint liquidity
102	params := newMintParams(processedInput, mintInput)
103	id, liquidity, amount0, amount1 := p.mint(params)
104
105	// refund leftover wrapped tokens
106	if processedInput.tokenPair.token0IsNative && processedInput.tokenPair.wrappedAmount > safeConvertToInt64(amount0) {
107		err = p.unwrap(processedInput.tokenPair.wrappedAmount-safeConvertToInt64(amount0), caller)
108		if err != nil {
109			panic(newErrorWithDetail(errWrapUnwrap, err.Error()))
110		}
111	}
112
113	if processedInput.tokenPair.token1IsNative && processedInput.tokenPair.wrappedAmount > safeConvertToInt64(amount1) {
114		err = p.unwrap(processedInput.tokenPair.wrappedAmount-safeConvertToInt64(amount1), caller)
115		if err != nil {
116			panic(newErrorWithDetail(errWrapUnwrap, err.Error()))
117		}
118	}
119
120	poolSqrtPriceX96 := pl.GetSlot0SqrtPriceX96(processedInput.poolPath)
121
122	tickCumulative, liquidityCumulative, secondsPerLiquidityCumulativeX128, observationTimestamp :=
123		pl.GetObservation(processedInput.poolPath, 0)
124
125	chain.Emit(
126		"Mint",
127		"prevAddr", prevRealmAddr.String(),
128		"prevRealm", previousRealm.PkgPath(),
129		"tickLower", formatInt(processedInput.tickLower),
130		"tickUpper", formatInt(processedInput.tickUpper),
131		"poolPath", processedInput.poolPath,
132		"mintTo", mintTo.String(),
133		"caller", caller.String(),
134		"lpPositionId", formatUint(id),
135		"liquidityDelta", liquidity.ToString(),
136		"amount0", amount0.ToString(),
137		"amount1", amount1.ToString(),
138		"sqrtPriceX96", poolSqrtPriceX96.ToString(),
139		"positionLiquidity", p.GetPositionLiquidity(id).ToString(),
140		"poolLiquidity", pl.GetLiquidity(processedInput.poolPath),
141		"token0Balance", pl.GetBalanceToken0(processedInput.poolPath),
142		"token1Balance", pl.GetBalanceToken1(processedInput.poolPath),
143		"tickCumulative", formatInt(tickCumulative),
144		"liquidityCumulative", liquidityCumulative,
145		"secondsPerLiquidityCumulativeX128", secondsPerLiquidityCumulativeX128,
146		"observationTimestamp", formatInt(observationTimestamp),
147		"referrer", actualReferrer,
148	)
149
150	return id, liquidity.ToString(), amount0.ToString(), amount1.ToString()
151}
152
153// IncreaseLiquidity increases liquidity of an existing position.
154//
155// Adds more liquidity to existing NFT position.
156// Maintains same price range as original position.
157// Calculates optimal token ratio for current price.
158//
159// Parameters:
160//   - positionId: NFT token ID to increase
161//   - amount0DesiredStr: Desired token0 amount
162//   - amount1DesiredStr: Desired token1 amount
163//   - amount0MinStr: Minimum token0 (slippage protection)
164//   - amount1MinStr: Minimum token1 (slippage protection)
165//   - deadline: Transaction expiration timestamp
166//
167// Returns:
168//   - positionId: Same NFT ID
169//   - liquidity: Liquidity amount added (the delta, not total)
170//   - amount0: Token0 actually deposited
171//   - amount1: Token1 actually deposited
172//   - poolPath: Pool identifier
173//
174// Requirements:
175//   - Caller must own the position NFT
176//   - Sufficient token balances and approvals
177func (p *positionV1) IncreaseLiquidity(
178	positionId uint64,
179	amount0DesiredStr string,
180	amount1DesiredStr string,
181	amount0MinStr string,
182	amount1MinStr string,
183	deadline int64,
184) (uint64, string, string, string, string) {
185	halt.AssertIsNotHaltedPosition()
186
187	caller := runtime.PreviousRealm().Address()
188	assertIsOwnerForToken(p, positionId, caller)
189
190	assertValidNumberString(amount0DesiredStr)
191	assertValidNumberString(amount1DesiredStr)
192	assertValidNumberString(amount0MinStr)
193	assertValidNumberString(amount1MinStr)
194	assertIsNotExpired(deadline)
195
196	emission.MintAndDistributeGns(cross)
197
198	position := p.mustGetPosition(positionId)
199	token0, token1, _ := splitOf(position.PoolKey())
200
201	// assert that the user has sent the correct amount of native coin
202	assertIsValidUserCoinSendWithWrappedTokenPair(token0, token1, amount0DesiredStr, amount1DesiredStr)
203
204	receivedCoins := common.ExistsUserSendCoins()
205
206	err := validateTokenPath(token0, token1)
207	if err != nil {
208		panic(newErrorWithDetail(err, ufmt.Sprintf("token0(%s), token1(%s)", token0, token1)))
209	}
210
211	wrappedAmount := int64(0)
212
213	if receivedCoins {
214		inputWrappedAmount := int64(0)
215
216		if common.IsGNOTPath(token0) {
217			inputWrappedAmount = mustParseInt64(amount0DesiredStr)
218		} else if common.IsGNOTPath(token1) {
219			inputWrappedAmount = mustParseInt64(amount1DesiredStr)
220		}
221
222		wrappedAmount, err = p.safeWrapNativeToken(inputWrappedAmount, caller)
223		if err != nil {
224			panic(err)
225		}
226	}
227
228	amount0Desired, amount1Desired, amount0Min, amount1Min := parseAmounts(amount0DesiredStr, amount1DesiredStr, amount0MinStr, amount1MinStr)
229	increaseLiquidityParams := IncreaseLiquidityParams{
230		positionId:     positionId,
231		amount0Desired: amount0Desired,
232		amount1Desired: amount1Desired,
233		amount0Min:     amount0Min,
234		amount1Min:     amount1Min,
235		deadline:       deadline,
236		caller:         caller,
237	}
238
239	_, liquidity, amount0, amount1, poolPath, err := p.increaseLiquidity(increaseLiquidityParams)
240	if err != nil {
241		panic(err)
242	}
243
244	if err := p.unwrapLeftoverWrappedToken(token0, wrappedAmount, safeConvertToInt64(amount0), caller); err != nil {
245		panic(err)
246	}
247	if err := p.unwrapLeftoverWrappedToken(token1, wrappedAmount, safeConvertToInt64(amount1), caller); err != nil {
248		panic(err)
249	}
250
251	tickCumulative, liquidityCumulative, secondsPerLiquidityCumulativeX128, observationTimestamp :=
252		pl.GetObservation(poolPath, 0)
253
254	previousRealm := runtime.PreviousRealm()
255	chain.Emit(
256		"IncreaseLiquidity",
257		"prevAddr", previousRealm.Address().String(),
258		"prevRealm", previousRealm.PkgPath(),
259		"poolPath", poolPath,
260		"caller", caller.String(),
261		"lpPositionId", formatUint(positionId),
262		"liquidityDelta", liquidity.ToString(),
263		"amount0", amount0.ToString(),
264		"amount1", amount1.ToString(),
265		"sqrtPriceX96", pl.GetSlot0SqrtPriceX96(poolPath).ToString(),
266		"positionLiquidity", p.GetPositionLiquidity(positionId).ToString(),
267		"poolLiquidity", pl.GetLiquidity(poolPath),
268		"token0Balance", pl.GetBalanceToken0(poolPath),
269		"token1Balance", pl.GetBalanceToken1(poolPath),
270		"tickCumulative", formatInt(tickCumulative),
271		"liquidityCumulative", liquidityCumulative,
272		"secondsPerLiquidityCumulativeX128", secondsPerLiquidityCumulativeX128,
273		"observationTimestamp", formatInt(observationTimestamp),
274	)
275
276	return positionId, liquidity.ToString(), amount0.ToString(), amount1.ToString(), poolPath
277}
278
279// unwrapLeftoverWrappedToken unwraps leftover wrapped tokens to native tokens.
280func (p *positionV1) unwrapLeftoverWrappedToken(token string, wrapped, amount int64, caller address) error {
281	unwrappable := isWrappedToken(token) && wrapped > amount
282	if !unwrappable {
283		return nil
284	}
285
286	err := p.unwrap(wrapped-amount, caller)
287	if err != nil {
288		return makeErrorWithDetails(errWrapUnwrap, err.Error())
289	}
290
291	return nil
292}
293
294// DecreaseLiquidity decreases liquidity of an existing position.
295//
296// Removes liquidity but keeps NFT ownership.
297// Calculates tokens owed based on current price.
298// Two-step: decrease then collect tokens.
299//
300// Parameters:
301//   - positionId: NFT token ID
302//   - liquidityStr: Amount of liquidity to remove
303//   - amount0MinStr: Min token0 to receive (slippage)
304//   - amount1MinStr: Min token1 to receive (slippage)
305//   - deadline: Transaction expiration
306//   - unwrapResult: Convert WUGNOT to GNOT if true
307//
308// Returns:
309//   - positionId: Same NFT ID
310//   - liquidity: Amount of liquidity removed (the delta)
311//   - fee0, fee1: Fees collected
312//   - amount0, amount1: Principal collected
313//   - poolPath: Pool identifier
314//
315// Note: Applies withdrawal fee on collected amounts.
316func (p *positionV1) DecreaseLiquidity(
317	positionId uint64,
318	liquidityStr string,
319	amount0MinStr string,
320	amount1MinStr string,
321	deadline int64,
322	unwrapResult bool,
323) (uint64, string, string, string, string, string, string) {
324	halt.AssertIsNotHaltedPosition()
325	halt.AssertIsNotHaltedWithdraw()
326
327	caller := runtime.PreviousRealm().Address()
328	assertIsOwnerForToken(p, positionId, caller)
329	assertIsNotExpired(deadline)
330	assertValidLiquidityAmount(liquidityStr)
331
332	emission.MintAndDistributeGns(cross)
333
334	amount0Min := u256.MustFromDecimal(amount0MinStr)
335	amount1Min := u256.MustFromDecimal(amount1MinStr)
336	decreaseLiquidityParams := DecreaseLiquidityParams{
337		positionId:   positionId,
338		liquidity:    liquidityStr,
339		amount0Min:   amount0Min,
340		amount1Min:   amount1Min,
341		deadline:     deadline,
342		unwrapResult: unwrapResult,
343		caller:       caller,
344	}
345
346	positionId, liquidity, fee0, fee1, amount0, amount1, poolPath, err := p.decreaseLiquidity(decreaseLiquidityParams)
347	if err != nil {
348		panic(err)
349	}
350
351	tickCumulative, liquidityCumulative, secondsPerLiquidityCumulativeX128, observationTimestamp :=
352		pl.GetObservation(poolPath, 0)
353
354	previousRealm := runtime.PreviousRealm()
355	chain.Emit(
356		"DecreaseLiquidity",
357		"prevAddr", previousRealm.Address().String(),
358		"prevRealm", previousRealm.PkgPath(),
359		"lpPositionId", formatUint(positionId),
360		"poolPath", poolPath,
361		"liquidityDelta", liquidity,
362		"feeAmount0", fee0,
363		"feeAmount1", fee1,
364		"amount0", amount0,
365		"amount1", amount1,
366		"unwrapResult", formatBool(unwrapResult),
367		"sqrtPriceX96", pl.GetSlot0SqrtPriceX96(poolPath).ToString(),
368		"positionLiquidity", p.GetPositionLiquidity(positionId).ToString(),
369		"poolLiquidity", pl.GetLiquidity(poolPath),
370		"token0Balance", pl.GetBalanceToken0(poolPath),
371		"token1Balance", pl.GetBalanceToken1(poolPath),
372		"tickCumulative", formatInt(tickCumulative),
373		"liquidityCumulative", liquidityCumulative,
374		"secondsPerLiquidityCumulativeX128", secondsPerLiquidityCumulativeX128,
375		"observationTimestamp", formatInt(observationTimestamp),
376	)
377
378	return positionId, liquidity, fee0, fee1, amount0, amount1, poolPath
379}
380
381// CollectFee collects swap fee from the position.
382//
383// Claims accumulated fees without removing liquidity.
384// Useful for active positions earning ongoing fees.
385// Applies protocol withdrawal fee.
386//
387// Parameters:
388//   - positionId: NFT token ID
389//   - unwrapResult: Convert WUGNOT to GNOT if true
390//
391// Returns:
392//   - positionId: Same NFT ID
393//   - tokensCollected0: Token0 amount sent to caller (after withdrawal fee)
394//   - tokensCollected1: Token1 amount sent to caller (after withdrawal fee)
395//   - poolPath: Pool identifier
396//   - totalAmount0: Raw token0 collected (before withdrawal fee)
397//   - totalAmount1: Raw token1 collected (before withdrawal fee)
398//
399// Requirements:
400//   - Caller must be owner or approved operator
401//   - Position must have accumulated fees
402func (p *positionV1) CollectFee(
403	positionId uint64,
404	unwrapResult bool,
405) (uint64, string, string, string, string, string) {
406	halt.AssertIsNotHaltedPosition()
407	halt.AssertIsNotHaltedWithdraw()
408
409	caller := runtime.PreviousRealm().Address()
410	assertIsOwnerOrOperatorForToken(p, positionId, caller)
411
412	emission.MintAndDistributeGns(cross)
413
414	return p.collectFee(positionId, unwrapResult, caller)
415}
416
417// collectFee performs fee collection and withdrawal fee calculation.
418func (p *positionV1) collectFee(
419	positionId uint64,
420	unwrapResult bool,
421	caller address,
422) (uint64, string, string, string, string, string) {
423	// verify position
424	position := p.mustGetPosition(positionId)
425	token0, token1, fee := splitOf(position.PoolKey())
426
427	pl.Burn(
428		cross,
429		token0,
430		token1,
431		fee,
432		position.TickLower(),
433		position.TickUpper(),
434		"0", // burn '0' liquidity to collect fee
435		caller,
436	)
437
438	currentFeeGrowth, err := p.getCurrentFeeGrowth(position, caller)
439	if err != nil {
440		panic(newErrorWithDetail(err, "failed to get current fee growth"))
441	}
442
443	tokensOwed0, tokensOwed1 := p.calculateFees(position, currentFeeGrowth)
444
445	position.SetFeeGrowthInside0LastX128(u256.Zero().Set(currentFeeGrowth.feeGrowthInside0LastX128))
446	position.SetFeeGrowthInside1LastX128(u256.Zero().Set(currentFeeGrowth.feeGrowthInside1LastX128))
447
448	// collect fee
449	amount0, amount1 := pl.Collect(
450		cross,
451		token0, token1, fee,
452		caller,
453		position.TickLower(), position.TickUpper(),
454		tokensOwed0.ToString(), tokensOwed1.ToString(),
455	)
456	amount0Uint256 := u256.MustFromDecimal(amount0)
457	amount1Uint256 := u256.MustFromDecimal(amount1)
458
459	// 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
460	// instead of the actual amount so we can burn the token
461	tokensOwed0Updated, underflow := u256.Zero().SubOverflow(tokensOwed0, amount0Uint256)
462	if underflow {
463		panic(newErrorWithDetail(errUnderflow, "tokensOwed0 - amount0 underflow"))
464	}
465	position.SetTokensOwed0(tokensOwed0Updated)
466
467	tokensOwed1Updated, underflow := u256.Zero().SubOverflow(tokensOwed1, amount1Uint256)
468	if underflow {
469		panic(newErrorWithDetail(errUnderflow, "tokensOwed1 - amount1 underflow"))
470	}
471	position.SetTokensOwed1(tokensOwed1Updated)
472	p.mustUpdatePosition(positionId, *position)
473
474	withdrawalFeeBps := pl.GetWithdrawalFee()
475	amount0WithoutFee, fee0 := calculateAmountWithWithdrawalFee(amount0Uint256, withdrawalFeeBps)
476	amount1WithoutFee, fee1 := calculateAmountWithWithdrawalFee(amount1Uint256, withdrawalFeeBps)
477
478	poolAddr := access.MustGetAddress(prabc.ROLE_POOL.String())
479
480	if isWrappedToken(token0) && unwrapResult {
481		p.unwrapWithTransferFrom(poolAddr, caller, amount0WithoutFee)
482	} else {
483		common.SafeGRC20TransferFrom(cross, token0, poolAddr, caller, amount0WithoutFee)
484	}
485
486	if isWrappedToken(token1) && unwrapResult {
487		p.unwrapWithTransferFrom(poolAddr, caller, amount1WithoutFee)
488	} else {
489		common.SafeGRC20TransferFrom(cross, token1, poolAddr, caller, amount1WithoutFee)
490	}
491
492	protocolFeeAddr := access.MustGetAddress(prabc.ROLE_PROTOCOL_FEE.String())
493
494	// handle withdrawal protocol fee
495	pf.AddToProtocolFee(cross, token0, fee0)
496	common.SafeGRC20TransferFrom(cross, token0, poolAddr, protocolFeeAddr, fee0)
497
498	pf.AddToProtocolFee(cross, token1, fee1)
499	common.SafeGRC20TransferFrom(cross, token1, poolAddr, protocolFeeAddr, fee1)
500
501	amount0WithoutFeeStr := formatInt(amount0WithoutFee)
502	amount1WithoutFeeStr := formatInt(amount1WithoutFee)
503
504	previousRealm := runtime.PreviousRealm()
505	chain.Emit(
506		"CollectSwapFee",
507		"prevAddr", previousRealm.Address().String(),
508		"prevRealm", previousRealm.PkgPath(),
509		"lpPositionId", formatUint(positionId),
510		"feeAmount0", amount0WithoutFeeStr,
511		"feeAmount1", amount1WithoutFeeStr,
512		"poolPath", position.PoolKey(),
513		"unwrapResult", formatBool(unwrapResult),
514		"feeGrowthInside0LastX128", position.FeeGrowthInside0LastX128().ToString(),
515		"feeGrowthInside1LastX128", position.FeeGrowthInside1LastX128().ToString(),
516	)
517
518	chain.Emit(
519		"WithdrawalFee",
520		"prevAddr", previousRealm.Address().String(),
521		"prevRealm", previousRealm.PkgPath(),
522		"lpTokenId", formatUint(positionId),
523		"poolPath", position.PoolKey(),
524		"feeAmount0", formatInt(fee0),
525		"feeAmount1", formatInt(fee1),
526		"amount0WithoutFee", amount0WithoutFeeStr,
527		"amount1WithoutFee", amount1WithoutFeeStr,
528	)
529
530	return positionId, amount0WithoutFeeStr, amount1WithoutFeeStr, position.PoolKey(), amount0, amount1
531}
532
533// calculateAmountWithWithdrawalFee calculates amount after deducting withdrawal fee.
534func calculateAmountWithWithdrawalFee(amount *u256.Uint, fee uint64) (int64, int64) {
535	if fee == 0 {
536		return safeConvertToInt64(amount), 0
537	}
538
539	feeAmount, overflow := u256.Zero().MulOverflow(amount, u256.NewUint(fee))
540	if overflow {
541		panic(errOverflow)
542	}
543	feeAmount = u256.Zero().Div(feeAmount, u256.NewUint(10000))
544	amountWithoutFee := u256.Zero().Sub(amount, feeAmount)
545
546	return safeConvertToInt64(amountWithoutFee), safeConvertToInt64(feeAmount)
547}
548
549// SetPositionOperator sets an operator for a position.
550// Only staker can call this function.
551func (p *positionV1) SetPositionOperator(
552	id uint64,
553	operator address,
554) {
555	halt.AssertIsNotHaltedPosition()
556
557	access.AssertIsStaker(runtime.PreviousRealm().Address())
558
559	assertValidOperatorAddress(operator)
560
561	position := p.mustGetPosition(id)
562	prevOperator := position.Operator()
563	position.SetOperator(operator)
564
565	p.mustUpdatePosition(id, *position)
566
567	previousRealm := runtime.PreviousRealm()
568	chain.Emit(
569		"SetPositionOperator",
570		"prevAddr", previousRealm.Address().String(),
571		"prevRealm", previousRealm.PkgPath(),
572		"lpPositionId", formatUint(id),
573		"prevOperator", prevOperator.String(),
574		"newOperator", operator.String(),
575	)
576}
577
578// getCurrentFeeGrowth retrieves current fee growth values for a position.
579func (p *positionV1) getCurrentFeeGrowth(position *position.Position, owner address) (FeeGrowthInside, error) {
580	positionKey := computePositionKey(position.TickLower(), position.TickUpper())
581	feeGrowthInside0LastX128, feeGrowthInside1LastX128 := pl.GetPositionFeeGrowthInsideLastX128(position.PoolKey(), positionKey)
582
583	feeGrowthInside := FeeGrowthInside{
584		feeGrowthInside0LastX128: feeGrowthInside0LastX128,
585		feeGrowthInside1LastX128: feeGrowthInside1LastX128,
586	}
587
588	return feeGrowthInside, nil
589}
590
591// computePositionKey generates a unique base64-encoded key for a liquidity position.
592func computePositionKey(
593	tickLower int32,
594	tickUpper int32,
595) string {
596	currentRealmPath := runtime.CurrentRealm().PkgPath()
597	key := currentRealmPath + "__" + strconv.Itoa(int(tickLower)) + "__" + strconv.Itoa(int(tickUpper))
598	encoded := base64.StdEncoding.EncodeToString([]byte(key))
599	return encoded
600}
601
602// calculatePositionBalances computes token balances for a position at current price.
603// Returns calculated token0 and token1 balances based on position liquidity and price range.
604func calculatePositionBalances(position *position.Position) (*u256.Uint, *u256.Uint) {
605	liquidity := position.Liquidity()
606	if liquidity.IsZero() {
607		return u256.Zero(), u256.Zero()
608	}
609
610	return common.GetAmountsForLiquidity(
611		pl.GetSlot0SqrtPriceX96(position.PoolKey()), // currentSqrtPriceX96
612		common.TickMathGetSqrtRatioAtTick(position.TickLower()),
613		common.TickMathGetSqrtRatioAtTick(position.TickUpper()),
614		liquidity,
615	)
616}