From e2f9fcf758d81e9d8882610470f06aeb379938f8 Mon Sep 17 00:00:00 2001 From: Sergei Drugalev Date: Fri, 8 Sep 2023 18:20:43 +0200 Subject: [PATCH] Merc-1640 fixing Mercury fee calculation (#168) * Fixing native/link fee calculation * Inclusing cents_in_usd into the price_scaling_factor to simplify the fee calculation * CalculateFee returns 0 when token price or base fee is 0 * Using decimal package for fee calculation * Fixing tests --- pkg/reportingplugins/mercury/fees.go | 41 +++++++++++++------ pkg/reportingplugins/mercury/fees_test.go | 41 ++++++++++++++++--- .../mercury/v2/mercury_test.go | 19 +++++---- .../mercury/v3/mercury_test.go | 19 +++++---- 4 files changed, 84 insertions(+), 36 deletions(-) diff --git a/pkg/reportingplugins/mercury/fees.go b/pkg/reportingplugins/mercury/fees.go index d5c8f58c4fb..6455ea014ef 100644 --- a/pkg/reportingplugins/mercury/fees.go +++ b/pkg/reportingplugins/mercury/fees.go @@ -1,24 +1,39 @@ package mercury -import "math/big" +import ( + "math/big" -// PriceScalingFactor indicates the multiplier applied to token prices. + "github.com/shopspring/decimal" +) + +// PriceScalingFactor indicates the multiplier applied to token prices that we expect from data source // e.g. for a 1e8 multiplier, a LINK/USD value of 7.42 will be represented as 742000000 -// This is what we expect from our data source. -var PRICE_SCALING_FACTOR = big.NewInt(1e8) +// The factor is decreased 1e8 -> 1e6 to comnpensate for baseUSDFee being in cents not usd +var PRICE_SCALING_FACTOR = decimal.NewFromInt(1e6) // FeeScalingFactor indicates the multiplier applied to fees. // e.g. for a 1e18 multiplier, a LINK fee of 7.42 will be represented as 7.42e18 // This is what will be baked into the report for use on-chain. -var FEE_SCALING_FACTOR = big.NewInt(1e18) +var FEE_SCALING_FACTOR = decimal.NewFromInt(1e18) + +// CalculateFee outputs a fee in wei according to the formula: baseUSDFeeCents * scaleFactor / tokenPriceInUSD +func CalculateFee(tokenPriceInUSD *big.Int, baseUSDFeeCents uint32) *big.Int { + if tokenPriceInUSD.Cmp(big.NewInt(0)) == 0 || baseUSDFeeCents == 0 { + // zero fee if token price or base fee is zero + return big.NewInt(0) + } + + // scale baseFee in USD + baseFeeScaled := decimal.NewFromInt32(int32(baseUSDFeeCents)).Mul(PRICE_SCALING_FACTOR) + + tokenPrice := decimal.NewFromBigInt(tokenPriceInUSD, 0) + + // fee denominated in token + fee := baseFeeScaled.Div(tokenPrice) -var CENTS_PER_DOLLAR = big.NewInt(100) + // scale fee to the expected format + fee = fee.Mul(FEE_SCALING_FACTOR) -// CalculateFee outputs a fee in wei -func CalculateFee(tokenPriceInUSD *big.Int, baseUSDFeeCents uint32) (fee *big.Int) { - fee = new(big.Int).Mul(big.NewInt(int64(baseUSDFeeCents)), tokenPriceInUSD) - fee = fee.Mul(fee, FEE_SCALING_FACTOR) - fee = fee.Div(fee, PRICE_SCALING_FACTOR) - fee = fee.Div(fee, CENTS_PER_DOLLAR) - return + // convert to big.Int + return fee.BigInt() } diff --git a/pkg/reportingplugins/mercury/fees_test.go b/pkg/reportingplugins/mercury/fees_test.go index c0d93b9b049..140de4c5110 100644 --- a/pkg/reportingplugins/mercury/fees_test.go +++ b/pkg/reportingplugins/mercury/fees_test.go @@ -3,15 +3,46 @@ package mercury import ( "math/big" "testing" + + "github.com/stretchr/testify/assert" ) +func scalePrice(usdPrice float64) *big.Int { + scaledPrice := new(big.Float).Mul(big.NewFloat(usdPrice), big.NewFloat(1e8)) + scaledPriceInt, _ := scaledPrice.Int(nil) + return scaledPriceInt +} + func Test_Fees(t *testing.T) { - t.Run("CalculateFee", func(t *testing.T) { - tokenPriceInUSD := big.NewInt(655000000) - var baseUSDFeeCents uint32 = 100 + var baseUSDFeeCents uint32 = 70 + t.Run("with token price > 1", func(t *testing.T) { + tokenPriceInUSD := scalePrice(1630) + fee := CalculateFee(tokenPriceInUSD, baseUSDFeeCents) + expectedFee := big.NewInt(429447852760700) // 0.0004294478527607 18 decimals + if fee.Cmp(expectedFee) != 0 { + t.Errorf("Expected fee to be %v, got %v", expectedFee, fee) + } + }) + + t.Run("with token price < 1", func(t *testing.T) { + tokenPriceInUSD := scalePrice(0.4) fee := CalculateFee(tokenPriceInUSD, baseUSDFeeCents) - if fee.Cmp(big.NewInt(6.55e18)) != 0 { - t.Errorf("Expected fee to be 6550000000000000000, got %v", fee) + expectedFee := big.NewInt(1750000000000000000) // 1.75 18 decimals + if fee.Cmp(expectedFee) != 0 { + t.Errorf("Expected fee to be %v, got %v", expectedFee, fee) } }) + + t.Run("with token price == 0", func(t *testing.T) { + tokenPriceInUSD := scalePrice(0) + fee := CalculateFee(tokenPriceInUSD, baseUSDFeeCents) + assert.Equal(t, big.NewInt(0), fee) + }) + + t.Run("with base fee == 0", func(t *testing.T) { + tokenPriceInUSD := scalePrice(123) + baseUSDFeeCents = 0 + fee := CalculateFee(tokenPriceInUSD, baseUSDFeeCents) + assert.Equal(t, big.NewInt(0), fee) + }) } diff --git a/pkg/reportingplugins/mercury/v2/mercury_test.go b/pkg/reportingplugins/mercury/v2/mercury_test.go index 37e4f220e87..ddc89fb0886 100644 --- a/pkg/reportingplugins/mercury/v2/mercury_test.go +++ b/pkg/reportingplugins/mercury/v2/mercury_test.go @@ -457,9 +457,13 @@ func Test_Plugin_Observation(t *testing.T) { assert.True(t, p.PricesValid) assert.Equal(t, obs.MaxFinalizedTimestamp.Val, p.MaxFinalizedTimestamp) assert.True(t, p.MaxFinalizedTimestampValid) - assert.Equal(t, mercury.CalculateFee(obs.LinkPrice.Val, 100), mustDecodeBigInt(p.LinkFee)) + + fee := mercury.CalculateFee(obs.LinkPrice.Val, 100) + assert.Equal(t, fee, mustDecodeBigInt(p.LinkFee)) assert.True(t, p.LinkFeeValid) - assert.Equal(t, mercury.CalculateFee(obs.NativePrice.Val, 100), mustDecodeBigInt(p.NativeFee)) + + fee = mercury.CalculateFee(obs.NativePrice.Val, 100) + assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) assert.True(t, p.NativeFeeValid) }) @@ -520,7 +524,9 @@ func Test_Plugin_Observation(t *testing.T) { assert.False(t, p.MaxFinalizedTimestampValid) assert.Zero(t, p.LinkFee) assert.False(t, p.LinkFeeValid) - assert.Equal(t, mercury.CalculateFee(obs.NativePrice.Val, 100), mustDecodeBigInt(p.NativeFee)) + + fee := mercury.CalculateFee(obs.NativePrice.Val, 100) + assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) assert.True(t, p.NativeFeeValid) }) @@ -585,8 +591,6 @@ func Test_Plugin_Observation(t *testing.T) { assert.Zero(t, p.BenchmarkPrice) assert.False(t, p.PricesValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) }) t.Run("encoding fails on all observations", func(t *testing.T) { @@ -597,6 +601,7 @@ func Test_Plugin_Observation(t *testing.T) { MaxFinalizedTimestamp: mercury.ObsResult[int64]{ Val: rand.Int63(), }, + // encoding never fails on calculated fees LinkPrice: mercury.ObsResult[*big.Int]{ Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), }, @@ -615,10 +620,6 @@ func Test_Plugin_Observation(t *testing.T) { assert.Zero(t, p.BenchmarkPrice) assert.False(t, p.PricesValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) - assert.Zero(t, p.NativeFee) - assert.False(t, p.NativeFeeValid) }) } diff --git a/pkg/reportingplugins/mercury/v3/mercury_test.go b/pkg/reportingplugins/mercury/v3/mercury_test.go index 6e72ea01469..187db5caba3 100644 --- a/pkg/reportingplugins/mercury/v3/mercury_test.go +++ b/pkg/reportingplugins/mercury/v3/mercury_test.go @@ -494,9 +494,13 @@ func Test_Plugin_Observation(t *testing.T) { assert.True(t, p.PricesValid) assert.Equal(t, obs.MaxFinalizedTimestamp.Val, p.MaxFinalizedTimestamp) assert.True(t, p.MaxFinalizedTimestampValid) - assert.Equal(t, mercury.CalculateFee(obs.LinkPrice.Val, 100), mustDecodeBigInt(p.LinkFee)) + + fee := mercury.CalculateFee(obs.LinkPrice.Val, 100) + assert.Equal(t, fee, mustDecodeBigInt(p.LinkFee)) assert.True(t, p.LinkFeeValid) - assert.Equal(t, mercury.CalculateFee(obs.NativePrice.Val, 100), mustDecodeBigInt(p.NativeFee)) + + fee = mercury.CalculateFee(obs.NativePrice.Val, 100) + assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) assert.True(t, p.NativeFeeValid) }) @@ -557,7 +561,9 @@ func Test_Plugin_Observation(t *testing.T) { assert.False(t, p.MaxFinalizedTimestampValid) assert.Zero(t, p.LinkFee) assert.False(t, p.LinkFeeValid) - assert.Equal(t, mercury.CalculateFee(obs.NativePrice.Val, 100), mustDecodeBigInt(p.NativeFee)) + + fee := mercury.CalculateFee(obs.NativePrice.Val, 100) + assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) assert.True(t, p.NativeFeeValid) }) @@ -630,8 +636,6 @@ func Test_Plugin_Observation(t *testing.T) { assert.Zero(t, p.BenchmarkPrice) assert.False(t, p.PricesValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) }) t.Run("encoding fails on all observations", func(t *testing.T) { @@ -648,6 +652,7 @@ func Test_Plugin_Observation(t *testing.T) { MaxFinalizedTimestamp: mercury.ObsResult[int64]{ Val: rand.Int63(), }, + // encoding never fails on calculated fees LinkPrice: mercury.ObsResult[*big.Int]{ Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), }, @@ -668,10 +673,6 @@ func Test_Plugin_Observation(t *testing.T) { assert.Zero(t, p.Bid) assert.Zero(t, p.Ask) assert.False(t, p.PricesValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) - assert.Zero(t, p.NativeFee) - assert.False(t, p.NativeFeeValid) }) }