Skip to content

Commit

Permalink
osmomath: mutative and efficient BigDec truncations with arbitrary de…
Browse files Browse the repository at this point in the history
…cimals (backport #6261) (#6470)

* osmomath: mutative and efficient BigDec truncations with arbitrary decimals (backport #6261)

* go.mod

---------

Co-authored-by: roman <roman@osmosis.team>
  • Loading branch information
mergify[bot] and p0mvn authored Sep 20, 2023
1 parent 3c734c9 commit 992b5b2
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#6427](https://github.com/osmosis-labs/osmosis/pull/6427) sdk.Coins Mul and Quo helpers in osmoutils
* [#6437](https://github.com/osmosis-labs/osmosis/pull/6437) mutative version for QuoRoundUp. Replace some non-mutative calls with mutative for better performance.
* [#6416](https://github.com/osmosis-labs/osmosis/pull/6416) feat[CL]: add num initialized ticks query
* [#6261](https://github.com/osmosis-labs/osmosis/pull/6261) mutative and efficient BigDec truncations with arbitrary decimals

### Misc Improvements

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.17
github.com/ory/dockertest/v3 v3.10.0
github.com/osmosis-labs/go-mutesting v0.0.0-20221208041716-b43bcd97b3b3
github.com/osmosis-labs/osmosis/osmomath v0.0.7-0.20230920173345-6401a459cb14
github.com/osmosis-labs/osmosis/osmomath v0.0.7-0.20230920194312-3eba9e93e29b
github.com/osmosis-labs/osmosis/osmoutils v0.0.7-0.20230920012324-f1a1ca887bd2
github.com/osmosis-labs/osmosis/x/epochs v0.0.2
github.com/osmosis-labs/osmosis/x/ibc-hooks v0.0.8
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,8 @@ github.com/osmosis-labs/osmosis/osmomath v0.0.7-0.20230920090526-bc02685001d4 h1
github.com/osmosis-labs/osmosis/osmomath v0.0.7-0.20230920090526-bc02685001d4/go.mod h1:oBmsOov8oxuWoI/yMQwyKGA6QfP0cBxylLt75gFbT8s=
github.com/osmosis-labs/osmosis/osmomath v0.0.7-0.20230920173345-6401a459cb14 h1:SR8J54Bi55oHr1KN7E+8PS1IZDXJJDPuBG+hrn/JoQA=
github.com/osmosis-labs/osmosis/osmomath v0.0.7-0.20230920173345-6401a459cb14/go.mod h1:oBmsOov8oxuWoI/yMQwyKGA6QfP0cBxylLt75gFbT8s=
github.com/osmosis-labs/osmosis/osmomath v0.0.7-0.20230920194312-3eba9e93e29b h1:IdvPd7vnth024fAQwFykphAjV1fpzUTSSs6VQ37QjTg=
github.com/osmosis-labs/osmosis/osmomath v0.0.7-0.20230920194312-3eba9e93e29b/go.mod h1:oBmsOov8oxuWoI/yMQwyKGA6QfP0cBxylLt75gFbT8s=
github.com/osmosis-labs/osmosis/osmoutils v0.0.7-0.20230920012324-f1a1ca887bd2 h1:A1Z6/SpM31atGBVyrgcgG0g50G9wHcfBxx8gIgQT9IQ=
github.com/osmosis-labs/osmosis/osmoutils v0.0.7-0.20230920012324-f1a1ca887bd2/go.mod h1:ukjFgxfR9obDrMd8ZsxKcp3HWL7+boYORVL7Bt7YOZM=
github.com/osmosis-labs/osmosis/x/epochs v0.0.2 h1:aEeXHGCSJMgMtAvCucsD2RSaWZ8lISFLD5u4MyF9KPc=
Expand Down
65 changes: 53 additions & 12 deletions osmomath/decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ var (
// initialized in init() since requires
// precision to be defined.
twoBigDec BigDec = MustNewBigDecFromStr("2")

// precisionFactors are used to adjust the scale of big.Int values to match the desired precision
precisionFactors = make(map[uint64]*big.Int)
)

// Decimal errors
Expand All @@ -70,6 +73,11 @@ func init() {
for i := 0; i <= PrecisionBigDec; i++ {
precisionMultipliers[i] = calcPrecisionMultiplier(int64(i))
}

for precision := uint64(0); precision <= PrecisionBigDec; precision++ {
precisionFactor := new(big.Int).Exp(big.NewInt(10), big.NewInt(PrecisionBigDec-int64(precision)), nil)
precisionFactors[precision] = precisionFactor
}
}

func precisionInt() *big.Int {
Expand Down Expand Up @@ -104,35 +112,35 @@ func NewBigDec(i int64) BigDec {
}

// create a new BigDec from integer with decimal place at prec
// CONTRACT: prec <= Precision
// CONTRACT: prec <= PrecisionBigDec
func NewBigDecWithPrec(i, prec int64) BigDec {
return BigDec{
new(big.Int).Mul(big.NewInt(i), precisionMultiplier(prec)),
}
}

// create a new BigDec from big integer assuming whole numbers
// CONTRACT: prec <= Precision
// CONTRACT: prec <= PrecisionBigDec
func NewBigDecFromBigInt(i *big.Int) BigDec {
return NewBigDecFromBigIntWithPrec(i, 0)
}

// create a new BigDec from big integer assuming whole numbers
// CONTRACT: prec <= Precision
// CONTRACT: prec <= PrecisionBigDec
func NewBigDecFromBigIntWithPrec(i *big.Int, prec int64) BigDec {
return BigDec{
new(big.Int).Mul(i, precisionMultiplier(prec)),
}
}

// create a new BigDec from big integer assuming whole numbers
// CONTRACT: prec <= Precision
// CONTRACT: prec <= PrecisionBigDec
func NewBigDecFromInt(i BigInt) BigDec {
return NewBigDecFromIntWithPrec(i, 0)
}

// create a new BigDec from big integer with decimal place at prec
// CONTRACT: prec <= Precision
// CONTRACT: prec <= PrecisionBigDec
func NewBigDecFromIntWithPrec(i BigInt, prec int64) BigDec {
return BigDec{
new(big.Int).Mul(i.BigInt(), precisionMultiplier(prec)),
Expand Down Expand Up @@ -567,23 +575,56 @@ func (d BigDec) MustFloat64() float64 {
// Dec returns the osmomath.Dec representation of a BigDec.
// Values in any additional decimal places are truncated.
func (d BigDec) Dec() Dec {
precisionDiff := PrecisionBigDec - PrecisionDec
precisionFactor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(precisionDiff)), nil)

if precisionDiff < 0 {
panic("invalid decimal precision")
return d.DecWithPrecision(PrecisionDec)
}

// DecWithPrecision converts BigDec to Dec with desired precision
// Example:
// BigDec: 1.010100000000153000000000000000000000
// precision: 4
// Output Dec: 1.010100000000000000
// Panics if precision exceeds PrecisionDec
func (d BigDec) DecWithPrecision(precision uint64) Dec {
var precisionFactor *big.Int
if precision > PrecisionDec {
panic(fmt.Sprintf("maximum Dec precision is (%v), provided (%v)", PrecisionDec, precision))
} else {
precisionFactor = precisionFactors[precision]
}

// Truncate any additional decimal values that exist due to BigDec's additional precision
// This relies on big.Int's Quo function doing floor division
intRepresentation := new(big.Int).Quo(d.BigInt(), precisionFactor)

// convert int representation back to SDK Dec precision
truncatedDec := NewDecFromBigIntWithPrec(intRepresentation, PrecisionDec)
truncatedDec := NewDecFromBigIntWithPrec(intRepresentation, int64(precision))

return truncatedDec
}

// ChopPrecisionMut truncates all decimals after precision numbers after decimal point. Mutative
// CONTRACT: precision <= PrecisionBigDec
// Panics if precision exceeds PrecisionBigDec
func (d *BigDec) ChopPrecisionMut(precision uint64) BigDec {
if precision > PrecisionBigDec {
panic(fmt.Sprintf("maximum BigDec precision is (%v), provided (%v)", PrecisionDec, precision))
}

precisionFactor := precisionFactors[precision]
// big.Quo truncates numbers that would have been after decimal point
d.i.Quo(d.i, precisionFactor)
d.i.Mul(d.i, precisionFactor)
return BigDec{d.i}
}

// ChopPrecision truncates all decimals after precision numbers after decimal point
// CONTRACT: precision <= PrecisionBigDec
// Panics if precision exceeds PrecisionBigDec
func (d *BigDec) ChopPrecision(precision uint64) BigDec {
copy := d.Clone()
return copy.ChopPrecisionMut(precision)
}

// DecRoundUp returns the osmomath.Dec representation of a BigDec.
// Round up at precision end.
// Values in any additional decimal places are truncated.
Expand Down Expand Up @@ -676,7 +717,7 @@ func chopPrecisionAndRoundUpBigDec(d *big.Int) *big.Int {
return chopPrecisionAndRoundUpMut(copy, precisionReuse)
}

// chopPrecisionAndRoundUpDec removes sdk.Precision amount of rightmost digits and rounds up.
// chopPrecisionAndRoundUpDec removes PrecisionDec amount of rightmost digits and rounds up.
// Non-mutative.
func chopPrecisionAndRoundUpDec(d *big.Int) *big.Int {
copy := new(big.Int).Set(d)
Expand Down
79 changes: 79 additions & 0 deletions osmomath/decimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"math/big"
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"gopkg.in/yaml.v2"
Expand Down Expand Up @@ -1502,6 +1503,84 @@ func (s *decimalTestSuite) TestPower() {
}
}

func (s *decimalTestSuite) TestDec_WithPrecision() {
tests := []struct {
d osmomath.BigDec
want osmomath.Dec
precision uint64
expPanic bool
}{
// test cases for basic SDKDec() conversion
{osmomath.NewBigDec(0), sdk.MustNewDecFromStr("0.000000000000000000"), osmomath.PrecisionDec, false},
{osmomath.NewBigDec(1), sdk.MustNewDecFromStr("1.000000000000000000"), osmomath.PrecisionDec, false},
{osmomath.NewBigDec(10), sdk.MustNewDecFromStr("10.000000000000000000"), osmomath.PrecisionDec, false},
{osmomath.NewBigDec(12340), sdk.MustNewDecFromStr("12340.000000000000000000"), osmomath.PrecisionDec, false},
{osmomath.NewBigDecWithPrec(12340, 4), sdk.MustNewDecFromStr("1.234000000000000000"), osmomath.PrecisionDec, false},
{osmomath.NewBigDecWithPrec(12340, 5), sdk.MustNewDecFromStr("0.123400000000000000"), osmomath.PrecisionDec, false},
{osmomath.NewBigDecWithPrec(12340, 8), sdk.MustNewDecFromStr("0.000123400000000000"), osmomath.PrecisionDec, false},
{osmomath.NewBigDecWithPrec(1009009009009009009, 17), sdk.MustNewDecFromStr("10.090090090090090090"), osmomath.PrecisionDec, false},
// test cases with custom precision:
{osmomath.NewBigDec(0), sdk.MustNewDecFromStr("0.000000000000"), 12, false},
{osmomath.NewBigDec(1), sdk.MustNewDecFromStr("1.000000000000"), 12, false},
// specified precision is the same as the initial precision: 12.3453123123 -> 12.3453123123
{osmomath.NewBigDecWithPrec(123453123123, 10), sdk.MustNewDecFromStr("12.3453123123"), 10, false},
// cut precision to 5 decimals: 3212.4623423462346 - 3212.46234
{osmomath.NewBigDecWithPrec(32124623423462346, 13), sdk.MustNewDecFromStr("3212.46234"), 5, false},
// no decimal point: 18012004 -> 18012004
{osmomath.NewBigDecWithPrec(18012004, 0), sdk.MustNewDecFromStr("18012004"), 13, false},
// if we try to convert to osmomath.Dec while specifying bigger precision than sdk.Dec has, panics
{osmomath.NewBigDecWithPrec(1009009009009009009, 17), sdk.MustNewDecFromStr("10.090090090090090090"), osmomath.PrecisionDec + 2, true},
}

for tcIndex, tc := range tests {
if tc.expPanic {
s.Require().Panics(func() { tc.d.DecWithPrecision(tc.precision) })
} else {
var got osmomath.Dec
if tc.precision == osmomath.PrecisionDec {
got = tc.d.Dec()
} else {
got = tc.d.DecWithPrecision(tc.precision)
}
s.Require().Equal(tc.want, got, "bad Dec conversion, index: %v", tcIndex)
}
}
}

func (s *decimalTestSuite) TestChopPrecision_Mutative() {
tests := []struct {
startValue osmomath.BigDec
expectedMutResult osmomath.BigDec
precision uint64
}{
{osmomath.NewBigDec(0), osmomath.MustNewBigDecFromStr("0"), 0},
{osmomath.NewBigDec(1), osmomath.MustNewBigDecFromStr("1"), 0},
{osmomath.NewBigDec(10), osmomath.MustNewBigDecFromStr("10"), 2},
// how to read these comments: ab.cde(fgh) -> ab.cdefgh = initial BigDec; (fgh) = decimal places that will be truncated
// 5.1()
{osmomath.NewBigDecWithPrec(51, 1), osmomath.MustNewBigDecFromStr("5.1"), 1},
// 1.(0010)
{osmomath.NewBigDecWithPrec(10010, 4), osmomath.MustNewBigDecFromStr("1"), 0},
// 1009.31254(83952)
{osmomath.NewBigDecWithPrec(10093125483952, 10), osmomath.MustNewBigDecFromStr("1009.31254"), 5},
// 0.1009312548(3952)
{osmomath.NewBigDecWithPrec(10093125483952, 14), osmomath.MustNewBigDecFromStr("0.1009312548"), 10},
// Edge case: max precision. Should remain unchanged
{osmomath.MustNewBigDecFromStr("1.000000000000000000000000000000000001"), osmomath.MustNewBigDecFromStr("1.000000000000000000000000000000000001"), osmomath.PrecisionBigDec},
}
for id, tc := range tests {
name := "testcase_" + fmt.Sprint(id)
s.Run(name, func() {
startMut := tc.startValue.Clone()
startNonMut := tc.startValue.Clone()

resultMut := startMut.ChopPrecisionMut(tc.precision)
resultNonMut := startNonMut.ChopPrecision(tc.precision)

s.assertMutResult(tc.expectedMutResult, tc.startValue, resultMut, resultNonMut, startMut, startNonMut)
})
}
}
func (s *decimalTestSuite) TestQuoRoundUp_MutativeAndNonMutative() {
tests := []struct {
d1, d2, expQuoRoundUpMut osmomath.BigDec
Expand Down
2 changes: 1 addition & 1 deletion simulation/simtypes/random/sdkrand.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func RandomDecAmount(r *rand.Rand, max osmomath.Dec) osmomath.Dec {
randInt = big.NewInt(0).Rand(r, max.BigInt())
}

return osmomath.NewDecFromBigIntWithPrec(randInt, sdk.Precision)
return osmomath.NewDecFromBigIntWithPrec(randInt, osmomath.PrecisionDec)
}

// RandTimestamp generates a random timestamp
Expand Down
3 changes: 1 addition & 2 deletions x/concentrated-liquidity/math/tick.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,9 @@ func CalculatePriceToTick(price osmomath.BigDec) (tickIndex int64, err error) {
// N.B. this exists to maintain backwards compatibility with
// the old version of the function that operated on decimal with precision of 18.
if price.GTE(types.MinSpotPriceBigDec) {
// TODO: implement efficient big decimal truncation.
// It is acceptable to truncate price as the minimum we support is
// 10**-12 which is above the smallest value of sdk.Dec.
price = osmomath.BigDecFromDec(price.Dec())
price.ChopPrecisionMut(osmomath.PrecisionDec)
}

// The approach here is to try determine which "geometric spacing" are we in.
Expand Down

0 comments on commit 992b5b2

Please sign in to comment.