Repository: osmosis-labs/osmosis (go)

Issue:
Delete osmomath.BigDec.ApproxRoot
### Summary

As far as I can tell, this is only used to derive some constants in a stableswap test. We can just hardcode those constants, and get this code out of the repo.

### Problem Definition

.

### Proposed Feature

Delete code

Pull request:
Improve stableswap constant declarations and add standalone ApproxRoot function
Description

  This PR addresses #8007 by improving the constants in the stableswap module and adding a standalone ApproxRoot function to avoid
  dependency on osmomath's implementation.

  The changes include:

  1. Replace calculated constants with exact decimal values
    - Replace runtime calculations with high-precision string literals
    - Add descriptive comments for each constant
    - Improve code readability and precision
  2. Add standalone ApproxRoot function
    - Copy the root approximation logic directly into the stableswap module
    - Remove dependency on osmomath's internal implementation
    - Ensure identical functionality and behavior
  3. Improved documentation
    - Add meaningful comments to all constants
    - Document the mathematical meaning and approximate values
    - Include string representations for reference

  Benefits

  - Decoupling: Reduces tight coupling between osmomath and stableswap modules
  - Stability: Protects against potential changes in osmomath's implementation
  - Performance: Removes runtime calculations for constants
  - Precision: Uses exact values instead of approximations for known constants
  - Maintainability: Easier to understand and modify each module independently

  Related Issues

  Closes #8007

Patch:
diff --git a/osmomath/decimal.go b/osmomath/decimal.go
index 17241a591..ca2f15bad 100644
--- a/osmomath/decimal.go
+++ b/osmomath/decimal.go
@@ -534,67 +534,12 @@ func (d BigDec) QuoInt64(i int64) BigDec {
 	return BigDec{mul}
 }
 
-// ApproxRoot returns an approximate estimation of a Dec's positive real nth root
-// using Newton's method (where n is positive). The algorithm starts with some guess and
-// computes the sequence of improved guesses until an answer converges to an
-// approximate answer.  It returns `|d|.ApproxRoot() * -1` if input is negative.
-// A maximum number of 100 iterations is used a backup boundary condition for
-// cases where the answer never converges enough to satisfy the main condition.
-func (d BigDec) ApproxRoot(root uint64) (guess BigDec, err error) {
-	defer func() {
-		if r := recover(); r != nil {
-			var ok bool
-			err, ok = r.(error)
-			if !ok {
-				err = errors.New("out of bounds")
-			}
-		}
-	}()
-
-	if d.IsNegative() {
-		absRoot, err := d.MulInt64(-1).ApproxRoot(root)
-		return absRoot.MulInt64(-1), err
-	}
-
-	if root == 1 || d.IsZero() || d.Equal(oneBigDec) {
-		return d, nil
-	}
-
-	if root == 0 {
-		return OneBigDec(), nil
-	}
-
-	rootInt := NewBigIntFromUint64(root)
-	guess, delta := OneBigDec(), OneBigDec()
-
-	for iter := 0; delta.Abs().GT(SmallestBigDec()) && iter < maxApproxRootIterations; iter++ {
-		prev := guess.PowerInteger(root - 1)
-		if prev.IsZero() {
-			prev = SmallestBigDec()
-		}
-		delta = d.Quo(prev)
-		delta.SubMut(guess)
-		delta = delta.QuoInt(rootInt)
-
-		guess = guess.Add(delta)
-	}
-
-	return guess, nil
-}
-
 func assertMaxBitLen(i *big.Int) {
 	if i.BitLen() > maxDecBitLen {
 		panic("Int overflow")
 	}
 }
 
