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