Transaction
79835747FD7CF2…C8F68FE58B19
Block 77,240 · index 0 · indexed
Summary
- Hash
- 79835747FD7CF285FD84B8E38A3B189457A7AD740C7F5366BCB4C8F68FE58B19
- Block
- 77,240
- Size
- 59592 bytes
- Gas used
- 75,356,683 / 90,427,984
- Fee
- 90428ugnot
- Status
- success
Messages
Arguments · 24
- #1gnsmath
- #2README.md
- #3# gnsmath Core mathematical operations for GnoSwap's concentrated liquidity AMM. ## Overview This package provides the fundamental calculations for concentrated liquidity, including tick conversion, liquidity math calculations, sqrt price math, swap calculations, and bit manipulation utilities. Operations use Q64.96, Q128.128, and Q160 fixed-point representations where appropriate. The implementation follows Uniswap V3's mathematical model, ensuring compatibility and correctness for cross-chain liquidity operations. ## Features - **Bit Math**: MSB/LSB calculations for tick bitmap operations - **Tick Math**: Tick and Q64.96 sqrt-price conversions - **Liquidity Math**: Liquidity and token amount conversions for price ranges - **Sqrt Price Math**: Token amount conversions using Q64.96 format - **Swap Math**: Single-step swap calculations with fee handling - **Overflow Protection**: Built-in int256 overflow detection - **Rounding Control**: Configurable rounding for AMM safety ## Core Concepts ### Q96 Fixed-Point Format Square root prices use Q64.96 representation: - `sqrtPriceX96 = sqrt(token1/token0) * 2^96` - Enables precise integer arithmetic without floating-point ### Rounding Directions - **Round UP**: Amounts owed TO pool (deposits, exact input) - **Round DOWN**: Amounts owed FROM pool (withdrawals, exact output) ## Usage ```go package main import ( "gno.land/p/gnoswap/gnsmath/v1" i256 "gno.land/p/gnoswap/int256/v1" u256 "gno.land/p/gnoswap/uint256/v1" ) func main() { // Calculate token amounts for a signed liquidity change. sqrtPriceA := u256.MustFromDecimal("79228162514264337593543950336") sqrtPriceB := u256.MustFromDecimal("79625275426524748796330556128") liquidityDelta := i256.MustFromDecimal("1000000000000000000") amount0 := gnsmath.GetAmount0Delta(sqrtPriceA, sqrtPriceB, liquidityDelta) amount1 := gnsmath.GetAmount1Delta(sqrtPriceA, sqrtPriceB, liquidityDelta) println(amount0.ToString(), amount1.ToString()) // Q64.96 prices and a positive amount remaining select exact input. feePips := uint64(3000) // 0.3% currentPrice := u256.MustFromDecimal("79228162514264337593543950336") targetPrice := u256.MustFromDecimal("158456325028528675187087900672") liquidity := u256.MustFromDecimal("1000000000000000000") amountRemaining := i256.MustFromDecimal("1000000") sqrtPriceNext, amountIn, amountOut, feeAmount := gnsmath.SwapMathComputeSwapStep( currentPrice, targetPrice, liquidity, amountRemaining, feePips, ) println(sqrtPriceNext.ToString(), amountIn.ToString(), amountOut.ToString(), feeAmount.ToString()) tickBitmap := u256.NewUint(0xFF00) println(gnsmath.BitMathMostSignificantBit(tickBitmap)) // 15 println(gnsmath.BitMathLeastSignificantBit(tickBitmap)) // 8 } ``` ## API ### Bit Math - `BitMathMostSignificantBit(x *u256.Uint) uint8` - Find MSB position (0-255) - `BitMathLeastSignificantBit(x *u256.Uint) uint8` - Find LSB position (0-255) ### Tick Math - `TickMathGetSqrtRatioAtTick(tick int32) *u256.Uint` - Convert tick to Q64.96 sqrt price - `TickMathGetTickAtSqrtRatio(sqrtPriceX96 *u256.Uint) int32` - Convert a Q64.96 sqrt price to tick; accepts `[MinSqrtRatio, MaxSqrtRatio)` ### Liquidity Math - `GetLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1 *u256.Uint) *u256.Uint` - Calculate max liquidity from token amounts and price range - `GetAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) (*u256.Uint, *u256.Uint)` - Calculate token amounts represented by liquidity and price range - `LiquidityMathAddDelta(x *u256.Uint, y *i256.Int) *u256.Uint` - Apply signed liquidity delta; the result is bounded by `MaxUint128` and panics if it exceeds that bound ### Sqrt Price Math - `GetAmount0Delta(sqrtRatioAX96, sqrtRatioBX96 *u256.Uint, liquidity *i256.Int) *i256.Int` - Calculate token0 amount as `liquidity * (1/√Pa - 1/√Pb)` after ordering the ratios; Q64.96 scaling and rounding are applied - `GetAmount1Delta(sqrtRatioAX96, sqrtRatioBX96 *u256.Uint, liquidity *i256.Int) *i256.Int` - Calculate token1 amount as `liquidity * (√Pb - √Pa) / 2^96` after ordering the ratios; rounding depends on the sign of liquidity ### Swap Math - `SwapMathComputeSwapStep(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity *u256.Uint, amountRemaining *i256.Int, feePips uint64) (*u256.Uint, *u256.Uint, *u256.Uint, *u256.Uint)` - Returns: (nextSqrtPrice, amountIn, amountOut, feeAmount) - Handles both exact input and exact output swaps
- #4bit_math.gno
- #5package gnsmath import ( "math/bits" u256 "gno.land/p/gnoswap/uint256/v1" ) // BitMathMostSignificantBit returns the 0-based position of the most significant bit in x. // This function is essential for AMM calculations involving price ranges and tick boundaries. // // Parameters: // - x: the non-zero value for which to compute the most significant set bit // // Returns: // - bitIndex: the zero-based position of the most significant set bit (0-255) // // Panics if x is zero. func BitMathMostSignificantBit(x *u256.Uint) uint8 { if x.IsZero() { panic(errMSBZeroInput) } return uint8(x.BitLen() - 1) } // BitMathLeastSignificantBit returns the 0-based position of the least significant bit in x. // This function is used in AMM calculations for efficient bit manipulation and range queries. // // Parameters: // - x: the non-zero value for which to compute the least significant set bit // // Returns: // - bitIndex: the zero-based position of the least significant set bit (0-255) // // Panics if x is zero. func BitMathLeastSignificantBit(x *u256.Uint) uint8 { if x.IsZero() { panic(errLSBZeroInput) } if x[0] != 0 { return uint8(bits.TrailingZeros64(x[0])) } if x[1] != 0 { return uint8(64 + bits.TrailingZeros64(x[1])) } if x[2] != 0 { return uint8(128 + bits.TrailingZeros64(x[2])) } return uint8(192 + bits.TrailingZeros64(x[3])) }
- #6consts.gno
- #7package gnsmath const ( // minTick is the minimum valid tick index in the concentrated liquidity model. // Represents the lowest possible price: 1.0001^(-887272) ≈ 0 minTick = -887272 // maxTick is the maximum valid tick index in the concentrated liquidity model. // Represents the highest possible price: 1.0001^887272 ≈ infinity maxTick = 887272 Q96_RESOLUTION uint = 96 Q160_RESOLUTION uint = 160 MAX_UINT128 = "340282366920938463463374607431768211455" // 2^128 - 1 Q96 = "79228162514264337593543950336" // 2^96 )
- #8doc.gno
- #9// Package gnsmath provides core mathematical operations for GnoSwap's concentrated liquidity AMM. // // ## Overview // // This package provides the fundamental calculations for concentrated liquidity, // including tick conversion, liquidity math calculations, sqrt price math, swap // calculations, and bit manipulation utilities. All operations use Q64.96, // Q128.128, and Q160 fixed-point arithmetic where appropriate. // // The implementation follows Uniswap V3's mathematical model. // // ## Features // // - Bit Math: MSB/LSB calculations for tick bitmap operations // - Tick Math: tick and Q64.96 sqrt-price conversions // - Liquidity Math: liquidity and token amount math for price ranges // - Sqrt Price Math: token amount conversions using Q64.96 format // - Swap Math: single-step swap calculations with fee handling // - Overflow Protection: explicit int256/uint256 overflow checks // - Rounding Control: configurable rounding for AMM safety // // ## Core Concepts // // ### Q64.96 Fixed-Point Format // // Square root prices use Q64.96 representation: // - sqrtPriceX96 = sqrt(token1/token0) * 2^96 // - Enables precise integer arithmetic without floating-point // // ### Rounding Directions // // - Round UP: amounts owed TO pool (deposits, exact input) // - Round DOWN: amounts owed FROM pool (withdrawals, exact output) // // ## API // // ### Bit Math // // - BitMathMostSignificantBit(x *u256.Uint) uint8 // - BitMathLeastSignificantBit(x *u256.Uint) uint8 // // ### Tick Math // // - TickMathGetSqrtRatioAtTick(tick int32) *u256.Uint // - TickMathGetTickAtSqrtRatio(sqrtPriceX96 *u256.Uint) int32; accepts // `[MinSqrtRatio, MaxSqrtRatio)` (the upper bound is exclusive) // // ### Liquidity Math // // - GetLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1 *u256.Uint) *u256.Uint // - GetAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) (*u256.Uint, *u256.Uint) // - LiquidityMathAddDelta(x *u256.Uint, y *i256.Int) *u256.Uint // // ### Sqrt Price Math // // - GetAmount0Delta(sqrtRatioAX96, sqrtRatioBX96 *u256.Uint, liquidity *i256.Int) *i256.Int // - GetAmount1Delta(sqrtRatioAX96, sqrtRatioBX96 *u256.Uint, liquidity *i256.Int) *i256.Int // // ### Swap Math // // - SwapMathComputeSwapStep(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity *u256.Uint, amountRemaining *i256.Int, feePips uint64) (*u256.Uint, *u256.Uint, *u256.Uint, *u256.Uint) package gnsmath
- #10errors.gno
- #11package gnsmath import ( ufmt "gno.land/p/nt/ufmt/v0" ) const ( errInvalidPoolSqrtPrice = "invalid pool sqrt price calculation: product/amount != sqrtPX96 or numerator1 <= product" errSqrtPriceOverflow = "sqrt price overflow" errSqrtPriceExceedsQuotient = "sqrt price exceeds calculated quotient" errSqrtPriceZero = "sqrtPX96 should not be zero" errLiquidityZero = "liquidity should not be zero" errSqrtRatioAX96Zero = "sqrtRatioAX96 must be greater than zero" errAmount0DeltaOverflow = "GetAmount0Delta: overflow" errAmount1DeltaOverflow = "GetAmount1Delta: overflow" errMSBZeroInput = "input for MSB calculation should not be zero" errLSBZeroInput = "input for LSB calculation should not be zero" errGetAmount0DeltaNilInput = "GetAmount0Delta: input parameters cannot be nil" errGetAmount1DeltaNilInput = "GetAmount1Delta: input parameters cannot be nil" errTickMathOutOfRange = "tick_math: value out of range" errTickMathInvalidInput = "tick_math: invalid input data" errTickMathOverflow = "tick_math: overflow" errLiquidityIdenticalTicks = "liquidity_math: identical ticks" errLiquidityOverflow = "liquidity_math: overflow" errSafeMathOverflow = "safe_math: overflow" ) func newErrorWithDetail(errMsg string, detail string) string { return ufmt.Errorf("%s || %s", errMsg, detail).Error() }
- #12gnomod.toml
- #13module = "gno.land/p/gnoswap/gnsmath/v1" gno = "0.9"
- #14liquidity_math.gno
- #15package gnsmath import ( ufmt "gno.land/p/nt/ufmt/v0" "gno.land/p/gnoswap/consts/v1" i256 "gno.land/p/gnoswap/int256/v1" u256 "gno.land/p/gnoswap/uint256/v1" ) // computeLiquidityForAmount0 calculates the liquidity for a given amount of token0. // // This function computes the maximum possible liquidity that can be provided for `token0` // based on the provided price boundaries (sqrtRatioAX96 and sqrtRatioBX96) in Q64.96 format. // // Parameters: // - sqrtRatioAX96: *u256.Uint - The square root price at the lower tick boundary (Q64.96). // - sqrtRatioBX96: *u256.Uint - The square root price at the upper tick boundary (Q64.96). // - amount0: *u256.Uint - The amount of token0 to be converted to liquidity. // // Returns: // - *u256.Uint: The calculated liquidity, represented as an unsigned 128-bit integer (uint128). // // Panics: // - If the resulting liquidity exceeds the uint128 range, `SafeConvertToUint128` will trigger a panic. func computeLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0 *u256.Uint) *u256.Uint { sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) intermediate := u256.MulDiv(sqrtRatioAX96, sqrtRatioBX96, consts.Q96()) diff := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) if diff.IsZero() { panic(newErrorWithDetail( errLiquidityIdenticalTicks, ufmt.Sprintf("sqrtRatioAX96 (%s) and sqrtRatioBX96 (%s) are identical", sqrtRatioAX96.ToString(), sqrtRatioBX96.ToString()), )) } res := u256.MulDiv(amount0, intermediate, diff) return SafeConvertToUint128(res) } // computeLiquidityForAmount1 calculates liquidity based on the provided token1 amount and price range. // // This function computes the liquidity for a given amount of token1 by using the difference // between the upper and lower square root price ratios. The calculation uses Q96 fixed-point // arithmetic to maintain precision. // // Parameters: // - sqrtRatioAX96: *u256.Uint - The square root ratio of price at the lower tick, represented in Q96 format. // - sqrtRatioBX96: *u256.Uint - The square root ratio of price at the upper tick, represented in Q96 format. // - amount1: *u256.Uint - The amount of token1 to calculate liquidity for. // // Returns: // - *u256.Uint: The calculated liquidity based on the provided amount of token1 and price range. // // Notes: // - The result is not directly limited to uint128, as liquidity values can exceed uint128 bounds. // - If `sqrtRatioAX96 == sqrtRatioBX96`, the function will panic due to division by zero. // - Q96 is a constant representing `2^96`, ensuring that precision is maintained during division. // // Panics: // - If the resulting liquidity exceeds the uint128 range, `SafeConvertToUint128` will trigger a panic. func computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1 *u256.Uint) *u256.Uint { sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) diff := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) if diff.IsZero() { panic(newErrorWithDetail( errLiquidityIdenticalTicks, ufmt.Sprintf("sqrtRatioAX96 (%s) and sqrtRatioBX96 (%s) are identical", sqrtRatioAX96.ToString(), sqrtRatioBX96.ToString()), )) } res := u256.MulDiv(amount1, consts.Q96(), diff) return SafeConvertToUint128(res) } // GetLiquidityForAmounts calculates the maximum liquidity supported by two token amounts. // // The current square-root price determines which token amounts are active. Below // the range only token0 is used, above it only token1 is used, and inside it the // smaller of the token0- and token1-derived liquidities is returned. // // Parameters: // - sqrtRatioX96: Current square-root price, encoded as a Q64.96 ratio. // - sqrtRatioAX96: First price-range endpoint, encoded as a Q64.96 ratio. // - sqrtRatioBX96: Second price-range endpoint, encoded as a Q64.96 ratio; the endpoints are sorted. // - amount0: Available token0 amount. // - amount1: Available token1 amount. // // Returns: // - liquidity: Maximum liquidity supported by the amounts, constrained to uint128. // // Panics if a selected amount/range calculation has identical bounds or exceeds uint128. func GetLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1 *u256.Uint) (liquidity *u256.Uint) { sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) if sqrtRatioX96.Lte(sqrtRatioAX96) { liquidity = computeLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0) } else if sqrtRatioX96.Lt(sqrtRatioBX96) { liquidity0 := computeLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0) liquidity1 := computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1) if liquidity0.Lt(liquidity1) { liquidity = liquidity0 } else { liquidity = liquidity1 } } else { liquidity = computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1) } return liquidity } // computeAmount0ForLiquidity calculates the required amount of token0 for a given liquidity level // within a specified price range (represented by sqrt ratios). // // This function determines the amount of token0 needed to provide a specified amount of liquidity // within a price range defined by sqrtRatioAX96 (lower bound) and sqrtRatioBX96 (upper bound). // // Parameters: // - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). // - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). // - liquidity: The liquidity to be provided (*u256.Uint). // // Returns: // - *u256.Uint: The amount of token0 required to achieve the specified liquidity level. // // Notes: // - This function assumes the price bounds are expressed in Q64.96 fixed-point format. // - The function returns 0 if the liquidity is 0 or the price bounds are invalid. // - Handles edge cases where sqrtRatioAX96 equals sqrtRatioBX96 by returning 0 (to prevent division by zero). func computeAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) *u256.Uint { sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) if sqrtRatioAX96.IsZero() || sqrtRatioBX96.IsZero() || liquidity.IsZero() || sqrtRatioAX96.Eq(sqrtRatioBX96) { return u256.Zero() } val1 := u256.Zero().Lsh(liquidity, Q96_RESOLUTION) val2 := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) res := u256.MulDiv(val1, val2, sqrtRatioBX96) res = res.Div(res, sqrtRatioAX96) return res } // computeAmount1ForLiquidity calculates the required amount of token1 for a given liquidity level // within a specified price range (represented by sqrt ratios). // // This function determines the amount of token1 needed to provide liquidity between the // lower (sqrtRatioAX96) and upper (sqrtRatioBX96) price bounds. The calculation is performed // in Q64.96 fixed-point format, which is standard for many liquidity calculations. // // Parameters: // - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). // - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). // - liquidity: The liquidity amount to be used in the calculation (*u256.Uint). // // Returns: // - *u256.Uint: The amount of token1 required to achieve the specified liquidity level. // // Notes: // - This function handles edge cases where the liquidity is zero or when sqrtRatioAX96 equals sqrtRatioBX96 // to prevent division by zero. // - The calculation assumes sqrtRatioAX96 is always less than or equal to sqrtRatioBX96 after the initial // ascending order sorting. func computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) *u256.Uint { sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) if liquidity.IsZero() || sqrtRatioAX96.Eq(sqrtRatioBX96) { return u256.Zero() } diff := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) res := u256.MulDiv(liquidity, diff, consts.Q96()) return res } // GetAmountsForLiquidity calculates the amounts of token0 and token1 represented // by a specified liquidity within a price range. // // If the current price is below the lower bound, only token0 is required. If the // current price is above the upper bound, only token1 is required. When the // price is within the range, both token0 and token1 are calculated. // // Parameters: // - sqrtRatioX96: Current square-root price in Q64.96 format. // - sqrtRatioAX96: First price-range endpoint in Q64.96 format. // - sqrtRatioBX96: Second price-range endpoint in Q64.96 format; the endpoints are ordered internally. // - liquidity: Non-negative liquidity amount to value. // // Returns: // - amount0: Token0 amount represented by liquidity; zero when the current price is at or above the upper endpoint. // - amount1: Token1 amount represented by liquidity; zero when the current price is at or below the lower endpoint. // Call ToString() on either value for decimal display. // // Notes: // - If liquidity is zero, the function returns zero values for both tokens. // - At a boundary, the corresponding out-of-range token amount is zero. // // Example: // ``` // amount0, amount1 := GetAmountsForLiquidity( // // u256.MustFromDecimal("79228162514264337593543950336"), // sqrtRatioX96 (1.0 in Q64.96) // u256.MustFromDecimal("39614081257132168796771975168"), // sqrtRatioAX96 (0.5 in Q64.96) // u256.MustFromDecimal("158456325028528675187087900672"), // sqrtRatioBX96 (2.0 in Q64.96) // u256.MustFromDecimal("1000000"), // Liquidity // // ) // // println("Token0:", amount0.ToString(), "Token1:", amount1.ToString()) // // // Output: // Token0: 500000, Token1: 500000 // ``` func GetAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) (*u256.Uint, *u256.Uint) { if liquidity.IsZero() { return u256.Zero(), u256.Zero() } sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) amount0 := u256.Zero() amount1 := u256.Zero() if sqrtRatioX96.Lte(sqrtRatioAX96) { amount0 = computeAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) } else if sqrtRatioX96.Lt(sqrtRatioBX96) { amount0 = computeAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, liquidity) amount1 = computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, liquidity) } else { amount1 = computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) } return amount0, amount1 } // LiquidityMathAddDelta calculates the new liquidity after applying a signed delta. // A negative delta subtracts its magnitude; a non-negative delta adds its magnitude. // // Parameters: // - x: Current non-negative liquidity value. // - y: Signed liquidity delta; positive values add and negative values subtract. // // Returns: // - liquidity: Updated liquidity, constrained to the uint128 range. // // Panics if x or y is nil, subtraction underflows, addition overflows uint256, // or the resulting liquidity exceeds MaxUint128. func LiquidityMathAddDelta(x *u256.Uint, y *i256.Int) *u256.Uint { if x == nil || y == nil { panic("liquidity_math: x or y is nil") } yAbs := y.Abs() // Subtract or add based on the sign of y if y.Lt(i256.Zero()) { z := u256.Zero().Sub(x, yAbs) if z.Gte(x) { panic(ufmt.Sprintf( "liquidity_math: underflow (x: %s, y: %s, z:%s)", x.ToString(), y.ToString(), z.ToString())) } if z.Gt(consts.MaxUint128()) { panic(ufmt.Sprintf( "liquidity_math: result exceeds uint128 range (z: %s)", z.ToString())) } return z } z := u256.Zero().Add(x, yAbs) if z.Lt(x) { panic(ufmt.Sprintf( "liquidity_math: overflow (x: %s, y: %s, z:%s)", x.ToString(), y.ToString(), z.ToString())) } if z.Gt(consts.MaxUint128()) { panic(ufmt.Sprintf( "liquidity_math: result exceeds uint128 range (z: %s)", z.ToString())) } return z } // toAscendingOrder returns the two values in ascending order. func toAscendingOrder(a, b *u256.Uint) (*u256.Uint, *u256.Uint) { if a.Gt(b) { return b, a } return a, b } // SafeConvertToUint128 verifies that value fits in the uint128 range. // // No representation conversion is performed: the original pointer is returned // when its value is at most 2^128 - 1. // // Parameters: // - value: Non-nil unsigned 256-bit value to validate. // // Returns: // - converted: The same *u256.Uint pointer when value fits in uint128. // // Panics if value is nil or exceeds the maximum uint128 value. func SafeConvertToUint128(value *u256.Uint) *u256.Uint { if value.Gt(consts.MaxUint128()) { panic(ufmt.Sprintf( "%v: amount(%s) overflows uint128 range", errLiquidityOverflow, value.ToString())) } return value }
- #16safe_math.gno
- #17package gnsmath import ( "math" ufmt "gno.land/p/nt/ufmt/v0" i256 "gno.land/p/gnoswap/int256/v1" u256 "gno.land/p/gnoswap/uint256/v1" ) // Range bounds used by the safe conversion helpers. const ( maxInt64Decimal = "9223372036854775807" // 2^63 - 1 maxInt128Decimal = "170141183460469231731687303715884105727" // 2^127 - 1 ) // MaxInt128 returns the largest positive value representable by a signed 128-bit integer. // // Returns: // - maxInt128: A fresh *i256.Int containing 2^127 - 1, used as the upper bound // for conversions that must fit in the signed int128 range. func MaxInt128() *i256.Int { return i256.MustFromDecimal(maxInt128Decimal) } // SafeAddInt64 returns the exact sum of two signed 64-bit integers. // // Parameters: // - a: First signed int64 operand. // - b: Second signed int64 operand. // // Returns: // - sum: a + b when the mathematical result is within [math.MinInt64, math.MaxInt64]. // // Panics if the signed int64 sum overflows or underflows. func SafeAddInt64(a, b int64) int64 { if a > 0 && b > math.MaxInt64-a { panic("int64 addition overflow") } if a < 0 && b < math.MinInt64-a { panic("int64 addition underflow") } return a + b } // SafeSubInt64 returns the exact difference of two signed 64-bit integers. // // Parameters: // - a: Signed int64 minuend. // - b: Signed int64 subtrahend. // // Returns: // - difference: a - b when the mathematical result is within [math.MinInt64, math.MaxInt64]. // // Panics if the signed int64 difference overflows or underflows. func SafeSubInt64(a, b int64) int64 { if b > 0 && a < math.MinInt64+b { panic("int64 subtraction underflow") } if b < 0 && a > math.MaxInt64+b { panic("int64 subtraction overflow") } return a - b } // SafeMulInt64 returns the exact product of two signed 64-bit integers. // // Parameters: // - a: First signed int64 factor. // - b: Second signed int64 factor. // // Returns: // - product: a * b when the mathematical result is within [math.MinInt64, math.MaxInt64]. // // Panics if the signed int64 product overflows or underflows. func SafeMulInt64(a, b int64) int64 { if a == 0 || b == 0 { return 0 } if a > 0 && b > 0 { if a > math.MaxInt64/b { panic("int64 multiplication overflow") } } else if a < 0 && b < 0 { if a < math.MaxInt64/b { panic("int64 multiplication overflow") } } else if a > 0 && b < 0 { if b < math.MinInt64/a { panic("int64 multiplication underflow") } } else { // a < 0 && b > 0 if a < math.MinInt64/b { panic("int64 multiplication underflow") } } return a * b } // SafeMulDivInt64 returns the truncated quotient (a * b) / c. // // The product is formed in signed 256-bit arithmetic before division, so an // intermediate product may exceed int64 while the final quotient must still fit. // // Parameters: // - a: First signed int64 factor. // - b: Second signed int64 factor. // - c: Non-zero signed int64 divisor. // // Returns: // - quotient: The signed integer quotient after dividing a * b by c. // // Panics if the 256-bit product overflows, c is zero, or the quotient is outside // the representable int64 range. func SafeMulDivInt64(a, b, c int64) int64 { if a == 0 || b == 0 { return 0 } result, overflow := i256.Zero().MulOverflow(i256.NewInt(a), i256.NewInt(b)) if overflow { panic(errSafeMathOverflow) } result = i256.Zero().Div(result, i256.NewInt(c)) if !result.IsInt64() { panic(errSafeMathOverflow) } return result.Int64() } // SafeAbsInt64 returns the non-negative absolute value of a. // // Parameters: // - a: Signed int64 value whose magnitude is requested. // // Returns: // - magnitude: |a| as int64. // // Panics when a is math.MinInt64 because its positive magnitude cannot be // represented by int64. func SafeAbsInt64(a int64) int64 { if a == math.MinInt64 { panic(errSafeMathOverflow) } if a < 0 { return -a } return a } // SafeAddUint64 returns the exact sum of two unsigned 64-bit integers. // // Parameters: // - a: First uint64 operand. // - b: Second uint64 operand. // // Returns: // - sum: a + b when the mathematical result is at most math.MaxUint64. // // Panics if the uint64 sum overflows. func SafeAddUint64(a, b uint64) uint64 { if a > math.MaxUint64-b { panic("uint64 addition overflow") } return a + b } // SafeSubUint64 returns the exact difference of two unsigned 64-bit integers. // // Parameters: // - a: Unsigned uint64 minuend. // - b: Unsigned uint64 subtrahend; it must not exceed a. // // Returns: // - difference: a - b. // // Panics if b is greater than a and the subtraction would underflow uint64. func SafeSubUint64(a, b uint64) uint64 { if a < b { panic("uint64 subtraction underflow") } return a - b } // SafeUint64ToInt64 converts a uint64 to a signed int64 without changing its value. // // Parameters: // - value: Unsigned value to convert; it must be no greater than 2^63 - 1. // // Returns: // - converted: value represented as int64. // // Panics when value exceeds math.MaxInt64. func SafeUint64ToInt64(value uint64) int64 { if value > uint64(math.MaxInt64) { panic(ufmt.Sprintf( "amount(%d) overflows int64 range (max: %s)", value, maxInt64Decimal, )) } return int64(value) } // SafeConvertToInt64 converts a non-negative 256-bit integer to int64. // // Parameters: // - value: Unsigned 256-bit value to convert; nil is invalid. // // Returns: // - converted: value represented as int64 when it is at most math.MaxInt64. // // Panics when value is nil or outside the int64 range. func SafeConvertToInt64(value *u256.Uint) int64 { if value == nil { panic("SafeConvertToInt64: value is nil") } res, overflow := value.Uint64WithOverflow() if overflow || res > uint64(math.MaxInt64) { panic(ufmt.Sprintf( "amount(%s) overflows int64 range (max: %s)", value.ToString(), maxInt64Decimal, )) } return int64(res) } // SafeConvertToInt128 converts a non-negative 256-bit integer to signed int128. // // Parameters: // - value: Unsigned 256-bit value to convert; nil is invalid. // // Returns: // - converted: A new *i256.Int representing value when it is at most 2^127 - 1. // // Panics when value is nil or exceeds the largest positive signed int128 value. func SafeConvertToInt128(value *u256.Uint) *i256.Int { if value == nil { panic("SafeConvertToInt128: value is nil") } converted := i256.FromUint256(value) if converted.Gt(MaxInt128()) { panic(ufmt.Sprintf( "amount(%s) overflows int128 range", value.ToString(), )) } return converted }
- #18sqrt_price_math.gno
- #19package gnsmath import ( "gno.land/p/gnoswap/consts/v1" i256 "gno.land/p/gnoswap/int256/v1" u256 "gno.land/p/gnoswap/uint256/v1" ) // MIN_SQRT_RATIO returns the minimum valid Q64.96 square-root price ratio. // // Returns: // - minSqrtRatio: A fresh *u256.Uint containing 4,295,128,739, the lower // boundary accepted by the pool's square-root price math. func MIN_SQRT_RATIO() *u256.Uint { return consts.MinSqrtRatio() } // MAX_SQRT_RATIO returns the upper boundary used by Q64.96 square-root price math. // // Returns: // - maxSqrtRatio: A fresh *u256.Uint containing // 1461446703485210103287273052203988822378723970342. Inverse tick conversion // treats this boundary as exclusive. func MAX_SQRT_RATIO() *u256.Uint { return consts.MaxSqrtRatio() } // getNextPriceAmount0Add calculates the next sqrt price when adding token0 liquidity, // rounding up to ensure conservative pricing for the protocol. // This internal function handles the case where token0 is being added to the pool. func getNextPriceAmount0Add( currentSqrtPriceX96, liquidity, amountToAdd *u256.Uint, ) *u256.Uint { // liquidityShifted = liquidity << 96 liquidityShifted := u256.Zero().Lsh(liquidity, Q96_RESOLUTION) // amountTimesSqrtPrice = amount * sqrtPrice amountTimesSqrtPrice := u256.Zero().Mul(amountToAdd, currentSqrtPriceX96) // Overflow check: Ensure (amountTimesSqrtPrice / amountToAdd) == currentSqrtPriceX96 quotientCheck := u256.Zero().Div(amountTimesSqrtPrice, amountToAdd) if quotientCheck.Eq(currentSqrtPriceX96) { // denominator = liquidityShifted + amountTimesSqrtPrice denominator := u256.Zero().Add(liquidityShifted, amountTimesSqrtPrice) // only take this path when denominator >= liquidityShifted if denominator.Gte(liquidityShifted) { return u256.MulDivRoundingUp(liquidityShifted, currentSqrtPriceX96, denominator) } } // fallback: liquidityShifted / ((liquidityShifted / sqrtPrice) + amount) divValue := u256.Zero().Div(liquidityShifted, currentSqrtPriceX96) denominator, overflow := u256.Zero().AddOverflow(divValue, amountToAdd) if overflow { panic(errSafeMathOverflow) } return u256.DivRoundingUp(liquidityShifted, denominator) } // getNextPriceAmount0Remove calculates the next sqrt price when removing token0 liquidity, // rounding up to ensure conservative pricing for the protocol. // This internal function handles the case where token0 is being removed from the pool. // Panics if validation checks fail (invalid pool sqrt price calculation). func getNextPriceAmount0Remove( currentSqrtPriceX96, liquidity, amountToRemove *u256.Uint, ) *u256.Uint { // liquidityShifted = liquidity << 96 liquidityShifted := u256.Zero().Lsh(liquidity, Q96_RESOLUTION) // amountTimesSqrtPrice = amountToRemove * currentSqrtPriceX96 amountTimesSqrtPrice := u256.Zero().Mul(amountToRemove, currentSqrtPriceX96) // Validation checks quotientCheck := u256.Zero().Div(amountTimesSqrtPrice, amountToRemove) if !quotientCheck.Eq(currentSqrtPriceX96) || !liquidityShifted.Gt(amountTimesSqrtPrice) { panic(errInvalidPoolSqrtPrice) } denominator := u256.Zero().Sub(liquidityShifted, amountTimesSqrtPrice) return u256.MulDivRoundingUp(liquidityShifted, currentSqrtPriceX96, denominator) } // getNextSqrtPriceFromAmount0RoundingUp calculates the next sqrt price based on token0 amount, // always rounding up to ensure conservative pricing in both exact output and exact input cases. // The add parameter determines whether liquidity is being added (true) or removed (false). func getNextSqrtPriceFromAmount0RoundingUp( sqrtPX96 *u256.Uint, liquidity *u256.Uint, amount *u256.Uint, add bool, ) *u256.Uint { // Shortcut: if no amount, return original price if amount.IsZero() { return sqrtPX96 } if add { return getNextPriceAmount0Add(sqrtPX96, liquidity, amount) } return getNextPriceAmount0Remove(sqrtPX96, liquidity, amount) } // getNextPriceAmount1Add calculates the next sqrt price when adding token1, // preserving rounding-down logic for the final result. // This internal function handles the case where token1 is being added to the pool. func getNextPriceAmount1Add( sqrtPX96, liquidity, amount *u256.Uint, ) *u256.Uint { var quotient *u256.Uint if amount.Lte(consts.Max160()) { // Use local variables to avoid allocation conflicts shifted := u256.Zero().Lsh(amount, Q96_RESOLUTION) quotient = u256.Zero().Div(shifted, liquidity) } else { quotient = u256.MulDiv(amount, consts.Q96(), liquidity) } result, overflow := u256.Zero().AddOverflow(sqrtPX96, quotient) if overflow || result.Gt(consts.Max160()) { panic(errSqrtPriceOverflow) } return result } // getNextPriceAmount1Remove calculates the next sqrt price when removing token1, // preserving rounding-down logic for the final result. // This internal function handles the case where token1 is being removed from the pool. // Panics if sqrt price would exceed quotient. func getNextPriceAmount1Remove( sqrtPX96, liquidity, amount *u256.Uint, ) *u256.Uint { var quotient *u256.Uint if amount.Lte(consts.Max160()) { shifted := u256.Zero().Lsh(amount, Q96_RESOLUTION) quotient = u256.DivRoundingUp(shifted, liquidity) } else { quotient = u256.MulDivRoundingUp(amount, consts.Q96(), liquidity) } if !sqrtPX96.Gt(quotient) { panic(errSqrtPriceExceedsQuotient) } return u256.Zero().Sub(sqrtPX96, quotient) } // getNextSqrtPriceFromAmount1RoundingDown calculates the next sqrt price based on token1 amount, // always rounding down to ensure conservative pricing in both exact output and exact input cases. // The add parameter determines whether liquidity is being added (true) or removed (false). func getNextSqrtPriceFromAmount1RoundingDown( sqrtPX96, liquidity, amount *u256.Uint, add bool, ) *u256.Uint { // Shortcut: if no amount, return original price if amount.IsZero() { return sqrtPX96 } if add { return getNextPriceAmount1Add(sqrtPX96, liquidity, amount) } return getNextPriceAmount1Remove(sqrtPX96, liquidity, amount) } // getNextSqrtPriceFromInput calculates the next sqrt price after adding tokens to the pool, // rounding up for conservative pricing in both swap directions. // The zeroForOne parameter indicates swap direction (token0 for token1 when true). // Panics if sqrtPX96 or liquidity is zero. func getNextSqrtPriceFromInput( sqrtPX96, liquidity, amountIn *u256.Uint, zeroForOne bool, ) *u256.Uint { if sqrtPX96.IsZero() { panic(errSqrtPriceZero) } if liquidity.IsZero() { panic(errLiquidityZero) } if zeroForOne { return getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountIn, true) } return getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountIn, true) } // getNextSqrtPriceFromOutput calculates the next sqrt price after removing tokens from the pool, // using different rounding directions based on swap direction. // The zeroForOne parameter indicates swap direction (token0 for token1 when true). // Panics if sqrtPX96 or liquidity is zero. func getNextSqrtPriceFromOutput( sqrtPX96, liquidity, amountOut *u256.Uint, zeroForOne bool, ) *u256.Uint { if sqrtPX96.IsZero() { panic(errSqrtPriceZero) } if liquidity.IsZero() { panic(errLiquidityZero) } if zeroForOne { return getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountOut, false) } return getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountOut, false) } // getAmount0DeltaHelper calculates the absolute token0 amount difference between two price ranges, // automatically swapping inputs to ensure correct ordering. The roundUp parameter controls // rounding direction for the final result to ensure conservative AMM calculations. // Panics if sqrtRatioAX96 is zero. func getAmount0DeltaHelper( sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint, roundUp bool, ) *u256.Uint { if sqrtRatioAX96.Gt(sqrtRatioBX96) { sqrtRatioAX96, sqrtRatioBX96 = sqrtRatioBX96, sqrtRatioAX96 } // Use local variables for thread safety numerator := u256.Zero().Lsh(liquidity, Q96_RESOLUTION) difference := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) if sqrtRatioAX96.IsZero() { panic(errSqrtRatioAX96Zero) } if roundUp { intermediate := u256.MulDivRoundingUp(numerator, difference, sqrtRatioBX96) return u256.DivRoundingUp(intermediate, sqrtRatioAX96) } intermediate := u256.MulDiv(numerator, difference, sqrtRatioBX96) return u256.Zero().Div(intermediate, sqrtRatioAX96) } // getAmount1DeltaHelper calculates the absolute token1 amount difference between two price ranges, // automatically swapping inputs to ensure correct ordering. The roundUp parameter controls // rounding direction for the final result to ensure conservative AMM calculations. func getAmount1DeltaHelper( sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint, roundUp bool, ) *u256.Uint { if sqrtRatioAX96.Gt(sqrtRatioBX96) { sqrtRatioAX96, sqrtRatioBX96 = sqrtRatioBX96, sqrtRatioAX96 } // amount1 = liquidity * (sqrtB - sqrtA) / 2^96 // Use local variable for thread safety difference := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) if roundUp { return u256.MulDivRoundingUp(liquidity, difference, consts.Q96()) } return u256.MulDiv(liquidity, difference, consts.Q96()) } // GetAmount0Delta computes the signed token0 amount represented between two prices. // Positive liquidity rounds the amount up; negative liquidity returns a negative amount // rounded down after applying the magnitude. // // Parameters: // - sqrtRatioAX96: First price endpoint in Q64.96 square-root format. // - sqrtRatioBX96: Second price endpoint in Q64.96 square-root format. // - liquidity: Signed liquidity value; its sign determines the result sign and rounding. // // Returns: // - amount0Delta: Signed int256 token0 amount represented by the range. // // Panics if an input is nil or the computed magnitude cannot be represented by int256. func GetAmount0Delta( sqrtRatioAX96, sqrtRatioBX96 *u256.Uint, liquidity *i256.Int, ) *i256.Int { if sqrtRatioAX96 == nil || sqrtRatioBX96 == nil || liquidity == nil { panic(errGetAmount0DeltaNilInput) } if liquidity.IsNeg() { u := getAmount0DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), false) if u.Gt(consts.MaxInt256()) { // if u > (2**255 - 1), cannot cast to int256 panic(errAmount0DeltaOverflow) } // Convert to i256 and negate properly return i256.Zero().Neg(i256.FromUint256(u)) } u := getAmount0DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), true) if u.Gt(consts.MaxInt256()) { // if u > (2**255 - 1), cannot cast to int256 panic(errAmount0DeltaOverflow) } return i256.FromUint256(u) } // GetAmount1Delta computes the signed token1 amount represented between two prices. // Positive liquidity rounds the amount up; negative liquidity returns a negative amount // rounded down after applying the magnitude. // // Parameters: // - sqrtRatioAX96: First price endpoint in Q64.96 square-root format. // - sqrtRatioBX96: Second price endpoint in Q64.96 square-root format. // - liquidity: Signed liquidity value; its sign determines the result sign and rounding. // // Returns: // - amount1Delta: Signed int256 token1 amount represented by the range. // // Panics if an input is nil or the computed magnitude cannot be represented by int256. func GetAmount1Delta( sqrtRatioAX96, sqrtRatioBX96 *u256.Uint, liquidity *i256.Int, ) *i256.Int { if sqrtRatioAX96 == nil || sqrtRatioBX96 == nil || liquidity == nil { panic(errGetAmount1DeltaNilInput) } if liquidity.IsNeg() { u := getAmount1DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), false) if u.Gt(consts.MaxInt256()) { // if u > (2**255 - 1), cannot cast to int256 panic(errAmount1DeltaOverflow) } // Convert to i256 and negate properly return i256.Zero().Neg(i256.FromUint256(u)) } u := getAmount1DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), true) if u.Gt(consts.MaxInt256()) { // if u > (2**255 - 1), cannot cast to int256 panic(errAmount1DeltaOverflow) } return i256.FromUint256(u) }
- #20swap_math.gno
- #21package gnsmath import ( i256 "gno.land/p/gnoswap/int256/v1" u256 "gno.land/p/gnoswap/uint256/v1" ) // denominator represents 100% in the fee calculation basis (1,000,000 = 100%). // Fee calculations use this to convert feePips to actual percentages. // For example, feePips=3000 means 3000/1000000 = 0.3% fee. const denominator = uint64(1_000_000) // SwapMathComputeSwapStep computes one swap step within a single tick range. // It determines the next square-root price, input amount, output amount, and fee, // using exact-input or exact-output semantics from amountRemaining. // // Parameters: // - sqrtRatioCurrentX96: Current pool square-root price in Q96 fixed-point format. // - sqrtRatioTargetX96: Tick-boundary square-root price that this step may reach. // - liquidity: Non-negative liquidity active between the current and target prices. // - amountRemaining: Signed amount remaining; non-negative selects exact input, negative selects exact output. // - feePips: Fee rate in millionths of the input amount (3,000 represents 0.3%). // // Returns: // - sqrtRatioNextX96: Square-root price after applying this step, within the valid pool price bounds. // - amountIn: Amount of the input token consumed by this step, excluding the fee. // - amountOut: Amount of the output token produced by this step. // - feeAmount: Input-token fee charged for this step. // // Panics if an input pointer is nil, feePips is at least 1,000,000, arithmetic // overflows, or the resulting square-root price is outside the valid bounds. func SwapMathComputeSwapStep( sqrtRatioCurrentX96 *u256.Uint, sqrtRatioTargetX96 *u256.Uint, liquidity *u256.Uint, amountRemaining *i256.Int, feePips uint64, ) (*u256.Uint, *u256.Uint, *u256.Uint, *u256.Uint) { if sqrtRatioCurrentX96 == nil || sqrtRatioTargetX96 == nil || liquidity == nil || amountRemaining == nil { panic("SwapMathComputeSwapStep: input parameters cannot be nil") } // This function is publicly accessible and can be called by external users or contracts. // While the pool realm only uses predefined fee values (100, 500, 3000, 10000) which are safely within range, // external callers could potentially pass any feePips value. The fee calculation involves dividing by // (1000000 - feePips), so feePips must be strictly less than 1000000 to avoid division by zero. // This follows Uniswap V3's factory-level validation: require(fee < 1000000). if feePips >= denominator { panic("SwapMathComputeSwapStep: feePips must be less than 1000000") } // zeroForOne determines swap direction based on the relationship of current vs. target zeroForOne := sqrtRatioCurrentX96.Gte(sqrtRatioTargetX96) // POSITIVE == EXACT_IN => Estimated AmountOut // NEGATIVE == EXACT_OUT => Estimated AmountIn exactIn := !amountRemaining.IsNeg() amountRemainingAbs := amountRemaining.Abs() feeRateInPips := u256.NewUint(feePips) withoutFeeRateInPips := u256.NewUint(denominator - feePips) sqrtRatioNextX96 := u256.Zero() amountIn := u256.Zero() amountOut := u256.Zero() feeAmount := u256.Zero() if exactIn { // Handle EXACT_IN scenario as a separate function sqrtRatioNextX96, amountIn = handleExactIn( zeroForOne, sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, amountRemainingAbs, // use absolute value here withoutFeeRateInPips, ) } else { // Handle EXACT_OUT scenario as a separate function sqrtRatioNextX96, amountOut = handleExactOut( zeroForOne, sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, amountRemainingAbs, ) } // isMax checks if we've hit the boundary price (target) isMax := sqrtRatioTargetX96.Eq(sqrtRatioNextX96) // Calculate final amountIn, amountOut if needed if zeroForOne { // If isMax && exactIn, we already have the correct amountIn if !(isMax && exactIn) { amountIn = getAmount0DeltaHelper( sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, true, ) } // If isMax && !exactIn, we already have the correct amountOut if !(isMax && !exactIn) { amountOut = getAmount1DeltaHelper( sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, false, ) } } else { if !(isMax && exactIn) { amountIn = getAmount1DeltaHelper( sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, true, ) } if !(isMax && !exactIn) { amountOut = getAmount0DeltaHelper( sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, false, ) } } // If we're in EXACT_OUT mode but overcalculated 'amountOut' if !exactIn && amountOut.Gt(amountRemainingAbs) { amountOut = amountRemainingAbs } // Fee logic // If exactIn and we haven't hit the target, the difference is the fee // Else, compute fee from feePips if exactIn && !sqrtRatioNextX96.Eq(sqrtRatioTargetX96) { feeAmount = u256.Zero().Sub(amountRemainingAbs, amountIn) } else { feeAmount = u256.MulDivRoundingUp( amountIn, feeRateInPips, withoutFeeRateInPips, ) } // Final sanity check for resulting price if sqrtRatioNextX96.Lt(MIN_SQRT_RATIO()) || sqrtRatioNextX96.Gt(MAX_SQRT_RATIO()) { panic(errInvalidPoolSqrtPrice) } return sqrtRatioNextX96, amountIn, amountOut, feeAmount } // handleExactIn handles the EXACT_IN scenario for swaps, returning the next sqrt price and // a provisional amount. When the target price is reached, it returns the exact amount needed. // When the target is not reached, it returns the amount needed to reach the target (which will // be recalculated by the caller since we only moved partially). // This internal function processes swaps where the input amount is specified exactly. func handleExactIn( zeroForOne bool, sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, amountRemainingAbs, withoutFeeRateInPips *u256.Uint, ) (*u256.Uint, *u256.Uint) { amountRemainingLessFee := u256.MulDiv( amountRemainingAbs, withoutFeeRateInPips, u256.NewUint(denominator), ) amountIn := u256.Zero() if zeroForOne { amountIn = getAmount0DeltaHelper( sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, true, ) } else { amountIn = getAmount1DeltaHelper( sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, true, ) } if amountRemainingLessFee.Gte(amountIn) { return sqrtRatioTargetX96, amountIn } // We don't reach target price; use partial move nextSqrt := getNextSqrtPriceFromInput( sqrtRatioCurrentX96, liquidity, amountRemainingLessFee, zeroForOne, ) // Return the partially moved price and the amount to reach target (will be recalculated by caller) return nextSqrt, amountIn } // handleExactOut handles the EXACT_OUT scenario for swaps, returning the next sqrt price and // a provisional amount. When the target price is reached, it returns the exact amount produced. // When the target is not reached due to insufficient liquidity, it returns the amount that would // be produced if we reached the target (which will be recalculated by the caller). // This internal function processes swaps where the output amount is specified exactly. func handleExactOut( zeroForOne bool, sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, amountRemainingAbs *u256.Uint, ) (*u256.Uint, *u256.Uint) { amountOut := u256.Zero() if zeroForOne { amountOut = getAmount1DeltaHelper(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, false) } else { amountOut = getAmount0DeltaHelper(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, false) } // Fast path: if sufficient liquidity, use target price if amountRemainingAbs.Gte(amountOut) { return sqrtRatioTargetX96, amountOut } // Otherwise, partial move: compute next price from residual output amount // and return the amount to reach target (will be recalculated by caller) nextSqrt := getNextSqrtPriceFromOutput( sqrtRatioCurrentX96, liquidity, amountRemainingAbs, zeroForOne, ) return nextSqrt, amountOut }
- #22tick_math.gno
- #23package gnsmath import ( "errors" ufmt "gno.land/p/nt/ufmt/v0" "gno.land/p/gnoswap/consts/v1" i256 "gno.land/p/gnoswap/int256/v1" u256 "gno.land/p/gnoswap/uint256/v1" ) // Pre-calculated ratio constants for performance optimization. // // These were previously package-level vars (a slice plus 19 exposed pointers), // the exact "globally exposed mutable array" anti-pattern. They are now // constructors: each call returns freshly allocated values built from // little-endian [4]uint64 literals, so no caller shares a mutable instance and // no runtime decimal parsing happens. Values match Uniswap V3 exactly. // initialRatio returns the LSB-selected initial ratio. // absTick&0x1 != 0 selects ratio0 (0xfffcb933bd6fad37aa2d162d1a594001), // otherwise ratio1 (2^128). func initialRatio(odd bool) *u256.Uint { if odd { return &u256.Uint{12262481743371124737, 18445821805675392311, 0, 0} // 0xfffcb933bd6fad37aa2d162d1a594001 } return &u256.Uint{0, 0, 1, 0} // 0x100000000000000000000000000000000 (2^128) } // ratioConstants returns the bit-mask ratio constants in order (bit 1 to bit 19). func ratioConstants() []*u256.Uint { return []*u256.Uint{ {6459403834229662010, 18444899583751176498, 0, 0}, // 0xfff97272373d413259a46990580e213a (bit 1) {17226890335427755468, 18443055278223354162, 0, 0}, // 0xfff2e50f5f656932ef12357cf3c7fdcc (bit 2) {2032852871939366096, 18439367220385604838, 0, 0}, // 0xffe5caca7e10e4e61c3624eaa0941cd0 (bit 3) {14545316742740207172, 18431993317065449817, 0, 0}, // 0xffcb9843d60f6159c9db58835c926644 (bit 4) {5129152022828963008, 18417254355718160513, 0, 0}, // 0xff973b41fa98c081472e6896dfb254c0 (bit 5) {4894419605888772193, 18387811781193591352, 0, 0}, // 0xff2ea16466c96a3843ec78b326b52861 (bit 6) {1280255884321894483, 18329067761203520168, 0, 0}, // 0xfe5dee046a99a2a811c461f1969c3053 (bit 7) {15924666964335305636, 18212142134806087854, 0, 0}, // 0xfcbe86c7900a88aedcffc83b479aa3a4 (bit 8) {8010504389359918676, 17980523815641551639, 0, 0}, // 0xf987a7253ac413176f2b074cf7815e54 (bit 9) {10668036004952895731, 17526086738831147013, 0, 0}, // 0xf3392b0822b70005940c7a398e4b70f3 (bit 10) {4878133418470705625, 16651378430235024244, 0, 0}, // 0xe7159475a2c29b7443b29c7fa6e889d9 (bit 11) {9537173718739605541, 15030750278693429944, 0, 0}, // 0xd097f3bdfd2022b8845ad8f792aa5825 (bit 12) {9972618978014552549, 12247334978882834399, 0, 0}, // 0xa9f746462d870fdf8a65dc1f90e061e5 (bit 13) {10428997489610666743, 8131365268884726200, 0, 0}, // 0x70d869a156d2a1b890bb3df62baf32f7 (bit 14) {9305304367709015974, 3584323654723342297, 0, 0}, // 0x31be135f97d08fd981231505542fcfa6 (bit 15) {14301143598189091785, 696457651847595233, 0, 0}, // 0x9aa508b5b7a84e1c677de54f3e99bc9 (bit 16) {7393154844743099908, 26294789957452057, 0, 0}, // 0x5d6af8dedb81196699c329225ee604 (bit 17) {2209338891292245656, 37481735321082, 0, 0}, // 0x2216e584f5fa1ea926041bedfe98 (bit 18) {10518117631919034274, 76158723, 0, 0}, // 0x48a170391f7dc42444e8fa2 (bit 19) } } // Pre-computed constants for tick calculation - returned as fresh instances per call. func log2Multiplier() *i256.Int { return &i256.Int{11745905768312294533, 13863, 0, 0} } // 255738958999603826347141 func tickLowOffset() *i256.Int { return &i256.Int{6552757943157144234, 184476617836266586, 0, 0} } // 3402992956809132418596140100660247210 func tickHiOffset() *i256.Int { return &i256.Int{4998474450511881007, 15793544031827761793, 0, 0} } // 291339464771989622907027621153398088495 // oneLsh32 returns 1 << 32. func oneLsh32() *u256.Uint { return &u256.Uint{4294967296, 0, 0, 0} } // TickMathGetSqrtRatioAtTick calculates sqrt price ratio for given tick. // // Converts tick index to square root price in Q64.96 fixed-point format. // Based on Uniswap V3's mathematical formula: price = 1.0001^tick. // Uses bit manipulation for gas-efficient calculation. // // Parameters: // - tick: tick index in range [-887272, 887272] // // Returns: // - sqrtPriceX96: the Q64.96 square root of the token1/token0 price, rounded up // // Mathematical formula: // // sqrtPriceX96 = sqrt(1.0001^tick) * 2^96 // // Panics if tick outside valid range. // Critical for all price calculations in concentrated liquidity. func TickMathGetSqrtRatioAtTick(tick int32) *u256.Uint { assertValidTickRange(tick) absTick := abs(tick) // Initialize ratio based on LSB - exactly like Uniswap V3 ratio := initialRatio(absTick&0x1 != 0) temp := u256.Zero() masks := ratioConstants() // Apply bit masks using optimized loop - maintains exact same logic for i := 1; i < 20; i++ { if absTick&(1<<uint(i)) != 0 { // Use temporary variables to avoid memory allocation in hot path r := masks[i-1] temp, overflow := temp.MulOverflow(ratio, r) if overflow { panic(errors.New(errTickMathOverflow)) } ratio = ratio.Rsh(temp, 128) } } // Invert ratio for positive ticks if tick > 0 { ratio = temp.Div(consts.MaxUint256(), ratio) } // Convert from Q128.128 to Q128.96 with rounding up. // This divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96 upper := u256.Zero().Rsh(ratio, 32) // ratio >> 32 remainder := u256.Zero().Mod(ratio, oneLsh32()) // ratio % (1 << 32) // Round up: add 1 if remainder != 0 if !remainder.IsZero() { upper = u256.Zero().Add(upper, u256.One()) } return upper } // TickMathGetTickAtSqrtRatio calculates the tick index for a given square root price ratio. // // Converts a square root price ratio in Q64.96 format back to its tick index, // returning the greatest tick where TickMathGetSqrtRatioAtTick(tick) <= sqrtPriceX96. // For this inverse API, sqrtPriceX96 must be in `[MinSqrtRatio, MaxSqrtRatio)`; // the upper bound equals the max-tick output but is itself excluded. // // Parameters: // - sqrtPriceX96: square root price ratio in Q64.96 format within [MinSqrtRatio, MaxSqrtRatio) // // Returns: // - tick: the greatest tick whose calculated ratio is at most sqrtPriceX96 // // Algorithm: // 1. Scales ratio from Q64.96 to Q96.128 by left-shifting 32 bits // 2. Finds MSB (most significant bit) to determine magnitude // 3. Calculates log_2 using fixed-point arithmetic // 4. Converts log_2 to log_sqrt(1.0001) to get tick // 5. Returns appropriate tick based on bounds checking // // Panics if sqrtPriceX96 is nil or outside valid range [minSqrtRatio, maxSqrtRatio). // Critical for converting prices to ticks for position management. func TickMathGetTickAtSqrtRatio(sqrtPriceX96 *u256.Uint) int32 { if sqrtPriceX96 == nil { panic(newErrorWithDetail( errTickMathInvalidInput, "sqrtPriceX96 cannot be nil", )) } if sqrtPriceX96.Lt(consts.MinSqrtRatio()) || sqrtPriceX96.Gte(consts.MaxSqrtRatio()) { panic(newErrorWithDetail( errTickMathOutOfRange, ufmt.Sprintf("sqrtPriceX96(%s) is out of range", sqrtPriceX96.ToString()), )) } // Scale ratio by 32 bits to convert from Q64.96 to Q96.128 ratio := u256.Zero().Lsh(sqrtPriceX96, 32) // The validated ratio is nonzero; its bit length gives the MSB directly. msb := uint64(ratio.BitLen() - 1) // Adjust ratio based on MSB var r *u256.Uint if msb >= 128 { r = u256.Zero().Rsh(ratio, uint(msb-127)) } else { r = u256.Zero().Lsh(ratio, uint(127-msb)) } // Calculate log_2 using fixed-point arithmetic log2 := i256.NewInt(int64(msb) - 128) log2 = i256.Zero().Lsh(log2, 64) // Define temporary variables for optimization tempR := u256.Zero() tempF := u256.Zero() tempI256 := i256.Zero() // Optimized iterative calculation using loop - maintains exact same logic for i := 0; i < 14; i++ { tempR, overflow := tempR.MulOverflow(r, r) if overflow { panic(errors.New(errTickMathOverflow)) } r = tempR.Rsh(tempR, 127) tempF = tempF.Rsh(r, 128) tempI256 = i256.FromUint256(tempF) f := tempF tempI256 = tempI256.Lsh(tempI256, uint(63-i)) log2 = log2.Or(log2, tempI256) r = r.Rsh(r, uint(f.Uint64())) } // Calculate tick from log_sqrt10001 logSqrt10001, overflow := i256.Zero().MulOverflow(log2, log2Multiplier()) if overflow { panic(errors.New(errTickMathOverflow)) } // Calculate tick bounds tickLow := i256.Zero().Sub(logSqrt10001, tickLowOffset()) tickLow = tickLow.Rsh(tickLow, 128) tickLowInt32 := int32(tickLow.Int64()) tickHi := i256.Zero().Add(logSqrt10001, tickHiOffset()) tickHi = tickHi.Rsh(tickHi, 128) tickHiInt32 := int32(tickHi.Int64()) // Select the appropriate tick if tickLowInt32 == tickHiInt32 { return tickLowInt32 } if TickMathGetSqrtRatioAtTick(tickHiInt32).Lte(sqrtPriceX96) { return tickHiInt32 } return tickLowInt32 } // abs returns the absolute value of a signed 32-bit integer. // Used internally for tick math calculations to handle negative tick indices. func abs(x int32) int32 { if x < 0 { return -x } return x } // assertValidTickRange panics if tick is outside valid range [-887272, 887272]. func assertValidTickRange(tick int32) { if tick > maxTick { panic(newErrorWithDetail( errTickMathOutOfRange, ufmt.Sprintf("tick is out of range (larger than 887272), tick: %d", tick), )) } if tick < minTick { panic(newErrorWithDetail( errTickMathOutOfRange, ufmt.Sprintf("tick is out of range (smaller than -887272), tick: %d", tick), )) } }
- #24/gno.MemPackageType
Result log
msg:0,success:true,log:,events:[]