-// ApproxSqrt is a wrapper around ApproxRoot for the common special case
-// of finding the square root of a number. It returns -(sqrt(abs(d)) if input is negative.
-// TODO: Optimize this to be faster just using native big int sqrt.
-func (d BigDec) ApproxSqrt() (BigDec, error) {
-	return d.ApproxRoot(2)
-}
-
 // is integer, e.g. decimals are zero
 func (d BigDec) IsInteger() bool {
 	return new(big.Int).Rem(d.i, defaultBigDecPrecisionReuse).Sign() == 0


Test patch:
diff --git a/osmomath/decimal_test.go b/osmomath/decimal_test.go
index e9beabefd..98e8d2a16 100644
--- a/osmomath/decimal_test.go
+++ b/osmomath/decimal_test.go
@@ -665,55 +665,6 @@ func (s *decimalTestSuite) TestDecCeil() {
 	}
 }
 
-func (s *decimalTestSuite) TestApproxRoot() {
-	testCases := []struct {
-		input    osmomath.BigDec
-		root     uint64
-		expected osmomath.BigDec
-	}{
-		{osmomath.OneBigDec(), 10, osmomath.OneBigDec()},                                                                               // 1.0 ^ (0.1) => 1.0
-		{osmomath.NewBigDecWithPrec(25, 2), 2, osmomath.NewBigDecWithPrec(5, 1)},                                                       // 0.25 ^ (0.5) => 0.5
-		{osmomath.NewBigDecWithPrec(4, 2), 2, osmomath.NewBigDecWithPrec(2, 1)},                                                        // 0.04 ^ (0.5) => 0.2
-		{osmomath.NewBigDecFromInt(osmomath.NewBigInt(27)), 3, osmomath.NewBigDecFromInt(osmomath.NewBigInt(3))},                       // 27 ^ (1/3) => 3
-		{osmomath.NewBigDecFromInt(osmomath.NewBigInt(-81)), 4, osmomath.NewBigDecFromInt(osmomath.NewBigInt(-3))},                     // -81 ^ (0.25) => -3
-		{osmomath.NewBigDecFromInt(osmomath.NewBigInt(2)), 2, osmomath.MustNewBigDecFromStr("1.414213562373095048801688724209698079")}, // 2 ^ (0.5) => 1.414213562373095048801688724209698079
-		{osmomath.NewBigDecWithPrec(1005, 3), 31536000, osmomath.MustNewBigDecFromStr("1.000000000158153903837946258002096839")},       // 1.005 ^ (1/31536000) ≈ 1.000000000158153903837946258002096839
-		{osmomath.SmallestBigDec(), 2, osmomath.NewBigDecWithPrec(1, 18)},                                                              // 1e-36 ^ (0.5) => 1e-18
-		{osmomath.SmallestBigDec(), 3, osmomath.MustNewBigDecFromStr("0.000000000001000000000000000002431786")},                        // 1e-36 ^ (1/3) => 1e-12
-		{osmomath.NewBigDecWithPrec(1, 8), 3, osmomath.MustNewBigDecFromStr("0.002154434690031883721759293566519280")},                 // 1e-8 ^ (1/3) ≈ 0.002154434690031883721759293566519
-	}
-
-	// In the case of 1e-8 ^ (1/3), the result repeats every 5 iterations starting from iteration 24
-	// (i.e. 24, 29, 34, ... give the same result) and never converges enough. The maximum number of
-	// iterations (100) causes the result at iteration 100 to be returned, regardless of convergence.
-
-	for i, tc := range testCases {
-		res, err := tc.input.ApproxRoot(tc.root)
-		s.Require().NoError(err)
-		s.Require().True(tc.expected.Sub(res).AbsMut().LTE(osmomath.SmallestBigDec()), "unexpected result for test case %d, input: %v", i, tc.input)
-	}
-}
-
-func (s *decimalTestSuite) TestApproxSqrt() {
-	testCases := []struct {
-		input    osmomath.BigDec
-		expected osmomath.BigDec
-	}{
-		{osmomath.OneBigDec(), osmomath.OneBigDec()},                                                                                // 1.0 => 1.0
-		{osmomath.NewBigDecWithPrec(25, 2), osmomath.NewBigDecWithPrec(5, 1)},                                                       // 0.25 => 0.5
-		{osmomath.NewBigDecWithPrec(4, 2), osmomath.NewBigDecWithPrec(2, 1)},                                                        // 0.09 => 0.3
-		{osmomath.NewBigDecFromInt(osmomath.NewBigInt(9)), osmomath.NewBigDecFromInt(osmomath.NewBigInt(3))},                        // 9 => 3
-		{osmomath.NewBigDecFromInt(osmomath.NewBigInt(-9)), osmomath.NewBigDecFromInt(osmomath.NewBigInt(-3))},                      // -9 => -3
-		{osmomath.NewBigDecFromInt(osmomath.NewBigInt(2)), osmomath.MustNewBigDecFromStr("1.414213562373095048801688724209698079")}, // 2 => 1.414213562373095048801688724209698079
-	}
-
-	for i, tc := range testCases {
-		res, err := tc.input.ApproxSqrt()
-		s.Require().NoError(err)
-		s.Require().Equal(tc.expected, res, "unexpected result for test case %d, input: %v", i, tc.input)
-	}
-}
-
 func (s *decimalTestSuite) TestDecEncoding() {
 	testCases := []struct {
 		input   osmomath.BigDec
diff --git a/x/gamm/pool-models/stableswap/amm_test.go b/x/gamm/pool-models/stableswap/amm_test.go
index 568496019..d5c2d5f16 100644
--- a/x/gamm/pool-models/stableswap/amm_test.go
+++ b/x/gamm/pool-models/stableswap/amm_test.go
@@ -1,6 +1,7 @@
 package stableswap
 
 import (
+	"errors"
 	"fmt"
 	"math/big"
 	"math/rand"
@@ -20,13 +21,20 @@ import (
 )
 
 var (
-	cubeRootTwo, _        = osmomath.NewBigDec(2).ApproxRoot(3)
-	threeRootTwo, _       = osmomath.NewBigDec(3).ApproxRoot(2)
-	cubeRootThree, _      = osmomath.NewBigDec(3).ApproxRoot(3)
-	threeCubeRootTwo      = cubeRootTwo.MulInt64(3)
-	cubeRootSixSquared, _ = (osmomath.NewBigDec(6).MulInt64(6)).ApproxRoot(3)
-	twoCubeRootThree      = cubeRootThree.MulInt64(2)
-	twentySevenRootTwo, _ = osmomath.NewBigDec(27).ApproxRoot(2)
+	// Cube root of 2: 2^(1/3) ≈ 1.259921049894873
+	cubeRootTwo, _ = osmomath.NewBigDecFromStr("1.259921049894873164767210607278228351")
+	// Square root of 3: 3^(1/2) ≈ 1.732050807568877
+	threeRootTwo, _ = osmomath.NewBigDecFromStr("1.732050807568877293527446341505872367")
+	// Cube root of 3: 3^(1/3) ≈ 1.442249570307408
+	cubeRootThree, _ = osmomath.NewBigDecFromStr("1.442249570307408382321638310780109589")
+	// 3 * cubeRootTwo ≈ 3.779763149684619
+	threeCubeRootTwo, _ = osmomath.NewBigDecFromStr("3.779763149684619494301631821834685053")
+	// Cube root of 36: (6*6)^(1/3) ≈ 3.3019272
+	cubeRootSixSquared, _ = osmomath.NewBigDecFromStr("3.301927248894626683874609952409084957")
+	// 2 * cubeRootThree ≈ 2.884499140614817
+	twoCubeRootThree, _ = osmomath.NewBigDecFromStr("2.884499140614816764643276621560219178")
+	// Square root of 27: 27^(1/2) ≈ 5.196152422706632
+	twentySevenRootTwo, _ = osmomath.NewBigDecFromStr("5.196152422706631880582339024517617101")
 )
 
 // CFMMTestCase defines a testcase for stableswap pools
@@ -513,13 +521,13 @@ func solveCfmmDirect(xReserve, yReserve, yIn osmomath.BigDec) osmomath.BigDec {
 	scaled_y4_quo_k2 := scaled_y4_quo_k.Mul(scaled_y4_quo_k)
 
 	// let sqrt_term = sqrt(1 + scaled_y4_quo_k2)
-	sqrt_term, err := (osmomath.OneBigDec().Add(scaled_y4_quo_k2)).ApproxRoot(2)
+	sqrt_term, err := ApproxRoot(osmomath.OneBigDec().Add(scaled_y4_quo_k2), 2)
 	if err != nil {
 		panic(err)
 	}
 
 	// let common_factor = [y^2 * 9k) * (sqrt_term + 1)]^(1/3)
-	common_factor, err := (y2.MulInt64(9).Mul(k).Mul((sqrt_term.Add(osmomath.OneBigDec())))).ApproxRoot(3)
+	common_factor, err := ApproxRoot(y2.MulInt64(9).Mul(k).Mul((sqrt_term.Add(osmomath.OneBigDec()))), 3)
 	if err != nil {
 		panic(err)
 	}
@@ -659,13 +667,13 @@ func solveCFMMMultiDirect(xReserve, yReserve, wSumSquares, yIn osmomath.BigDec)
 	wypy3 := (wSumSquares.Mul(y_new)).Add(y3)
 	wypy3pow3 := wypy3.Mul(wypy3).Mul(wypy3)
 
-	sqrt_term, err := ((k2.Mul(y4).MulInt64(729)).Add(y3.MulInt64(108).Mul(wypy3pow3))).ApproxRoot(2)
+	sqrt_term, err := ApproxRoot((k2.Mul(y4).MulInt64(729)).Add(y3.MulInt64(108).Mul(wypy3pow3)), 2)
 	if err != nil {
 		panic(err)
 	}
 
 	// let cube_root_term = (sqrt_term + 27 k y^2)^(1/3)
-	cube_root_term, err := (sqrt_term.Add(k.Mul(y2).MulInt64(27))).ApproxRoot(3)
+	cube_root_term, err := ApproxRoot(sqrt_term.Add(k.Mul(y2).MulInt64(27)), 3)
 	if err != nil {
 		panic(err)
 	}
@@ -1247,3 +1255,52 @@ func TestSingleAssetJoinSpreadFactorRatio(t *testing.T) {
 		})
 	}
 }
+
+// ApproxRoot returns an approximate estimation of a Dec's positive real nth root
+// using Newton's method (where n is positive). The algorithm starts with some guess and
+// computes the sequence of improved guesses until an answer converges to an
+// approximate answer.  It returns `|d|.ApproxRoot() * -1` if input is negative.
+// A maximum number of 100 iterations is used a backup boundary condition for
+// cases where the answer never converges enough to satisfy the main condition.
+func ApproxRoot(d osmomath.BigDec, root uint64) (guess osmomath.BigDec, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			var ok bool
+			err, ok = r.(error)
+			if !ok {
+				err = errors.New("out of bounds")
+			}
+		}
+	}()
+
+	if d.IsNegative() {
+		absRoot, err := ApproxRoot(d.MulInt64(-1), root)
+		return absRoot.MulInt64(-1), err
+	}
+
+	if root == 1 || d.IsZero() || d.Equal(osmomath.OneBigDec()) {
+		return d, nil
+	}
+
+	if root == 0 {
+		return osmomath.OneBigDec(), nil
+	}
+
+	rootInt := osmomath.NewBigIntFromUint64(root)
+	guess, delta := osmomath.OneBigDec(), osmomath.OneBigDec()
+
+	maxApproxRootIterations := 100
+	for iter := 0; delta.Abs().GT(osmomath.SmallestBigDec()) && iter < maxApproxRootIterations; iter++ {
+		prev := guess.PowerInteger(root - 1)
+		if prev.IsZero() {
+			prev = osmomath.SmallestBigDec()
+		}
+		delta = d.Quo(prev)
+		delta.SubMut(guess)
+		delta = delta.QuoInt(rootInt)
+
+		guess = guess.Add(delta)
+	}
+
+	return guess, nil
+}