Search Apps Documentation Source Content File Folder Download Copy Actions Download

position.gno

13.45 Kb ยท 402 lines
  1package v1
  2
  3import (
  4	"encoding/base64"
  5	"strconv"
  6	"time"
  7
  8	plp "gno.land/p/gnoswap/gnsmath"
  9	i256 "gno.land/p/gnoswap/int256"
 10	u256 "gno.land/p/gnoswap/uint256"
 11	"gno.land/p/nt/ufmt"
 12
 13	"gno.land/r/gnoswap/common"
 14	pl "gno.land/r/gnoswap/pool"
 15)
 16
 17const positionPackagePath = "gno.land/r/gnoswap/position"
 18
 19// getPositionKey generates a unique base64-encoded key for a liquidity position.
 20//
 21// Creates deterministic identifier for position tracking.
 22// Ensures unique positions per (position package path, price range) combination.
 23//
 24// Used internally for position state management.
 25//
 26// Parameters:
 27//   - tickLower: Lower boundary tick of position range
 28//   - tickUpper: Upper boundary tick of position range
 29//
 30// Key Format:
 31//
 32//	base64(position_package_path + "__" + tickLower + "__" + tickUpper)
 33//
 34// Returns base64-encoded position key or error.
 35// Combines position package path, tickLower, and tickUpper.
 36func getPositionKey(
 37	tickLower int32,
 38	tickUpper int32,
 39) string {
 40	positionKey := positionPackagePath + "__" + strconv.Itoa(int(tickLower)) + "__" + strconv.Itoa(int(tickUpper))
 41	encodedPositionKey := base64.StdEncoding.EncodeToString([]byte(positionKey))
 42	return encodedPositionKey
 43}
 44
 45// positionUpdate updates a position's liquidity and calculates fees owed.
 46// Returns the updated position information and any error.
 47func positionUpdate(
 48	position pl.PositionInfo,
 49	liquidityDelta *i256.Int,
 50	feeGrowthInside0X128 *u256.Uint,
 51	feeGrowthInside1X128 *u256.Uint,
 52) (pl.PositionInfo, error) {
 53	isZeroLiquidityDelta := liquidityDelta.IsZero()
 54
 55	if position.Liquidity().IsZero() && isZeroLiquidityDelta {
 56		return pl.NewDefaultPositionInfo(), makeErrorWithDetails(
 57			errZeroLiquidity,
 58			"both liquidityDelta and current position's liquidity are zero",
 59		)
 60	}
 61
 62	if liquidityDelta.IsNeg() {
 63		// absolute value of negative liquidity delta must be less than current liquidity
 64		absDelta := i256.Zero().Set(liquidityDelta).Abs()
 65		currentLiquidity := position.Liquidity()
 66		if absDelta.Gt(currentLiquidity) {
 67			return pl.NewDefaultPositionInfo(), makeErrorWithDetails(
 68				errZeroLiquidity,
 69				ufmt.Sprintf("liquidity delta(%s) is greater than current liquidity(%s)",
 70					liquidityDelta.ToString(), position.Liquidity().ToString()),
 71			)
 72		}
 73	}
 74
 75	var liquidityNext *u256.Uint
 76	if isZeroLiquidityDelta {
 77		liquidityNext = position.Liquidity()
 78	} else {
 79		liquidityNext = common.LiquidityMathAddDelta(position.Liquidity(), liquidityDelta)
 80	}
 81
 82	// validate negative feeGrowth before calculation
 83	diff0 := u256.Zero().Sub(feeGrowthInside0X128, position.FeeGrowthInside0LastX128())
 84	diff1 := u256.Zero().Sub(feeGrowthInside1X128, position.FeeGrowthInside1LastX128())
 85
 86	// calculate tokensOwed
 87	tokensOwed0 := u256.Zero()
 88	if !diff0.IsZero() {
 89		tokensOwed0 = u256.MulDiv(diff0, position.Liquidity(), q128FromDecimal)
 90	}
 91
 92	tokensOwed1 := u256.Zero()
 93	if !diff1.IsZero() {
 94		tokensOwed1 = u256.MulDiv(diff1, position.Liquidity(), q128FromDecimal)
 95	}
 96
 97	if !isZeroLiquidityDelta {
 98		position.SetLiquidity(liquidityNext)
 99	}
100
101	position.SetFeeGrowthInside0LastX128(feeGrowthInside0X128)
102	position.SetFeeGrowthInside1LastX128(feeGrowthInside1X128)
103
104	// add tokensOwed only when it's greater than 0
105	if tokensOwed0.Gt(zero) || tokensOwed1.Gt(zero) {
106		owed0, overflow := u256.Zero().AddOverflow(position.TokensOwed0(), tokensOwed0)
107		if overflow {
108			return pl.NewDefaultPositionInfo(), errOverflow
109		}
110		owed1, overflow := u256.Zero().AddOverflow(position.TokensOwed1(), tokensOwed1)
111		if overflow {
112			return pl.NewDefaultPositionInfo(), errOverflow
113		}
114
115		position.SetTokensOwed0(owed0)
116		position.SetTokensOwed1(owed1)
117	}
118
119	return position, nil
120}
121
122// calculateToken0Amount calculates the amount of token0 based on price range and liquidity delta.
123func calculateToken0Amount(sqrtPriceLower, sqrtPriceUpper *u256.Uint, liquidityDelta *i256.Int) *i256.Int {
124	return plp.GetAmount0Delta(sqrtPriceLower, sqrtPriceUpper, liquidityDelta)
125}
126
127// calculateToken1Amount calculates the amount of token1 based on price range and liquidity delta.
128func calculateToken1Amount(sqrtPriceLower, sqrtPriceUpper *u256.Uint, liquidityDelta *i256.Int) *i256.Int {
129	return plp.GetAmount1Delta(sqrtPriceLower, sqrtPriceUpper, liquidityDelta)
130}
131
132// PositionLiquidity returns the liquidity of a position.
133func PositionLiquidity(p *pl.Pool, key string) *u256.Uint {
134	return mustGetPositionByPool(p, key).Liquidity()
135}
136
137// PositionFeeGrowthInside0LastX128 returns the fee growth of token0 inside a position.
138func PositionFeeGrowthInside0LastX128(p *pl.Pool, key string) *u256.Uint {
139	return mustGetPositionByPool(p, key).FeeGrowthInside0LastX128()
140}
141
142// PositionFeeGrowthInside1LastX128 returns the fee growth of token1 inside a position.
143func PositionFeeGrowthInside1LastX128(p *pl.Pool, key string) *u256.Uint {
144	return mustGetPositionByPool(p, key).FeeGrowthInside1LastX128()
145}
146
147// PositionTokensOwed0 returns the amount of token0 owed by a position.
148func PositionTokensOwed0(p *pl.Pool, key string) *u256.Uint {
149	return mustGetPositionByPool(p, key).TokensOwed0()
150}
151
152// PositionTokensOwed1 returns the amount of token1 owed by a position.
153func PositionTokensOwed1(p *pl.Pool, key string) *u256.Uint {
154	return mustGetPositionByPool(p, key).TokensOwed1()
155}
156
157// GetPosition returns the position info for a given key.
158func GetPositionByPool(p *pl.Pool, key string) (pl.PositionInfo, bool) {
159	iPositionInfo, exist := p.Positions().Get(key)
160	if !exist {
161		newPosition := pl.NewDefaultPositionInfo()
162		return newPosition, false
163	}
164
165	positionInfo, ok := iPositionInfo.(pl.PositionInfo)
166	if !ok {
167		panic(ufmt.Sprintf("failed to cast iPositionInfo to PositionInfo: %T", iPositionInfo))
168	}
169
170	return positionInfo, true
171}
172
173// positionUpdateWithKey updates a position in the pool and returns the updated position.
174func positionUpdateWithKey(
175	p *pl.Pool,
176	positionKey string,
177	liquidityDelta *i256.Int,
178	feeGrowthInside0X128, feeGrowthInside1X128 *u256.Uint,
179) (pl.PositionInfo, error) {
180	// if position does not exist, create a new position
181	//
182	// Note: The positionUpdate function is designed to handle both new positions and existing positions,
183	// so there's no need to check for existence in GetPosition.
184	positionToUpdate, _ := GetPositionByPool(p, positionKey)
185	positionAfterUpdate, err := positionUpdate(positionToUpdate, liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128)
186	if err != nil {
187		return pl.NewDefaultPositionInfo(), err
188	}
189
190	setPosition(p, positionKey, positionAfterUpdate)
191
192	return positionAfterUpdate, nil
193}
194
195// setPosition sets the position info for a given key.
196func setPosition(p *pl.Pool, posKey string, positionInfo pl.PositionInfo) {
197	p.Positions().Set(posKey, positionInfo)
198}
199
200// mustGetPosition returns the position info for a given key.
201func mustGetPositionByPool(p *pl.Pool, positionKey string) *pl.PositionInfo {
202	positionInfo, exist := GetPositionByPool(p, positionKey)
203	if !exist {
204		panic(newErrorWithDetail(
205			errDataNotFound,
206			ufmt.Sprintf("positionKey(%s) does not exist", positionKey),
207		))
208	}
209
210	return &positionInfo
211}
212
213// modifyPosition updates a position in the pool and calculates the amount of tokens
214// needed (for minting) or returned (for burning). The calculation depends on the current
215// price (tick) relative to the position's price range.
216//
217// The function handles three cases:
218//  1. Current price below range (tick < tickLower): only token0 is used/returned
219//  2. Current price in range (tickLower <= tick < tickUpper): both tokens are used/returned
220//  3. Current price above range (tick >= tickUpper): only token1 is used/returned
221//
222// Parameters:
223//   - params: ModifyPositionParams containing owner, tickLower, tickUpper, and liquidityDelta
224//
225// Returns:
226//   - PositionInfo: updated position information
227//   - *u256.Uint: amount of token0 needed/returned
228//   - *u256.Uint: amount of token1 needed/returned
229func modifyPosition(p *pl.Pool, params ModifyPositionParams) (pl.PositionInfo, *u256.Uint, *u256.Uint, error) {
230	if err := validateTicks(params.tickLower, params.tickUpper); err != nil {
231		return pl.NewDefaultPositionInfo(), zero, zero, err
232	}
233
234	// get current state and price bounds
235	tick := p.Slot0Tick()
236	// update position state
237	position, err := updatePosition(p, params, tick)
238	if err != nil {
239		return pl.NewDefaultPositionInfo(), zero, zero, err
240	}
241
242	liqDelta := params.liquidityDelta
243	if liqDelta.IsZero() {
244		return position, zero, zero, nil
245	}
246
247	amount0, amount1 := i256.Zero(), i256.Zero()
248
249	// covert ticks to sqrt price to use in amount calculations
250	// price = 1.0001^tick, but we use sqrtPriceX96
251	sqrtRatioLower := common.TickMathGetSqrtRatioAtTick(params.tickLower)
252	sqrtRatioUpper := common.TickMathGetSqrtRatioAtTick(params.tickUpper)
253	sqrtPriceX96 := p.Slot0SqrtPriceX96()
254
255	// calculate token amounts based on current price position relative to range
256	switch {
257	case tick < params.tickLower:
258		// case 1
259		// full range between lower and upper tick is used for token0
260		// current tick is below the passed range; liquidity can only become in range by crossing from left to
261		// right, when we'll need _more_ token0 (it's becoming more valuable) so user must provide it
262		amount0 = calculateToken0Amount(sqrtRatioLower, sqrtRatioUpper, liqDelta)
263
264	case tick < params.tickUpper:
265		// case 2: Current price is within the position range
266		liquidityBefore := p.Liquidity()
267		currentTime := time.Now().Unix()
268
269		// Update oracle BEFORE liquidity changes
270		err := writeObservationByPool(p, currentTime, tick, liquidityBefore)
271		if err != nil {
272			return pl.NewDefaultPositionInfo(), zero, zero, err
273		}
274
275		// token0 used from current price to upper tick
276		amount0 = calculateToken0Amount(sqrtPriceX96, sqrtRatioUpper, liqDelta)
277		// token1 used from lower tick to current price
278		amount1 = calculateToken1Amount(sqrtRatioLower, sqrtPriceX96, liqDelta)
279		// update pool's active liquidity since price is in range
280		p.SetLiquidity(common.LiquidityMathAddDelta(liquidityBefore, liqDelta))
281
282	default:
283		// case 3
284		// full range between lower and upper tick is used for token1
285		// current tick is above the passed range; liquidity can only become in range by crossing from right to
286		// left, when we'll need _more_ token1 (it's becoming more valuable) so user must provide it
287		amount1 = calculateToken1Amount(sqrtRatioLower, sqrtRatioUpper, liqDelta)
288	}
289
290	return position, amount0.Abs(), amount1.Abs(), nil
291}
292
293// updatePosition modifies the position's liquidity and updates the corresponding tick states.
294//
295// This function updates the position data based on the specified liquidity delta and tick range.
296// It also manages the fee growth, tick state flipping, and cleanup of unused tick data.
297//
298// Parameters:
299//   - positionParams: ModifyPositionParams, the parameters for the position modification, which include:
300//   - owner: The address of the position owner.
301//   - tickLower: The lower tick boundary of the position.
302//   - tickUpper: The upper tick boundary of the position.
303//   - liquidityDelta: The change in liquidity (positive or negative).
304//   - tick: int32, the current tick position.
305//
306// Returns:
307//   - PositionInfo: The updated position information.
308//
309// Workflow:
310//  1. Clone the global fee growth values (token 0 and token 1).
311//  2. If the liquidity delta is non-zero:
312//     - Update the lower and upper ticks using `tickUpdate`, flipping their states if necessary.
313//     - If a tick's state was flipped, update the tick bitmap to reflect the new state.
314//  3. Calculate the fee growth inside the tick range using `getFeeGrowthInside`.
315//  4. Generate a unique position key and update the position data using `positionUpdateWithKey`.
316//  5. If liquidity is being removed (negative delta), clean up unused tick data by deleting the tick entries.
317//  6. Return the updated position.
318//
319// Notes:
320//   - The function flips the tick states and cleans up unused tick data when liquidity is removed.
321//   - It ensures fee growth and position data remain accurate after the update.
322//
323// Example Usage:
324//
325// ```gno
326//
327//	updatedPosition := pool.updatePosition(positionParams, currentTick)
328//	println("Updated Position Info:", updatedPosition)
329//
330// ```
331func updatePosition(p *pl.Pool, positionParams ModifyPositionParams, tick int32) (pl.PositionInfo, error) {
332	feeGrowthGlobal0X128 := p.FeeGrowthGlobal0X128().Clone()
333	feeGrowthGlobal1X128 := p.FeeGrowthGlobal1X128().Clone()
334	liquidityDelta := positionParams.liquidityDelta
335
336	var flippedLower, flippedUpper bool
337	if !liquidityDelta.IsZero() {
338		flippedLower = tickUpdate(
339			p,
340			positionParams.tickLower,
341			tick,
342			liquidityDelta,
343			feeGrowthGlobal0X128,
344			feeGrowthGlobal1X128,
345			false,
346			p.MaxLiquidityPerTick(),
347		)
348
349		flippedUpper = tickUpdate(
350			p,
351			positionParams.tickUpper,
352			tick,
353			liquidityDelta,
354			feeGrowthGlobal0X128,
355			feeGrowthGlobal1X128,
356			true,
357			p.MaxLiquidityPerTick(),
358		)
359
360		if flippedLower {
361			tickBitmapFlipTick(p, positionParams.tickLower, p.TickSpacing())
362		}
363
364		if flippedUpper {
365			tickBitmapFlipTick(p, positionParams.tickUpper, p.TickSpacing())
366		}
367	}
368
369	feeGrowthInside0X128, feeGrowthInside1X128 := getFeeGrowthInside(
370		p,
371		positionParams.tickLower,
372		positionParams.tickUpper,
373		tick,
374		feeGrowthGlobal0X128,
375		feeGrowthGlobal1X128,
376	)
377
378	positionKey := getPositionKey(positionParams.tickLower, positionParams.tickUpper)
379
380	position, err := positionUpdateWithKey(
381		p,
382		positionKey,
383		liquidityDelta,
384		feeGrowthInside0X128.Clone(),
385		feeGrowthInside1X128.Clone(),
386	)
387	if err != nil {
388		return pl.NewDefaultPositionInfo(), err
389	}
390
391	// clear any tick data that is no longer needed
392	if liquidityDelta.IsNeg() {
393		if flippedLower {
394			deleteTick(p, positionParams.tickLower)
395		}
396		if flippedUpper {
397			deleteTick(p, positionParams.tickUpper)
398		}
399	}
400
401	return position, nil
402}