diff --git a/contract/r/gnoswap/common/liquidity_amounts.gno b/contract/r/gnoswap/common/liquidity_amounts.gno index 2b6db66e8..a655f5868 100644 --- a/contract/r/gnoswap/common/liquidity_amounts.gno +++ b/contract/r/gnoswap/common/liquidity_amounts.gno @@ -26,74 +26,6 @@ var ( zero = u256.Zero() ) -// 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 -} - -// toUint128 ensures the value fits within uint128 range. -// -// Validates and constrains a 256-bit unsigned integer to 128-bit range. -// Used for liquidity calculations where amounts must fit in compact storage. -// -// Parameters: -// - value: 256-bit unsigned integer to constrain -// -// Returns: -// - Masked value if exceeds MAX_UINT128 (2^128 - 1) -// - Original value if within range -// -// Panics if value is nil. -// Critical for preventing overflow in liquidity math. -func toUint128(value *u256.Uint) *u256.Uint { - if value == nil { - panic(newErrorWithDetail( - errInvalidInput, - "value is nil", - )) - } - - if value.Gt(maxUint128) { - return u256.Zero().And(value, q128Mask) - } - return value -} - -// safeConvertToUint128 safely ensures a *u256.Uint value fits within the uint128 range. -// -// This function verifies that the provided unsigned 256-bit integer does not exceed the maximum value for uint128 (`2^128 - 1`). -// If the value is within the uint128 range, it is returned as is; otherwise, the function triggers a panic. -// -// Parameters: -// - value (*u256.Uint): The unsigned 256-bit integer to be checked. -// -// Returns: -// - *u256.Uint: The same value if it is within the uint128 range. -// -// Panics: -// - If the value exceeds the maximum uint128 value (`2^128 - 1`), the function will panic with a descriptive error -// indicating the overflow and the original value. -// -// Notes: -// - The constant `MAX_UINT128` is defined as `340282366920938463463374607431768211455` (the largest uint128 value). -// - No actual conversion occurs since the function works directly with *u256.Uint types. -// -// Example: -// validUint128 := safeConvertToUint128(u256.MustFromDecimal("340282366920938463463374607431768211455")) // Valid -// safeConvertToUint128(u256.MustFromDecimal("340282366920938463463374607431768211456")) // Panics due to overflow -func safeConvertToUint128(value *u256.Uint) *u256.Uint { - if value.Gt(maxUint128) { - panic(ufmt.Sprintf( - "%v: amount(%s) overflows uint128 range", - errOverFlow, value.ToString())) - } - return value -} - // computeLiquidityForAmount0 calculates the liquidity for a given amount of token0. // // This function computes the maximum possible liquidity that can be provided for `token0` @@ -104,13 +36,16 @@ func safeConvertToUint128(value *u256.Uint) *u256.Uint { // - 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. // +// Notes: +// - The function assumes the input values are already in ascending order. +// // 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) + // sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) intermediate := u256.MulDiv(sqrtRatioAX96, sqrtRatioBX96, q96Uint) diff := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) @@ -142,12 +77,11 @@ func computeLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0 *u256.Uint // - 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. +// - The function assumes the input values are already in ascending order. // // 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( @@ -181,13 +115,13 @@ func computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1 *u256.Uint // - The function ensures that liquidity calculations handle edge cases when the current price // is outside the specified range by returning liquidity based on the dominant token. func GetLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1 *u256.Uint) (liquidity *u256.Uint) { - sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96.Clone(), sqrtRatioBX96.Clone()) + sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) if sqrtRatioX96.Lte(sqrtRatioAX96) { - liquidity = computeLiquidityForAmount0(sqrtRatioAX96.Clone(), sqrtRatioBX96.Clone(), amount0.Clone()) + liquidity = computeLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0) } else if sqrtRatioX96.Lt(sqrtRatioBX96) { - liquidity0 := computeLiquidityForAmount0(sqrtRatioX96.Clone(), sqrtRatioBX96.Clone(), amount0.Clone()) - liquidity1 := computeLiquidityForAmount1(sqrtRatioAX96.Clone(), sqrtRatioX96.Clone(), amount1.Clone()) + liquidity0 := computeLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0) + liquidity1 := computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1) if liquidity0.Lt(liquidity1) { liquidity = liquidity0 @@ -195,7 +129,7 @@ func GetLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, liquidity = liquidity1 } } else { - liquidity = computeLiquidityForAmount1(sqrtRatioAX96.Clone(), sqrtRatioBX96.Clone(), amount1.Clone()) + liquidity = computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1) } return liquidity } @@ -218,8 +152,8 @@ func GetLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, // - 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). +// - The function assumes the input values are already in ascending order. 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 zero } @@ -253,8 +187,8 @@ func computeAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Ui // to prevent division by zero. // - The calculation assumes sqrtRatioAX96 is always less than or equal to sqrtRatioBX96 after the initial // ascending order sorting. +// - The function assumes the input values are already in ascending order. func computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) *u256.Uint { - sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) if liquidity.IsZero() || sqrtRatioAX96.Eq(sqrtRatioBX96) { return zero } @@ -340,24 +274,98 @@ func LiquidityMathAddDelta(x *u256.Uint, y *i256.Int) *u256.Uint { panic("liquidity_math: x or y is nil") } + if y.IsZero() { + return x.Clone() + } + yAbs := y.Abs() + yNeg := y.IsNeg() + var z u256.Uint // Subtract or add based on the sign of y - if y.Lt(i256.Zero()) { - z := u256.Zero().Sub(x, yAbs) - if z.Gte(x) { + if yNeg { + _, underflow := z.SubOverflow(x, yAbs) + if underflow { panic(ufmt.Sprintf( "liquidity_math: underflow (x: %s, y: %s, z:%s)", x.ToString(), y.ToString(), z.ToString())) } - return z + return &z } - z := u256.Zero().Add(x, yAbs) - if z.Lt(x) { + _, overflow := z.AddOverflow(x, yAbs) + if overflow { panic(ufmt.Sprintf( "liquidity_math: overflow (x: %s, y: %s, z:%s)", x.ToString(), y.ToString(), z.ToString())) } - return z + 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 +} + +// toUint128 ensures the value fits within uint128 range. +// +// Validates and constrains a 256-bit unsigned integer to 128-bit range. +// Used for liquidity calculations where amounts must fit in compact storage. +// +// Parameters: +// - value: 256-bit unsigned integer to constrain +// +// Returns: +// - Masked value if exceeds MAX_UINT128 (2^128 - 1) +// - Original value if within range +// +// Panics if value is nil. +// Critical for preventing overflow in liquidity math. +func toUint128(value *u256.Uint) *u256.Uint { + if value == nil { + panic(newErrorWithDetail( + errInvalidInput, + "value is nil", + )) + } + + if value.Gt(maxUint128) { + return u256.Zero().And(value, q128Mask) + } + return value +} + +// safeConvertToUint128 safely ensures a *u256.Uint value fits within the uint128 range. +// +// This function verifies that the provided unsigned 256-bit integer does not exceed the maximum value for uint128 (`2^128 - 1`). +// If the value is within the uint128 range, it is returned as is; otherwise, the function triggers a panic. +// +// Parameters: +// - value (*u256.Uint): The unsigned 256-bit integer to be checked. +// +// Returns: +// - *u256.Uint: The same value if it is within the uint128 range. +// +// Panics: +// - If the value exceeds the maximum uint128 value (`2^128 - 1`), the function will panic with a descriptive error +// indicating the overflow and the original value. +// +// Notes: +// - The constant `MAX_UINT128` is defined as `340282366920938463463374607431768211455` (the largest uint128 value). +// - No actual conversion occurs since the function works directly with *u256.Uint types. +// +// Example: +// validUint128 := safeConvertToUint128(u256.MustFromDecimal("340282366920938463463374607431768211455")) // Valid +// safeConvertToUint128(u256.MustFromDecimal("340282366920938463463374607431768211456")) // Panics due to overflow +func safeConvertToUint128(value *u256.Uint) *u256.Uint { + if value.Gt(maxUint128) { + panic(ufmt.Sprintf( + "%v: amount(%s) overflows uint128 range", + errOverFlow, value.ToString())) + } + return value } diff --git a/contract/r/gnoswap/common/liquidity_amounts_runtime_metrics_test.gno b/contract/r/gnoswap/common/liquidity_amounts_runtime_metrics_test.gno new file mode 100644 index 000000000..64b048886 --- /dev/null +++ b/contract/r/gnoswap/common/liquidity_amounts_runtime_metrics_test.gno @@ -0,0 +1,418 @@ +package common + +import ( + "runtime" + "strconv" + "strings" + "testing" + "time" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +// readAllocatorBytesForLiquidity parses runtime.MemStats() into the current allocated bytes. +func readAllocatorBytesForLiquidity(t *testing.T) int64 { + t.Helper() + stats := runtime.MemStats() + if stats == "nil allocator" { + return 0 + } + if !strings.HasPrefix(stats, "Allocator{") || !strings.HasSuffix(stats, "}") { + t.Fatalf("unexpected runtime.MemStats output: %q", stats) + } + body := strings.TrimSuffix(strings.TrimPrefix(stats, "Allocator{"), "}") + parts := strings.Split(body, ", ") + if len(parts) != 2 { + t.Fatalf("unexpected runtime.MemStats content: %q", stats) + } + var ( + bytes int64 + found bool + ) + for _, part := range parts { + fields := strings.Split(part, ":") + if len(fields) != 2 { + t.Fatalf("unexpected runtime.MemStats pair %q", part) + } + if fields[0] == "bytes" { + val, err := strconv.ParseInt(fields[1], 10, 64) + if err != nil { + t.Fatalf("failed to parse bytes from %q: %v", part, err) + } + bytes = val + found = true + break + } + } + if !found { + t.Fatalf("bytes key not found in runtime.MemStats output: %q", stats) + } + return bytes +} + +type LiquidityMetricResult struct { + Name string + Iterations int + DurationNs int64 + AllocDelta int64 +} + +// runLiquidityMetric executes fn the given number of iterations while logging elapsed time and allocation deltas. +func runLiquidityMetric(t *testing.T, name string, iterations int, fn func()) LiquidityMetricResult { + t.Helper() + + // Skip GC if allocator is nil to avoid panic + stats := runtime.MemStats() + if stats != "nil allocator" { + runtime.GC() + } + beforeBytes := readAllocatorBytesForLiquidity(t) + start := time.Now() + for i := 0; i < iterations; i++ { + fn() + } + elapsed := time.Since(start) + afterBytes := readAllocatorBytesForLiquidity(t) + + return LiquidityMetricResult{ + Name: name, + Iterations: iterations, + DurationNs: elapsed.Nanoseconds(), + AllocDelta: afterBytes - beforeBytes, + } +} + +func TestLiquidityAmountsRuntimeMetrics(t *testing.T) { + const iterations = 200 + var results []LiquidityMetricResult + + // Test input values - common price ratios in Q64.96 format + sqrtRatioX96Current := u256.MustFromDecimal("79228162514264337593543950336") // sqrt(1) = 1.0 + sqrtRatioX96Low := u256.MustFromDecimal("56022770974786139918731938227") // sqrt(0.5) ≈ 0.707 + sqrtRatioX96High := u256.MustFromDecimal("112045541949572279837463876454") // sqrt(2) ≈ 1.414 + sqrtRatioX96VeryLow := u256.MustFromDecimal("7922816251426433759") // sqrt(0.00001) + sqrtRatioX96VeryHigh := u256.MustFromDecimal("792281625142643375935439503360") // sqrt(100000) + + // Edge case ratios + sqrtRatioX96Min := u256.MustFromDecimal("4295128739") // Min sqrt ratio + sqrtRatioX96Max := u256.MustFromDecimal("1461446703485210103287273052203988822378723970341") // Max sqrt ratio + + // Liquidity amounts + liquidityMedium := u256.MustFromDecimal("1000000000000") // 1T + liquidityLarge := u256.MustFromDecimal("1000000000000000000") // 1Q + liquidityMax := u256.MustFromDecimal(MAX_UINT128) // Max uint128 + liquidityHalfMax := u256.MustFromDecimal("170141183460469231731687303715884105727") // Half of max uint128 + + // Token amounts + amount0Small := u256.MustFromDecimal("100000") + amount0Medium := u256.MustFromDecimal("100000000000") + amount0Large := u256.MustFromDecimal("100000000000000000") + amount1Small := u256.MustFromDecimal("50000") + amount1Medium := u256.MustFromDecimal("50000000000") + amount1Large := u256.MustFromDecimal("50000000000000000") + + // Delta values for LiquidityMathAddDelta + deltaPositiveSmall := i256.MustFromDecimal("1000000") + deltaPositiveLarge := i256.MustFromDecimal("1000000000000000") + deltaNegativeSmall := i256.MustFromDecimal("-1000000") + deltaNegativeLarge := i256.MustFromDecimal("-1000000000000000") + + tests := []struct { + name string + run func(t *testing.T) LiquidityMetricResult + }{ + // toAscendingOrder tests + { + name: "toAscendingOrder_AlreadyOrdered", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "toAscendingOrder_AlreadyOrdered", iterations, func() { + _, _ = toAscendingOrder(sqrtRatioX96Low, sqrtRatioX96High) + }) + }, + }, + { + name: "toAscendingOrder_NeedsSwap", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "toAscendingOrder_NeedsSwap", iterations, func() { + _, _ = toAscendingOrder(sqrtRatioX96High, sqrtRatioX96Low) + }) + }, + }, + // toUint128 tests + { + name: "toUint128_WithinRange", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "toUint128_WithinRange", iterations, func() { + _ = toUint128(liquidityMedium) + }) + }, + }, + { + name: "toUint128_MaxValue", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "toUint128_MaxValue", iterations, func() { + _ = toUint128(liquidityMax) + }) + }, + }, + // safeConvertToUint128 tests + { + name: "safeConvertToUint128_Valid", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "safeConvertToUint128_Valid", iterations, func() { + _ = safeConvertToUint128(liquidityHalfMax) + }) + }, + }, + // computeLiquidityForAmount0 tests + { + name: "computeLiquidityForAmount0_Normal", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "computeLiquidityForAmount0_Normal", iterations, func() { + _ = computeLiquidityForAmount0(sqrtRatioX96Low, sqrtRatioX96High, amount0Medium) + }) + }, + }, + { + name: "computeLiquidityForAmount0_SmallRange", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "computeLiquidityForAmount0_SmallRange", iterations, func() { + sqrtLow := u256.MustFromDecimal("79228162514264337593543950336") + sqrtHigh := u256.MustFromDecimal("79300000000000000000000000000") + _ = computeLiquidityForAmount0(sqrtLow, sqrtHigh, amount0Small) + }) + }, + }, + { + name: "computeLiquidityForAmount0_LargeRange", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "computeLiquidityForAmount0_LargeRange", iterations, func() { + _ = computeLiquidityForAmount0(sqrtRatioX96VeryLow, sqrtRatioX96VeryHigh, amount0Large) + }) + }, + }, + // computeLiquidityForAmount1 tests + { + name: "computeLiquidityForAmount1_Normal", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "computeLiquidityForAmount1_Normal", iterations, func() { + _ = computeLiquidityForAmount1(sqrtRatioX96Low, sqrtRatioX96High, amount1Medium) + }) + }, + }, + { + name: "computeLiquidityForAmount1_SmallRange", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "computeLiquidityForAmount1_SmallRange", iterations, func() { + sqrtLow := u256.MustFromDecimal("79228162514264337593543950336") + sqrtHigh := u256.MustFromDecimal("79300000000000000000000000000") + _ = computeLiquidityForAmount1(sqrtLow, sqrtHigh, amount1Small) + }) + }, + }, + { + name: "computeLiquidityForAmount1_LargeRange", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "computeLiquidityForAmount1_LargeRange", iterations, func() { + _ = computeLiquidityForAmount1(sqrtRatioX96VeryLow, sqrtRatioX96VeryHigh, amount1Large) + }) + }, + }, + // GetLiquidityForAmounts tests + { + name: "GetLiquidityForAmounts_CurrentBelowRange", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "GetLiquidityForAmounts_CurrentBelowRange", iterations, func() { + _ = GetLiquidityForAmounts(sqrtRatioX96Low, sqrtRatioX96Current, sqrtRatioX96High, amount0Medium, amount1Medium) + }) + }, + }, + { + name: "GetLiquidityForAmounts_CurrentInRange", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "GetLiquidityForAmounts_CurrentInRange", iterations, func() { + _ = GetLiquidityForAmounts(sqrtRatioX96Current, sqrtRatioX96Low, sqrtRatioX96High, amount0Medium, amount1Medium) + }) + }, + }, + { + name: "GetLiquidityForAmounts_CurrentAboveRange", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "GetLiquidityForAmounts_CurrentAboveRange", iterations, func() { + _ = GetLiquidityForAmounts(sqrtRatioX96High, sqrtRatioX96Low, sqrtRatioX96Current, amount0Medium, amount1Medium) + }) + }, + }, + { + name: "GetLiquidityForAmounts_EdgeCase_MinMax", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "GetLiquidityForAmounts_EdgeCase_MinMax", iterations, func() { + _ = GetLiquidityForAmounts(sqrtRatioX96Current, sqrtRatioX96Min, sqrtRatioX96Max, amount0Small, amount1Small) + }) + }, + }, + // computeAmount0ForLiquidity tests + { + name: "computeAmount0ForLiquidity_Normal", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "computeAmount0ForLiquidity_Normal", iterations, func() { + _ = computeAmount0ForLiquidity(sqrtRatioX96Low, sqrtRatioX96High, liquidityMedium) + }) + }, + }, + { + name: "computeAmount0ForLiquidity_LargeLiquidity", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "computeAmount0ForLiquidity_LargeLiquidity", iterations, func() { + _ = computeAmount0ForLiquidity(sqrtRatioX96Low, sqrtRatioX96High, liquidityLarge) + }) + }, + }, + { + name: "computeAmount0ForLiquidity_ZeroLiquidity", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "computeAmount0ForLiquidity_ZeroLiquidity", iterations, func() { + _ = computeAmount0ForLiquidity(sqrtRatioX96Low, sqrtRatioX96High, u256.Zero()) + }) + }, + }, + // computeAmount1ForLiquidity tests + { + name: "computeAmount1ForLiquidity_Normal", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "computeAmount1ForLiquidity_Normal", iterations, func() { + _ = computeAmount1ForLiquidity(sqrtRatioX96Low, sqrtRatioX96High, liquidityMedium) + }) + }, + }, + { + name: "computeAmount1ForLiquidity_LargeLiquidity", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "computeAmount1ForLiquidity_LargeLiquidity", iterations, func() { + _ = computeAmount1ForLiquidity(sqrtRatioX96Low, sqrtRatioX96High, liquidityLarge) + }) + }, + }, + { + name: "computeAmount1ForLiquidity_ZeroLiquidity", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "computeAmount1ForLiquidity_ZeroLiquidity", iterations, func() { + _ = computeAmount1ForLiquidity(sqrtRatioX96Low, sqrtRatioX96High, u256.Zero()) + }) + }, + }, + // GetAmountsForLiquidity tests + { + name: "GetAmountsForLiquidity_CurrentBelowRange", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "GetAmountsForLiquidity_CurrentBelowRange", iterations, func() { + _, _ = GetAmountsForLiquidity(sqrtRatioX96Low, sqrtRatioX96Current, sqrtRatioX96High, liquidityMedium) + }) + }, + }, + { + name: "GetAmountsForLiquidity_CurrentInRange", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "GetAmountsForLiquidity_CurrentInRange", iterations, func() { + _, _ = GetAmountsForLiquidity(sqrtRatioX96Current, sqrtRatioX96Low, sqrtRatioX96High, liquidityMedium) + }) + }, + }, + { + name: "GetAmountsForLiquidity_CurrentAboveRange", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "GetAmountsForLiquidity_CurrentAboveRange", iterations, func() { + _, _ = GetAmountsForLiquidity(sqrtRatioX96High, sqrtRatioX96Low, sqrtRatioX96Current, liquidityMedium) + }) + }, + }, + { + name: "GetAmountsForLiquidity_ZeroLiquidity", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "GetAmountsForLiquidity_ZeroLiquidity", iterations, func() { + _, _ = GetAmountsForLiquidity(sqrtRatioX96Current, sqrtRatioX96Low, sqrtRatioX96High, u256.Zero()) + }) + }, + }, + { + name: "GetAmountsForLiquidity_MaxLiquidity", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "GetAmountsForLiquidity_MaxLiquidity", iterations, func() { + _, _ = GetAmountsForLiquidity(sqrtRatioX96Current, sqrtRatioX96Low, sqrtRatioX96High, liquidityMax) + }) + }, + }, + // LiquidityMathAddDelta tests + { + name: "LiquidityMathAddDelta_AddSmall", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "LiquidityMathAddDelta_AddSmall", iterations, func() { + _ = LiquidityMathAddDelta(liquidityMedium, deltaPositiveSmall) + }) + }, + }, + { + name: "LiquidityMathAddDelta_AddLarge", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "LiquidityMathAddDelta_AddLarge", iterations, func() { + _ = LiquidityMathAddDelta(liquidityMedium, deltaPositiveLarge) + }) + }, + }, + { + name: "LiquidityMathAddDelta_SubtractSmall", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "LiquidityMathAddDelta_SubtractSmall", iterations, func() { + _ = LiquidityMathAddDelta(liquidityMedium, deltaNegativeSmall) + }) + }, + }, + { + name: "LiquidityMathAddDelta_SubtractLarge", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "LiquidityMathAddDelta_SubtractLarge", iterations, func() { + _ = LiquidityMathAddDelta(liquidityLarge, deltaNegativeLarge) + }) + }, + }, + // Combined/Complex operations + { + name: "Combined_LiquidityRoundtrip", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "Combined_LiquidityRoundtrip", iterations, func() { + // Get liquidity from amounts, then get amounts back + liq := GetLiquidityForAmounts(sqrtRatioX96Current, sqrtRatioX96Low, sqrtRatioX96High, amount0Medium, amount1Medium) + _, _ = GetAmountsForLiquidity(sqrtRatioX96Current, sqrtRatioX96Low, sqrtRatioX96High, liq) + }) + }, + }, + { + name: "Combined_MultipleAddDelta", + run: func(t *testing.T) LiquidityMetricResult { + return runLiquidityMetric(t, "Combined_MultipleAddDelta", iterations, func() { + liq := liquidityMedium + liq = LiquidityMathAddDelta(liq, deltaPositiveSmall) + liq = LiquidityMathAddDelta(liq, deltaNegativeSmall) + _ = LiquidityMathAddDelta(liq, deltaPositiveLarge) + }) + }, + }, + } + + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + result := test.run(t) + results = append(results, result) + }) + } + + // Print results as markdown table + t.Log("\n## Liquidity Amounts Runtime Metrics Results\n") + t.Log("| Function | Iterations | Duration (ns) | Alloc Delta (bytes) |") + t.Log("|----------|------------|---------------|---------------------|") + for _, result := range results { + t.Logf("| %s | %d | %d | %d |", result.Name, result.Iterations, result.DurationNs, result.AllocDelta) + } +} diff --git a/contract/r/gnoswap/common/liquidity_amounts_test.gno b/contract/r/gnoswap/common/liquidity_amounts_test.gno index ef2b961bc..1604b52a7 100644 --- a/contract/r/gnoswap/common/liquidity_amounts_test.gno +++ b/contract/r/gnoswap/common/liquidity_amounts_test.gno @@ -263,23 +263,28 @@ func TestLiquidityAmounts_ComputeLiquidityForAmount1(t *testing.T) { expected: "66666666", expectedPanic: false, }, + // If ordering were performed inside the function, it should return 66666666, + // which is the same as the "Large liquidity calculation" case. + // + // However, since input values always come in as sorted pairs before this function is called, + // there is no need to perform additional sorting internally. + { + name: "Large liquidity calculation in reverse price order", + sqrtRatioAX96: new(u256.Uint).Mul(q96, u256.NewUint(16)), + sqrtRatioBX96: q96, + amount1: u256.MustFromDecimal("1000000000"), + expected: "0", + expectedPanic: false, + }, { - name: "Large liquidity calculation in reverse price order", - sqrtRatioAX96: new(u256.Uint).Mul(q96, u256.NewUint(16)), - sqrtRatioBX96: q96, - amount1: u256.MustFromDecimal("1000000000"), - expected: "66666666", - expectedPanic: false, - }, - { - name: "Result is greater than u128 - overflow", - sqrtRatioAX96: u256.Zero(), - sqrtRatioBX96: u256.MustFromDecimal("1"), - amount1: u256.MustFromDecimal("340282366920938463463374607431768211456"), // uint256 max value + 1 - expected: "0", - expectedPanic: true, - expectedPanicMsg: "[GNOSWAP-COMMON-009] overflow: amount(26959946667150639794667015087019630673637144422540572481103610249216) overflows uint128 range", - }, + name: "Result is greater than u128 - overflow", + sqrtRatioAX96: u256.Zero(), + sqrtRatioBX96: u256.MustFromDecimal("1"), + amount1: u256.MustFromDecimal("340282366920938463463374607431768211456"), // uint256 max value + 1 + expected: "0", + expectedPanic: true, + expectedPanicMsg: "[GNOSWAP-COMMON-009] overflow: amount(26959946667150639794667015087019630673637144422540572481103610249216) overflows uint128 range", + }, } for _, tt := range tests { @@ -476,11 +481,11 @@ func TestLiquidityAmounts_ComputeAmount1ForLiquidity(t *testing.T) { expectedAmount: "15000000000000000000", // (16-1) * 1e18 = 15 * 1e18 }, { - name: "Descending Ratios (Order Correction)", + name: "Descending Ratios", sqrtRatioAX96: new(u256.Uint).Mul(q96, u256.NewUint(8)), sqrtRatioBX96: q96, liquidity: u256.NewUint(500000), - expectedAmount: "3500000", // (8-1)*500000 + expectedAmount: "730750818665451459101842416358141509827966271484500000", }, } @@ -580,10 +585,10 @@ func TestLiquidityAmounts_GetAmountsForLiquidity(t *testing.T) { func TestLiquidityAmounts_ExtremeRatioValues(t *testing.T) { tests := []struct { - name string - testFunc func() string - expected string - shouldNotPanic bool + name string + testFunc func() string + expected string + shouldNotPanic bool }{ { name: "Zero sqrtRatio handling",