Search Apps Documentation Source Content File Folder Download Copy Actions Download

position.gno

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