diff --git a/util/bpmath/bp.go b/util/bpmath/bp.go new file mode 100644 index 0000000000..825c7829d6 --- /dev/null +++ b/util/bpmath/bp.go @@ -0,0 +1,55 @@ +package bpmath + +import ( + "math" + + cmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// BP represents values in basis points. Maximum value is 2^32-1. +// Note: BP operations should not be chained - this causes precision losses. +type BP uint32 + +func (bp BP) ToDec() sdk.Dec { + return sdk.NewDecWithPrec(int64(bp), 4) +} + +// Mul return a*bp rounding towards zero. +func (bp BP) Mul(a cmath.Int) cmath.Int { + return Mul(a, bp) +} + +// FromQuo returns a/b in basis points. +// Contract: a>=0 and b > 0. +// Panics if a/b >= MaxUint32/10'000 or if b==0. +func FromQuo(dividend, divisor cmath.Int, rounding Rounding) BP { + return BP(quo(dividend, divisor, rounding, math.MaxUint32)) +} + +func quo(a, b cmath.Int, rounding Rounding, max uint64) uint64 { + if b.IsZero() { + panic("divider can't be zero") + } + bp := a.MulRaw(One) + if rounding == UP { + bp = bp.Add(b.SubRaw(1)) + } + x := bp.Quo(b).Uint64() + if x > max { + panic("basis points out of band") + } + return x +} + +// Mul returns a * b_basis_points rounding towards zero. +// Contract: b in [0, MaxUint32] +func Mul[T BP | FixedBP](a cmath.Int, b T) cmath.Int { + if b == 0 { + return cmath.ZeroInt() + } + if b == One { + return a + } + return a.MulRaw(int64(b)).Quo(oneBigInt) +} diff --git a/util/bpmath/bp_test.go b/util/bpmath/bp_test.go new file mode 100644 index 0000000000..4faa1f6f99 --- /dev/null +++ b/util/bpmath/bp_test.go @@ -0,0 +1,58 @@ +package bpmath + +import ( + "fmt" + "testing" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestBPToDec(t *testing.T) { + t.Parallel() + tcs := []struct { + name string + a FixedBP + exp math.LegacyDec + }{ + {"t1", 99999, math.LegacyMustNewDecFromStr("9.9999")}, + {"t2", One * 10, math.LegacyMustNewDecFromStr("10.0")}, + } + require := require.New(t) + for _, tc := range tcs { + bp := BP(tc.a).ToDec() + require.Equal(tc.exp.String(), bp.String(), fmt.Sprint("test-bp ", tc.name)) + } +} + +// Tests if it works both with sdk.Int and math.Int +func TestInt(t *testing.T) { + t.Parallel() + require := require.New(t) + + bp := BP(100) + si := sdk.NewInt(1234) + + var sresult sdk.Int = Mul(si, bp) + require.Equal(sresult, sdk.NewInt(12)) + var mresult math.Int = Mul(si, bp) + require.Equal(mresult, math.NewInt(12)) + + // now let's check math.Int + mi := math.NewInt(1234) + sresult = Mul(mi, bp) + require.Equal(sresult, sdk.NewInt(12)) + mresult = Mul(mi, bp) + require.Equal(mresult, math.NewInt(12)) + + // test rounding + si = sdk.NewInt(1299) + require.Equal(bp.Mul(si), sdk.NewInt(12)) + + si = sdk.NewInt(-1299) + require.Equal(bp.Mul(si), sdk.NewInt(-12)) + + si = sdk.NewInt(-1201) + require.Equal(bp.Mul(si), sdk.NewInt(-12)) +} diff --git a/util/bpmath/doc.go b/util/bpmath/doc.go new file mode 100644 index 0000000000..a35b593b36 --- /dev/null +++ b/util/bpmath/doc.go @@ -0,0 +1,4 @@ +// Package bpmath provides types and functions for doing basis point operations with math.Int. +// So, the result is always math.Int, and follows the Go rounding semantic for integer nubmer: +// rounds towards zero. +package bpmath diff --git a/util/bpmath/fixed_bp.go b/util/bpmath/fixed_bp.go new file mode 100644 index 0000000000..d28e4a4e22 --- /dev/null +++ b/util/bpmath/fixed_bp.go @@ -0,0 +1,26 @@ +package bpmath + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// FixedBP assures that all operations are in 0-10'000 range +// Note: FixedBP operations should not be chained - this causes precision loses. +type FixedBP uint32 + +// FixedFromQuo returns a/b in basis points. Returns 10'000 if a >= b; +// Contract: a>=0 and b > 0. +// Panics if b==0. +func FixedFromQuo(dividend, divisor math.Int, rounding Rounding) FixedBP { + return FixedBP(quo(dividend, divisor, rounding, One)) +} + +func (bp FixedBP) ToDec() sdk.Dec { + return sdk.NewDecWithPrec(int64(bp), 4) +} + +// Mul return a*bp rounding towards zero. +func (bp FixedBP) Mul(a math.Int) math.Int { + return Mul(a, bp) +} diff --git a/util/bpmath/fixed_bp_test.go b/util/bpmath/fixed_bp_test.go new file mode 100644 index 0000000000..6d0b4b9208 --- /dev/null +++ b/util/bpmath/fixed_bp_test.go @@ -0,0 +1,109 @@ +package bpmath + +import ( + "fmt" + "testing" + + "cosmossdk.io/math" + "github.com/stretchr/testify/require" +) + +func TestFixedQuo(t *testing.T) { + t.Parallel() + tcs := []struct { + name string + a uint64 + b uint64 + r Rounding + exp FixedBP + panics bool + }{ + {"t1", 0, 0, UP, One, true}, + {"t2", 0, 0, DOWN, One, true}, + {"t3", 1, 0, UP, One, true}, + {"t4", 1, 0, DOWN, One, true}, + + {"t5", 20, 10, UP, 0, true}, + {"t6", 20, 10, DOWN, 0, true}, + {"t7", 20, 20, UP, One, false}, + {"t7-1", 20, 20, DOWN, One, false}, + + {"t8", 1, 2, UP, One / 2, false}, + {"t9", 1, 2, DOWN, One / 2, false}, + {"t10", 1, 3, UP, 3334, false}, + {"t11", 1, 3, DOWN, 3333, false}, + {"t12", 2, 3, UP, 6667, false}, + {"t13", 2, 3, DOWN, 6666, false}, + {"t14", 10, 99999, UP, 2, false}, + {"t15", 10, 99999, DOWN, 1, false}, + {"t16", 10, 99999999, UP, 1, false}, + {"t17", 10, 99999999, DOWN, 0, false}, + } + require := require.New(t) + for _, tc := range tcs { + a, b := math.NewIntFromUint64(tc.a), math.NewIntFromUint64(tc.b) + if tc.panics { + require.Panics(func() { + FixedFromQuo(a, b, tc.r) + }, tc.name) + continue + } + o := FixedFromQuo(a, b, tc.r) + require.Equal(int(tc.exp), int(o), fmt.Sprint("test ", tc.name)) + } +} + +func TestFixedMul(t *testing.T) { + t.Parallel() + tcs := []struct { + name string + a uint64 + b BP + exp uint64 + }{ + {"t1", 20, 0, 0}, + {"t2", 20, 1, 0}, + {"t3", 20, One, 20}, + {"t4", 20000, 0, 0}, + {"t5", 20000, 1, 2}, + {"t6", 20000, 2, 4}, + {"t7", 20000, Half, 10000}, + {"t8", 2000, 4, 0}, + {"t9", 2000, 5, 1}, + {"t10", 2000, Half, 1000}, + } + require := require.New(t) + for _, tc := range tcs { + a := math.NewIntFromUint64(tc.a) + o := Mul(a, tc.b) + require.Equal(int64(tc.exp), o.Int64(), fmt.Sprint("test ", tc.name)) + + // must work with both FixedBP and BP + o = Mul(a, FixedBP(tc.b)) + require.Equal(int64(tc.exp), o.Int64(), fmt.Sprint("test ", tc.name)) + + } +} + +func TestFixedToDec(t *testing.T) { + t.Parallel() + tcs := []struct { + name string + a FixedBP + exp math.LegacyDec + }{ + {"t1", 0, math.LegacyZeroDec()}, + {"t2", 1, math.LegacyMustNewDecFromStr("0.0001")}, + {"t3", 20, math.LegacyMustNewDecFromStr("0.002")}, + {"t4", 9999, math.LegacyMustNewDecFromStr("0.9999")}, + {"t5", One, math.LegacyNewDec(1)}, + } + require := require.New(t) + for _, tc := range tcs { + o := tc.a.ToDec() + require.Equal(tc.exp.String(), o.String(), fmt.Sprint("test-fixedbp ", tc.name)) + + bp := BP(tc.a).ToDec() + require.Equal(tc.exp.String(), bp.String(), fmt.Sprint("test-bp ", tc.name)) + } +} diff --git a/util/bpmath/types.go b/util/bpmath/types.go new file mode 100644 index 0000000000..705a75b13c --- /dev/null +++ b/util/bpmath/types.go @@ -0,0 +1,19 @@ +package bpmath + +import ( + "cosmossdk.io/math" +) + +type Rounding uint + +const ( + DOWN Rounding = iota + UP +) + +const One = 10000 +const Half = One / 2 + +var ( + oneBigInt = math.NewIntFromUint64(One) +)