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}