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}