From 011b9c211ac5cea614a1c0015a7a24bca181f989 Mon Sep 17 00:00:00 2001 From: Brendan Chou <3680392+BrendanChou@users.noreply.github.com> Date: Wed, 22 May 2024 11:45:34 -0400 Subject: [PATCH] Remove logic of `FundingRateToIndex` (#1528) --- protocol/lib/quantums.go | 72 --------------- protocol/testutil/perpetuals/perpetuals.go | 6 ++ protocol/x/perpetuals/funding/funding.go | 47 ++++++++++ protocol/x/perpetuals/funding/funding_test.go | 89 +++++++++++++++++++ protocol/x/perpetuals/keeper/perpetual.go | 59 +++--------- 5 files changed, 155 insertions(+), 118 deletions(-) create mode 100644 protocol/x/perpetuals/funding/funding.go create mode 100644 protocol/x/perpetuals/funding/funding_test.go diff --git a/protocol/lib/quantums.go b/protocol/lib/quantums.go index cf5634c36a..b10286e490 100644 --- a/protocol/lib/quantums.go +++ b/protocol/lib/quantums.go @@ -88,75 +88,3 @@ func QuoteToBaseQuantums( return result } - -// multiplyByPrice multiples a value by price, factoring in exponents of base -// and quote currencies. -// Given `value`, returns result of the following: -// -// `value * priceValue * 10^(priceExponent + baseAtomicResolution - quoteAtomicResolution)` [expression 2] -// -// Note that both `BaseToQuoteQuantums` and `FundingRateToIndex` directly wrap around this function. -// - 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, - 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(), - ) -} - -// FundingRateToIndex converts funding rate (in ppm) to FundingIndex given the oracle price. -// -// To get funding index from funding rate, we know that: -// -// - `fundingPaymentQuoteQuantum = fundingRatePpm / 1_000_000 * time * quoteQuantums` -// - Divide both sides by `baseQuantums`: -// - Left side: `fundingPaymentQuoteQuantums / baseQuantums = fundingIndexDelta / 1_000_000` -// - right side: -// ``` -// fundingRate * time * quoteQuantums / baseQuantums = fundingRatePpm / 1_000_000 * -// priceValue * 10^(priceExponent + baseCurrencyAtomicResolution - quoteCurrencyAtomicResolution) [expression 3] -// ``` -// -// Hence, further multiplying both sides by 1_000_000, we have: -// -// fundingIndexDelta = -// (fundingRatePpm * time) * priceValue * -// 10^(priceExponent + baseCurrencyAtomicResolution - quoteCurrencyAtomicResolution) -// -// Arguments: -// -// proratedFundingRate: prorated funding rate adjusted by time delta, in parts-per-million -// baseCurrencyAtomicResolution: atomic resolution of the base currency -// 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, - baseCurrencyAtomicResolution int32, - priceValue uint64, - priceExponent int32, -) (fundingIndex *big.Int) { - return multiplyByPrice( - proratedFundingRate, - baseCurrencyAtomicResolution, - priceValue, - priceExponent, - ) -} diff --git a/protocol/testutil/perpetuals/perpetuals.go b/protocol/testutil/perpetuals/perpetuals.go index deb06ae378..2c468b7977 100644 --- a/protocol/testutil/perpetuals/perpetuals.go +++ b/protocol/testutil/perpetuals/perpetuals.go @@ -44,6 +44,12 @@ func WithLiquidityTier(liquidityTier uint32) PerpetualModifierOption { } } +func WithAtomicResolution(atomicResolution int32) PerpetualModifierOption { + return func(cp *perptypes.Perpetual) { + cp.Params.AtomicResolution = atomicResolution + } +} + func WithMarketType(marketType perptypes.PerpetualMarketType) PerpetualModifierOption { return func(cp *perptypes.Perpetual) { cp.Params.MarketType = marketType diff --git a/protocol/x/perpetuals/funding/funding.go b/protocol/x/perpetuals/funding/funding.go new file mode 100644 index 0000000000..fab7897b41 --- /dev/null +++ b/protocol/x/perpetuals/funding/funding.go @@ -0,0 +1,47 @@ +package funding + +import ( + "math/big" + + "github.com/dydxprotocol/v4-chain/protocol/lib" + "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" + pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" +) + +// GetFundingIndexDelta returns `fundingIndexDelta` which represents the change of the funding index +// given the funding rate, the time since the last funding tick, and the oracle price. The index delta +// is in parts-per-million (PPM) and is calculated as follows: +// +// indexDelta = +// fundingRatePpm * +// (time / realizationPeriod) * +// quoteQuantumsPerBaseQuantum +// +// Any multiplication is done before division to avoid precision loss. +func GetFundingIndexDelta( + perp types.Perpetual, + marketPrice pricestypes.MarketPrice, + big8hrFundingRatePpm *big.Int, + timeSinceLastFunding uint32, +) (fundingIndexDelta *big.Int) { + // Get pro-rated funding rate adjusted by time delta. + result := new(big.Int).SetUint64(uint64(timeSinceLastFunding)) + + // Multiply by the time-delta numerator upfront. + result.Mul(result, big8hrFundingRatePpm) + + // Multiply by the price of the asset. + result = lib.BaseToQuoteQuantums( + result, + perp.Params.AtomicResolution, + marketPrice.Price, + marketPrice.Exponent, + ) + + // Divide by the time-delta denominator. + // Use truncated division (towards zero) instead of Euclidean division. + // TODO(DEC-1536): Make the 8-hour funding rate period configurable. + result.Quo(result, big.NewInt(60*60*8)) + + return result +} diff --git a/protocol/x/perpetuals/funding/funding_test.go b/protocol/x/perpetuals/funding/funding_test.go new file mode 100644 index 0000000000..f396cae924 --- /dev/null +++ b/protocol/x/perpetuals/funding/funding_test.go @@ -0,0 +1,89 @@ +package funding_test + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + perptest "github.com/dydxprotocol/v4-chain/protocol/testutil/perpetuals" + "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/funding" + "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" + pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" +) + +func TestGetFundingIndexDelta(t *testing.T) { + testCases := map[string]struct { + perp types.Perpetual + marketPrice pricestypes.MarketPrice + big8hrFundingRatePpm *big.Int + timeSinceLastFunding uint32 + expected *big.Int + }{ + "Positive Funding Rate (rounds towards zero)": { + perp: *perptest.GeneratePerpetual( + perptest.WithAtomicResolution(-12), + ), + marketPrice: pricestypes.MarketPrice{ + Id: 0, + Exponent: 0, + Price: 1_000, + }, + big8hrFundingRatePpm: big.NewInt(1_001_999), + timeSinceLastFunding: 8 * 60 * 60, + expected: big.NewInt(1_001), + }, + "Negative Funding Rate (rounds towards zero)": { + perp: *perptest.GeneratePerpetual( + perptest.WithAtomicResolution(-12), + ), + marketPrice: pricestypes.MarketPrice{ + Id: 0, + Exponent: 0, + Price: 1_000, + }, + big8hrFundingRatePpm: big.NewInt(-1_001_999), + timeSinceLastFunding: 8 * 60 * 60, + expected: big.NewInt(-1_001), + }, + "Varied parameters (1)": { + perp: *perptest.GeneratePerpetual( + perptest.WithAtomicResolution(-4), + ), + marketPrice: pricestypes.MarketPrice{ + Id: 0, + Exponent: 2, + Price: 1_000, + }, + big8hrFundingRatePpm: big.NewInt(-1_001_999), + timeSinceLastFunding: 8 * 60 * 60 / 2, + expected: big.NewInt(-5_009_995_000_000), + }, + "Varied parameters (2)": { + perp: *perptest.GeneratePerpetual( + perptest.WithAtomicResolution(0), + ), + marketPrice: pricestypes.MarketPrice{ + Id: 0, + Exponent: -6, + Price: 1_000, + }, + big8hrFundingRatePpm: big.NewInt(-1_001_999), + timeSinceLastFunding: 8 * 60 * 60 / 8, + expected: big.NewInt(-125_249_875), + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := funding.GetFundingIndexDelta( + tc.perp, + tc.marketPrice, + tc.big8hrFundingRatePpm, + tc.timeSinceLastFunding, + ) + + require.Equal(t, tc.expected, result) + }) + } +} diff --git a/protocol/x/perpetuals/keeper/perpetual.go b/protocol/x/perpetuals/keeper/perpetual.go index 4e8460b466..5f552b9961 100644 --- a/protocol/x/perpetuals/keeper/perpetual.go +++ b/protocol/x/perpetuals/keeper/perpetual.go @@ -27,6 +27,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib/log" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" epochstypes "github.com/dydxprotocol/v4-chain/protocol/x/epochs/types" + "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/funding" "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" gometrics "github.com/hashicorp/go-metrics" @@ -432,46 +433,6 @@ func (k Keeper) MaybeProcessNewFundingSampleEpoch( k.processPremiumVotesIntoSamples(ctx, newFundingSampleEpoch) } -// getFundingIndexDelta returns fundingIndexDelta which represents the change of FundingIndex since -// the last time `funding-tick` was processed. -// TODO(DEC-1536): Make the 8-hour funding rate period configurable. -func (k Keeper) getFundingIndexDelta( - ctx sdk.Context, - perp types.Perpetual, - big8hrFundingRatePpm *big.Int, - timeSinceLastFunding uint32, -) ( - fundingIndexDelta *big.Int, - err error, -) { - marketPrice, err := k.pricesKeeper.GetMarketPrice(ctx, perp.Params.MarketId) - if err != nil { - return nil, fmt.Errorf("failed to get market price for perpetual %v, err = %w", perp.Params.Id, err) - } - - // Get pro-rated funding rate adjusted by time delta. - proratedFundingRate := new(big.Rat).SetInt(big8hrFundingRatePpm) - proratedFundingRate.Mul( - proratedFundingRate, - new(big.Rat).SetUint64(uint64(timeSinceLastFunding)), - ) - - proratedFundingRate.Quo( - proratedFundingRate, - // TODO(DEC-1536): Make the 8-hour funding rate period configurable. - new(big.Rat).SetUint64(3600*8), - ) - - bigFundingIndexDelta := lib.FundingRateToIndex( - proratedFundingRate, - perp.Params.AtomicResolution, - marketPrice.Price, - marketPrice.Exponent, - ) - - return bigFundingIndexDelta, nil -} - // GetAddPremiumVotes returns the newest premiums for all perpetuals, // if the current block is the start of a new funding-sample epoch. // Otherwise, does nothing and returns an empty message. @@ -770,20 +731,26 @@ func (k Keeper) MaybeProcessNewFundingTickEpoch(ctx sdk.Context) { )) } + // Update the funding index if the funding rate is non-zero. if bigFundingRatePpm.Sign() != 0 { - fundingIndexDelta, err := k.getFundingIndexDelta( - ctx, + // Get the price of the perpetual from state. + marketPrice, err := k.pricesKeeper.GetMarketPrice(ctx, perp.Params.MarketId) + if err != nil { + panic(err) + } + + // Calculate the delta in the funding index. + fundingIndexDelta := funding.GetFundingIndexDelta( perp, + marketPrice, bigFundingRatePpm, // use funding-tick duration as `timeSinceLastFunding` // TODO(DEC-1483): Handle the case when duration value is updated // during the epoch. fundingTickEpochInfo.Duration, ) - if err != nil { - panic(err) - } + // Update the funding index in state. if err := k.ModifyFundingIndex(ctx, perp.Params.Id, fundingIndexDelta); err != nil { panic(err) } @@ -1095,7 +1062,7 @@ func GetSettlementPpmWithPerpetual( bigNetSettlementPpm = new(big.Int).Mul(indexDelta, quantums) - // `bigNetSettlementPpm` carries sign. `indexDelta`` is the increase in `fundingIndex`, so if + // `bigNetSettlementPpm` carries sign. `indexDelta` is the increase in `fundingIndex`, so if // the position is long (positive), the net settlement should be short (negative), and vice versa. // Thus, always negate `bigNetSettlementPpm` here. bigNetSettlementPpm = bigNetSettlementPpm.Neg(bigNetSettlementPpm)