Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions 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}