Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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