sqrt_price_math.gno
11.77 Kb · 341 lines
1package gnsmath
2
3import (
4 "gno.land/p/gnoswap/consts/v1"
5 i256 "gno.land/p/gnoswap/int256/v1"
6 u256 "gno.land/p/gnoswap/uint256/v1"
7)
8
9// MIN_SQRT_RATIO returns the minimum valid Q64.96 square-root price ratio.
10//
11// Returns:
12// - minSqrtRatio: A fresh *u256.Uint containing 4,295,128,739, the lower
13// boundary accepted by the pool's square-root price math.
14func MIN_SQRT_RATIO() *u256.Uint { return consts.MinSqrtRatio() }
15
16// MAX_SQRT_RATIO returns the upper boundary used by Q64.96 square-root price math.
17//
18// Returns:
19// - maxSqrtRatio: A fresh *u256.Uint containing
20// 1461446703485210103287273052203988822378723970342. Inverse tick conversion
21// treats this boundary as exclusive.
22func MAX_SQRT_RATIO() *u256.Uint { return consts.MaxSqrtRatio() }
23
24// getNextPriceAmount0Add calculates the next sqrt price when adding token0 liquidity,
25// rounding up to ensure conservative pricing for the protocol.
26// This internal function handles the case where token0 is being added to the pool.
27func getNextPriceAmount0Add(
28 currentSqrtPriceX96, liquidity, amountToAdd *u256.Uint,
29) *u256.Uint {
30 // liquidityShifted = liquidity << 96
31 liquidityShifted := u256.Zero().Lsh(liquidity, Q96_RESOLUTION)
32 // amountTimesSqrtPrice = amount * sqrtPrice
33 amountTimesSqrtPrice := u256.Zero().Mul(amountToAdd, currentSqrtPriceX96)
34
35 // Overflow check: Ensure (amountTimesSqrtPrice / amountToAdd) == currentSqrtPriceX96
36 quotientCheck := u256.Zero().Div(amountTimesSqrtPrice, amountToAdd)
37 if quotientCheck.Eq(currentSqrtPriceX96) {
38 // denominator = liquidityShifted + amountTimesSqrtPrice
39 denominator := u256.Zero().Add(liquidityShifted, amountTimesSqrtPrice)
40 // only take this path when denominator >= liquidityShifted
41 if denominator.Gte(liquidityShifted) {
42 return u256.MulDivRoundingUp(liquidityShifted, currentSqrtPriceX96, denominator)
43 }
44 }
45
46 // fallback: liquidityShifted / ((liquidityShifted / sqrtPrice) + amount)
47 divValue := u256.Zero().Div(liquidityShifted, currentSqrtPriceX96)
48 denominator, overflow := u256.Zero().AddOverflow(divValue, amountToAdd)
49 if overflow {
50 panic(errSafeMathOverflow)
51 }
52 return u256.DivRoundingUp(liquidityShifted, denominator)
53}
54
55// getNextPriceAmount0Remove calculates the next sqrt price when removing token0 liquidity,
56// rounding up to ensure conservative pricing for the protocol.
57// This internal function handles the case where token0 is being removed from the pool.
58// Panics if validation checks fail (invalid pool sqrt price calculation).
59func getNextPriceAmount0Remove(
60 currentSqrtPriceX96, liquidity, amountToRemove *u256.Uint,
61) *u256.Uint {
62 // liquidityShifted = liquidity << 96
63 liquidityShifted := u256.Zero().Lsh(liquidity, Q96_RESOLUTION)
64 // amountTimesSqrtPrice = amountToRemove * currentSqrtPriceX96
65 amountTimesSqrtPrice := u256.Zero().Mul(amountToRemove, currentSqrtPriceX96)
66
67 // Validation checks
68 quotientCheck := u256.Zero().Div(amountTimesSqrtPrice, amountToRemove)
69 if !quotientCheck.Eq(currentSqrtPriceX96) || !liquidityShifted.Gt(amountTimesSqrtPrice) {
70 panic(errInvalidPoolSqrtPrice)
71 }
72
73 denominator := u256.Zero().Sub(liquidityShifted, amountTimesSqrtPrice)
74 return u256.MulDivRoundingUp(liquidityShifted, currentSqrtPriceX96, denominator)
75}
76
77// getNextSqrtPriceFromAmount0RoundingUp calculates the next sqrt price based on token0 amount,
78// always rounding up to ensure conservative pricing in both exact output and exact input cases.
79// The add parameter determines whether liquidity is being added (true) or removed (false).
80func getNextSqrtPriceFromAmount0RoundingUp(
81 sqrtPX96 *u256.Uint,
82 liquidity *u256.Uint,
83 amount *u256.Uint,
84 add bool,
85) *u256.Uint {
86 // Shortcut: if no amount, return original price
87 if amount.IsZero() {
88 return sqrtPX96
89 }
90
91 if add {
92 return getNextPriceAmount0Add(sqrtPX96, liquidity, amount)
93 }
94 return getNextPriceAmount0Remove(sqrtPX96, liquidity, amount)
95}
96
97// getNextPriceAmount1Add calculates the next sqrt price when adding token1,
98// preserving rounding-down logic for the final result.
99// This internal function handles the case where token1 is being added to the pool.
100func getNextPriceAmount1Add(
101 sqrtPX96, liquidity, amount *u256.Uint,
102) *u256.Uint {
103 var quotient *u256.Uint
104
105 if amount.Lte(consts.Max160()) {
106 // Use local variables to avoid allocation conflicts
107 shifted := u256.Zero().Lsh(amount, Q96_RESOLUTION)
108 quotient = u256.Zero().Div(shifted, liquidity)
109 } else {
110 quotient = u256.MulDiv(amount, consts.Q96(), liquidity)
111 }
112
113 result, overflow := u256.Zero().AddOverflow(sqrtPX96, quotient)
114 if overflow || result.Gt(consts.Max160()) {
115 panic(errSqrtPriceOverflow)
116 }
117
118 return result
119}
120
121// getNextPriceAmount1Remove calculates the next sqrt price when removing token1,
122// preserving rounding-down logic for the final result.
123// This internal function handles the case where token1 is being removed from the pool.
124// Panics if sqrt price would exceed quotient.
125func getNextPriceAmount1Remove(
126 sqrtPX96, liquidity, amount *u256.Uint,
127) *u256.Uint {
128 var quotient *u256.Uint
129
130 if amount.Lte(consts.Max160()) {
131 shifted := u256.Zero().Lsh(amount, Q96_RESOLUTION)
132 quotient = u256.DivRoundingUp(shifted, liquidity)
133 } else {
134 quotient = u256.MulDivRoundingUp(amount, consts.Q96(), liquidity)
135 }
136
137 if !sqrtPX96.Gt(quotient) {
138 panic(errSqrtPriceExceedsQuotient)
139 }
140
141 return u256.Zero().Sub(sqrtPX96, quotient)
142}
143
144// getNextSqrtPriceFromAmount1RoundingDown calculates the next sqrt price based on token1 amount,
145// always rounding down to ensure conservative pricing in both exact output and exact input cases.
146// The add parameter determines whether liquidity is being added (true) or removed (false).
147func getNextSqrtPriceFromAmount1RoundingDown(
148 sqrtPX96,
149 liquidity,
150 amount *u256.Uint,
151 add bool,
152) *u256.Uint {
153 // Shortcut: if no amount, return original price
154 if amount.IsZero() {
155 return sqrtPX96
156 }
157
158 if add {
159 return getNextPriceAmount1Add(sqrtPX96, liquidity, amount)
160 }
161 return getNextPriceAmount1Remove(sqrtPX96, liquidity, amount)
162}
163
164// getNextSqrtPriceFromInput calculates the next sqrt price after adding tokens to the pool,
165// rounding up for conservative pricing in both swap directions.
166// The zeroForOne parameter indicates swap direction (token0 for token1 when true).
167// Panics if sqrtPX96 or liquidity is zero.
168func getNextSqrtPriceFromInput(
169 sqrtPX96, liquidity, amountIn *u256.Uint,
170 zeroForOne bool,
171) *u256.Uint {
172 if sqrtPX96.IsZero() {
173 panic(errSqrtPriceZero)
174 }
175
176 if liquidity.IsZero() {
177 panic(errLiquidityZero)
178 }
179
180 if zeroForOne {
181 return getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountIn, true)
182 }
183
184 return getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountIn, true)
185}
186
187// getNextSqrtPriceFromOutput calculates the next sqrt price after removing tokens from the pool,
188// using different rounding directions based on swap direction.
189// The zeroForOne parameter indicates swap direction (token0 for token1 when true).
190// Panics if sqrtPX96 or liquidity is zero.
191func getNextSqrtPriceFromOutput(
192 sqrtPX96, liquidity, amountOut *u256.Uint,
193 zeroForOne bool,
194) *u256.Uint {
195 if sqrtPX96.IsZero() {
196 panic(errSqrtPriceZero)
197 }
198
199 if liquidity.IsZero() {
200 panic(errLiquidityZero)
201 }
202
203 if zeroForOne {
204 return getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountOut, false)
205 }
206
207 return getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountOut, false)
208}
209
210// getAmount0DeltaHelper calculates the absolute token0 amount difference between two price ranges,
211// automatically swapping inputs to ensure correct ordering. The roundUp parameter controls
212// rounding direction for the final result to ensure conservative AMM calculations.
213// Panics if sqrtRatioAX96 is zero.
214func getAmount0DeltaHelper(
215 sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint,
216 roundUp bool,
217) *u256.Uint {
218 if sqrtRatioAX96.Gt(sqrtRatioBX96) {
219 sqrtRatioAX96, sqrtRatioBX96 = sqrtRatioBX96, sqrtRatioAX96
220 }
221
222 // Use local variables for thread safety
223 numerator := u256.Zero().Lsh(liquidity, Q96_RESOLUTION)
224 difference := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96)
225
226 if sqrtRatioAX96.IsZero() {
227 panic(errSqrtRatioAX96Zero)
228 }
229
230 if roundUp {
231 intermediate := u256.MulDivRoundingUp(numerator, difference, sqrtRatioBX96)
232 return u256.DivRoundingUp(intermediate, sqrtRatioAX96)
233 }
234
235 intermediate := u256.MulDiv(numerator, difference, sqrtRatioBX96)
236 return u256.Zero().Div(intermediate, sqrtRatioAX96)
237}
238
239// getAmount1DeltaHelper calculates the absolute token1 amount difference between two price ranges,
240// automatically swapping inputs to ensure correct ordering. The roundUp parameter controls
241// rounding direction for the final result to ensure conservative AMM calculations.
242func getAmount1DeltaHelper(
243 sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint,
244 roundUp bool,
245) *u256.Uint {
246 if sqrtRatioAX96.Gt(sqrtRatioBX96) {
247 sqrtRatioAX96, sqrtRatioBX96 = sqrtRatioBX96, sqrtRatioAX96
248 }
249
250 // amount1 = liquidity * (sqrtB - sqrtA) / 2^96
251 // Use local variable for thread safety
252 difference := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96)
253
254 if roundUp {
255 return u256.MulDivRoundingUp(liquidity, difference, consts.Q96())
256 }
257
258 return u256.MulDiv(liquidity, difference, consts.Q96())
259}
260
261// GetAmount0Delta computes the signed token0 amount represented between two prices.
262// Positive liquidity rounds the amount up; negative liquidity returns a negative amount
263// rounded down after applying the magnitude.
264//
265// Parameters:
266// - sqrtRatioAX96: First price endpoint in Q64.96 square-root format.
267// - sqrtRatioBX96: Second price endpoint in Q64.96 square-root format.
268// - liquidity: Signed liquidity value; its sign determines the result sign and rounding.
269//
270// Returns:
271// - amount0Delta: Signed int256 token0 amount represented by the range.
272//
273// Panics if an input is nil or the computed magnitude cannot be represented by int256.
274func GetAmount0Delta(
275 sqrtRatioAX96, sqrtRatioBX96 *u256.Uint,
276 liquidity *i256.Int,
277) *i256.Int {
278 if sqrtRatioAX96 == nil || sqrtRatioBX96 == nil || liquidity == nil {
279 panic(errGetAmount0DeltaNilInput)
280 }
281
282 if liquidity.IsNeg() {
283 u := getAmount0DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), false)
284 if u.Gt(consts.MaxInt256()) {
285 // if u > (2**255 - 1), cannot cast to int256
286 panic(errAmount0DeltaOverflow)
287 }
288
289 // Convert to i256 and negate properly
290 return i256.Zero().Neg(i256.FromUint256(u))
291 }
292
293 u := getAmount0DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), true)
294 if u.Gt(consts.MaxInt256()) {
295 // if u > (2**255 - 1), cannot cast to int256
296 panic(errAmount0DeltaOverflow)
297 }
298
299 return i256.FromUint256(u)
300}
301
302// GetAmount1Delta computes the signed token1 amount represented between two prices.
303// Positive liquidity rounds the amount up; negative liquidity returns a negative amount
304// rounded down after applying the magnitude.
305//
306// Parameters:
307// - sqrtRatioAX96: First price endpoint in Q64.96 square-root format.
308// - sqrtRatioBX96: Second price endpoint in Q64.96 square-root format.
309// - liquidity: Signed liquidity value; its sign determines the result sign and rounding.
310//
311// Returns:
312// - amount1Delta: Signed int256 token1 amount represented by the range.
313//
314// Panics if an input is nil or the computed magnitude cannot be represented by int256.
315func GetAmount1Delta(
316 sqrtRatioAX96, sqrtRatioBX96 *u256.Uint,
317 liquidity *i256.Int,
318) *i256.Int {
319 if sqrtRatioAX96 == nil || sqrtRatioBX96 == nil || liquidity == nil {
320 panic(errGetAmount1DeltaNilInput)
321 }
322
323 if liquidity.IsNeg() {
324 u := getAmount1DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), false)
325 if u.Gt(consts.MaxInt256()) {
326 // if u > (2**255 - 1), cannot cast to int256
327 panic(errAmount1DeltaOverflow)
328 }
329
330 // Convert to i256 and negate properly
331 return i256.Zero().Neg(i256.FromUint256(u))
332 }
333
334 u := getAmount1DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), true)
335 if u.Gt(consts.MaxInt256()) {
336 // if u > (2**255 - 1), cannot cast to int256
337 panic(errAmount1DeltaOverflow)
338 }
339
340 return i256.FromUint256(u)
341}