Skip to content

Commit

Permalink
Use int256 in lib/quantums
Browse files Browse the repository at this point in the history
  • Loading branch information
roy-dydx committed Apr 6, 2024
1 parent e9f4210 commit ca802fb
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 147 deletions.
68 changes: 18 additions & 50 deletions protocol/lib/quantums.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package lib

import (
"math/big"
"github.com/dydxprotocol/v4-chain/protocol/lib/int256"
)

// BaseToQuoteQuantums converts an amount denoted in base quantums, to an equivalent amount denoted in quote
Expand All @@ -24,13 +24,13 @@ import (
//
// The result is rounded down.
func BaseToQuoteQuantums(
bigBaseQuantums *big.Int,
baseQuantums *int256.Int,
baseCurrencyAtomicResolution int32,
priceValue uint64,
priceExponent int32,
) (bigNotional *big.Int) {
) (notional *int256.Int) {
return multiplyByPrice(
new(big.Rat).SetInt(bigBaseQuantums),
baseQuantums,
baseCurrencyAtomicResolution,
priceValue,
priceExponent,
Expand All @@ -53,35 +53,16 @@ func BaseToQuoteQuantums(
//
// The result is rounded down.
func QuoteToBaseQuantums(
bigQuoteQuantums *big.Int,
quoteQuantums *int256.Int,
baseCurrencyAtomicResolution int32,
priceValue uint64,
priceExponent int32,
) (bigNotional *big.Int) {
// Determine the non-exponent part of the equation.
// We perform all calculations using positive rationals for consistent rounding.
isLong := bigQuoteQuantums.Sign() >= 0
ratAbsQuoteQuantums := new(big.Rat).Abs(
new(big.Rat).SetInt(bigQuoteQuantums),
)
ratPrice := new(big.Rat).SetUint64(priceValue)
ratQuoteQuantumsDivPrice := new(big.Rat).Quo(ratAbsQuoteQuantums, ratPrice)

// Determine the absolute value of the return value.
exponent := priceExponent + baseCurrencyAtomicResolution - QuoteCurrencyAtomicResolution
ratBaseQuantums := new(big.Rat).Quo(
ratQuoteQuantumsDivPrice,
RatPow10(exponent),
)

// Round down.
bigBaseQuantums := BigRatRound(ratBaseQuantums, false)

// Flip the sign of the return value if necessary.
if !isLong {
bigBaseQuantums.Neg(bigBaseQuantums)
}
return bigBaseQuantums
) (notional *int256.Int) {
notional = new(int256.Int).Set(quoteQuantums)
exponent := int64(-(priceExponent + baseCurrencyAtomicResolution - QuoteCurrencyAtomicResolution))
notional.MulExp10(notional, exponent)
notional.Div(notional, int256.NewUnsignedInt(priceValue))
return notional
}

// multiplyByPrice multiples a value by price, factoring in exponents of base
Expand All @@ -94,27 +75,14 @@ func QuoteToBaseQuantums(
// - For `BaseToQuoteQuantums`, substituing `value` with `baseQuantums` in expression 2 yields expression 1.
// - For `FundingRateToIndex`, substituing `value` with `fundingRatePpm * time` in expression 2 yields expression 3.
func multiplyByPrice(
value *big.Rat,
value *int256.Int,
baseCurrencyAtomicResolution int32,
priceValue uint64,
priceExponent int32,
) (result *big.Int) {
ratResult := new(big.Rat).SetUint64(priceValue)

ratResult.Mul(
ratResult,
value,
)

ratResult.Mul(
ratResult,
RatPow10(priceExponent+baseCurrencyAtomicResolution-QuoteCurrencyAtomicResolution),
)

return new(big.Int).Quo(
ratResult.Num(),
ratResult.Denom(),
)
) (result *int256.Int) {
result = int256.NewUnsignedInt(priceValue)
result.Mul(result, value)
return result.MulExp10(result, int64(priceExponent+baseCurrencyAtomicResolution-QuoteCurrencyAtomicResolution))
}

// FundingRateToIndex converts funding rate (in ppm) to FundingIndex given the oracle price.
Expand Down Expand Up @@ -143,11 +111,11 @@ func multiplyByPrice(
// priceValue: index price of the perpetual market according to the pricesKeeper
// priceExponent: priceExponent of the market according to the pricesKeeper
func FundingRateToIndex(
proratedFundingRate *big.Rat,
proratedFundingRate *int256.Int,
baseCurrencyAtomicResolution int32,
priceValue uint64,
priceExponent int32,
) (fundingIndex *big.Int) {
) (fundingIndex *int256.Int) {
return multiplyByPrice(
proratedFundingRate,
baseCurrencyAtomicResolution,
Expand Down
124 changes: 47 additions & 77 deletions protocol/lib/quantums_test.go
Original file line number Diff line number Diff line change
@@ -1,106 +1,90 @@
package lib_test

import (
"math"
"math/big"
"testing"

"github.com/dydxprotocol/v4-chain/protocol/lib"
big_testutil "github.com/dydxprotocol/v4-chain/protocol/testutil/big"
"github.com/dydxprotocol/v4-chain/protocol/lib/int256"
)

func TestBaseToQuoteQuantums(t *testing.T) {
tests := map[string]struct {
bigBaseQuantums *big.Int
baseQuantums *int256.Int
baseCurrencyAtomicResolution int32
priceValue uint64
priceExponent int32
bigExpectedQuoteQuantums *big.Int
expectedQuoteQuantums *int256.Int
}{
"Converts from base to quote quantums": {
bigBaseQuantums: big.NewInt(1),
baseQuantums: int256.NewInt(1),
baseCurrencyAtomicResolution: -6,
priceValue: 1,
priceExponent: 1,
bigExpectedQuoteQuantums: big.NewInt(10),
expectedQuoteQuantums: int256.NewInt(10),
},
"Correctly converts negative value": {
bigBaseQuantums: big.NewInt(-100),
baseQuantums: int256.NewInt(-100),
baseCurrencyAtomicResolution: -6,
priceValue: 1,
priceExponent: 1,
bigExpectedQuoteQuantums: big.NewInt(-1000),
expectedQuoteQuantums: int256.NewInt(-1000),
},
"priceExponent is negative": {
bigBaseQuantums: big.NewInt(5_000_000),
baseQuantums: int256.NewInt(5_000_000),
baseCurrencyAtomicResolution: -6,
priceValue: 7,
priceExponent: -5,
bigExpectedQuoteQuantums: big.NewInt(350),
expectedQuoteQuantums: int256.NewInt(350),
},
"priceExponent is zero": {
bigBaseQuantums: big.NewInt(5_000_000),
baseQuantums: int256.NewInt(5_000_000),
baseCurrencyAtomicResolution: -6,
priceValue: 7,
priceExponent: 0,
bigExpectedQuoteQuantums: big.NewInt(35_000_000),
expectedQuoteQuantums: int256.NewInt(35_000_000),
},
"baseCurrencyAtomicResolution is greater than 10^6": {
bigBaseQuantums: big.NewInt(5_000_000),
baseQuantums: int256.NewInt(5_000_000),
baseCurrencyAtomicResolution: -8,
priceValue: 7,
priceExponent: 0,
bigExpectedQuoteQuantums: big.NewInt(350_000),
expectedQuoteQuantums: int256.NewInt(350_000),
},
"baseCurrencyAtomicResolution is less than 10^6": {
bigBaseQuantums: big.NewInt(5_000_000),
baseQuantums: int256.NewInt(5_000_000),
baseCurrencyAtomicResolution: -4,
priceValue: 7,
priceExponent: 1,
bigExpectedQuoteQuantums: big.NewInt(35_000_000_000),
expectedQuoteQuantums: int256.NewInt(35_000_000_000),
},
"Calculation rounds down": {
bigBaseQuantums: big.NewInt(9),
baseQuantums: int256.NewInt(9),
baseCurrencyAtomicResolution: -8,
priceValue: 1,
priceExponent: 1,
bigExpectedQuoteQuantums: big.NewInt(0),
expectedQuoteQuantums: int256.NewInt(0),
},
"Negative calculation rounds up": {
bigBaseQuantums: big.NewInt(-9),
baseQuantums: int256.NewInt(-9),
baseCurrencyAtomicResolution: -8,
priceValue: 1,
priceExponent: 1,
bigExpectedQuoteQuantums: big.NewInt(0),
},
"Calculation overflows": {
bigBaseQuantums: new(big.Int).SetUint64(math.MaxUint64),
baseCurrencyAtomicResolution: -6,
priceValue: 2,
priceExponent: 0,
bigExpectedQuoteQuantums: big_testutil.MustFirst(new(big.Int).SetString("36893488147419103230", 10)),
},
"Calculation underflows": {
bigBaseQuantums: big_testutil.MustFirst(new(big.Int).SetString("-18446744073709551615", 10)),
baseCurrencyAtomicResolution: -6,
priceValue: 2,
priceExponent: 0,
bigExpectedQuoteQuantums: big_testutil.MustFirst(new(big.Int).SetString("-36893488147419103230", 10)),
expectedQuoteQuantums: int256.NewInt(0),
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
quoteQuantums := lib.BaseToQuoteQuantums(
tc.bigBaseQuantums,
tc.baseQuantums,
tc.baseCurrencyAtomicResolution,
tc.priceValue,
tc.priceExponent,
)
if tc.bigExpectedQuoteQuantums.Cmp(quoteQuantums) != 0 {
if tc.expectedQuoteQuantums.Cmp(quoteQuantums) != 0 {
t.Fatalf(
"%s: expectedQuoteQuantums: %s, quoteQuantums: %s",
name,
tc.bigExpectedQuoteQuantums.String(),
tc.expectedQuoteQuantums.String(),
quoteQuantums.String(),
)
}
Expand All @@ -110,110 +94,96 @@ func TestBaseToQuoteQuantums(t *testing.T) {

func TestQuoteToBaseQuantums(t *testing.T) {
tests := map[string]struct {
bigQuoteQuantums *big.Int
quoteQuantums *int256.Int
baseCurrencyAtomicResolution int32
priceValue uint64
priceExponent int32
bigExpectedBaseQuantums *big.Int
expectedBaseQuantums *int256.Int
}{
"Converts from base to quote quantums": {
bigQuoteQuantums: big.NewInt(10),
quoteQuantums: int256.NewInt(10),
baseCurrencyAtomicResolution: -6,
priceValue: 1,
priceExponent: 1,
bigExpectedBaseQuantums: big.NewInt(1),
expectedBaseQuantums: int256.NewInt(1),
},
"Correctly converts negative value": {
bigQuoteQuantums: big.NewInt(-1000),
quoteQuantums: int256.NewInt(-1000),
baseCurrencyAtomicResolution: -6,
priceValue: 1,
priceExponent: 1,
bigExpectedBaseQuantums: big.NewInt(-100),
expectedBaseQuantums: int256.NewInt(-100),
},
"priceExponent is negative": {
bigQuoteQuantums: big.NewInt(350),
quoteQuantums: int256.NewInt(350),
baseCurrencyAtomicResolution: -6,
priceValue: 7,
priceExponent: -5,
bigExpectedBaseQuantums: big.NewInt(5_000_000),
expectedBaseQuantums: int256.NewInt(5_000_000),
},
"priceExponent is zero": {
bigQuoteQuantums: big.NewInt(35_000_000),
quoteQuantums: int256.NewInt(35_000_000),
baseCurrencyAtomicResolution: -6,
priceValue: 7,
priceExponent: 0,
bigExpectedBaseQuantums: big.NewInt(5_000_000),
expectedBaseQuantums: int256.NewInt(5_000_000),
},
"realistic values: 1 BTC at $29001": {
bigQuoteQuantums: big.NewInt(29_001_000_000), // $29_001
quoteQuantums: int256.NewInt(29_001_000_000), // $29_001
baseCurrencyAtomicResolution: -10,
priceValue: 2_900_100_000,
priceExponent: -5,
bigExpectedBaseQuantums: big.NewInt(10_000_000_000),
expectedBaseQuantums: int256.NewInt(10_000_000_000),
},
"realistic values: 25.123 BTC at $29001": {
bigQuoteQuantums: big.NewInt(728_592_123_000), // $728_592.123
quoteQuantums: int256.NewInt(728_592_123_000), // $728_592.123
baseCurrencyAtomicResolution: -10,
priceValue: 2_900_100_000,
priceExponent: -5,
bigExpectedBaseQuantums: big.NewInt(251_230_000_000),
expectedBaseQuantums: int256.NewInt(251_230_000_000),
},
"baseCurrencyAtomicResolution is greater than 10^6": {
bigQuoteQuantums: big.NewInt(350_000),
quoteQuantums: int256.NewInt(350_000),
baseCurrencyAtomicResolution: -8,
priceValue: 7,
priceExponent: 0,
bigExpectedBaseQuantums: big.NewInt(5_000_000),
expectedBaseQuantums: int256.NewInt(5_000_000),
},
"baseCurrencyAtomicResolution is less than 10^6": {
bigQuoteQuantums: big.NewInt(35_000_000_000),
quoteQuantums: int256.NewInt(35_000_000_000),
baseCurrencyAtomicResolution: -4,
priceValue: 7,
priceExponent: 1,
bigExpectedBaseQuantums: big.NewInt(5_000_000),
expectedBaseQuantums: int256.NewInt(5_000_000),
},
"Calculation rounds down": {
bigQuoteQuantums: big.NewInt(99),
quoteQuantums: int256.NewInt(99),
baseCurrencyAtomicResolution: -6,
priceValue: 1,
priceExponent: 1,
bigExpectedBaseQuantums: big.NewInt(9),
expectedBaseQuantums: int256.NewInt(9),
},
"Negative calculation rounds up": {
bigQuoteQuantums: big.NewInt(-99),
quoteQuantums: int256.NewInt(-99),
baseCurrencyAtomicResolution: -6,
priceValue: 1,
priceExponent: 1,
bigExpectedBaseQuantums: big.NewInt(-9),
},
"Calculation overflows": {
bigQuoteQuantums: new(big.Int).SetUint64(math.MaxUint64),
baseCurrencyAtomicResolution: -7,
priceValue: 1,
priceExponent: 0,
bigExpectedBaseQuantums: big_testutil.MustFirst(new(big.Int).SetString("184467440737095516150", 10)),
},
"Calculation underflows": {
bigQuoteQuantums: big_testutil.MustFirst(new(big.Int).SetString("-18446744073709551615", 10)),
baseCurrencyAtomicResolution: -7,
priceValue: 1,
priceExponent: 0,
bigExpectedBaseQuantums: big_testutil.MustFirst(new(big.Int).SetString("-184467440737095516150", 10)),
expectedBaseQuantums: int256.NewInt(-9),
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
baseQuantums := lib.QuoteToBaseQuantums(
tc.bigQuoteQuantums,
tc.quoteQuantums,
tc.baseCurrencyAtomicResolution,
tc.priceValue,
tc.priceExponent,
)
if tc.bigExpectedBaseQuantums.Cmp(baseQuantums) != 0 {
if tc.expectedBaseQuantums.Cmp(baseQuantums) != 0 {
t.Fatalf(
"%s: expectedBaseQuantums: %s, baseQuantums: %s",
name,
tc.bigExpectedBaseQuantums.String(),
tc.expectedBaseQuantums.String(),
baseQuantums.String(),
)
}
Expand Down
Loading

0 comments on commit ca802fb

Please sign in to comment.