Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

position.gno

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