diff --git a/core/services/llo/evm/fees.go b/core/services/llo/evm/fees.go index b74d68b08d2..a6ff7a31178 100644 --- a/core/services/llo/evm/fees.go +++ b/core/services/llo/evm/fees.go @@ -16,8 +16,9 @@ const Precision int32 = 18 // CalculateFee outputs a fee in wei according to the formula: baseUSDFee / tokenPriceInUSD func CalculateFee(tokenPriceInUSD decimal.Decimal, baseUSDFee decimal.Decimal) *big.Int { - if tokenPriceInUSD.IsZero() || baseUSDFee.IsZero() { + if baseUSDFee.IsZero() || baseUSDFee.IsNegative() || tokenPriceInUSD.IsZero() || tokenPriceInUSD.IsNegative() { // zero fee if token price or base fee is zero + // if either fee should somehow be negative, also, return zero return big.NewInt(0) } diff --git a/core/services/llo/evm/fees_test.go b/core/services/llo/evm/fees_test.go index 33888de14ec..4f3fedbaedc 100644 --- a/core/services/llo/evm/fees_test.go +++ b/core/services/llo/evm/fees_test.go @@ -38,8 +38,27 @@ func Test_Fees(t *testing.T) { t.Run("with base fee == 0", func(t *testing.T) { tokenPriceInUSD := decimal.NewFromInt32(123) - BaseUSDFee = decimal.NewFromInt32(0) - fee := CalculateFee(tokenPriceInUSD, BaseUSDFee) + baseUSDFee := decimal.NewFromInt32(0) + fee := CalculateFee(tokenPriceInUSD, baseUSDFee) + assert.Equal(t, big.NewInt(0), fee) + }) + + t.Run("negative fee rounds up to zero", func(t *testing.T) { + tokenPriceInUSD := decimal.NewFromInt32(-123) + baseUSDFee := decimal.NewFromInt32(1) + fee := CalculateFee(tokenPriceInUSD, baseUSDFee) + assert.Equal(t, big.NewInt(0), fee) + + tokenPriceInUSD = decimal.NewFromInt32(123) + baseUSDFee = decimal.NewFromInt32(-1) + fee = CalculateFee(tokenPriceInUSD, baseUSDFee) + assert.Equal(t, big.NewInt(0), fee) + + // Multiple negative values also return a zero fee since negative + // prices are always nonsensical + tokenPriceInUSD = decimal.NewFromInt32(-123) + baseUSDFee = decimal.NewFromInt32(-1) + fee = CalculateFee(tokenPriceInUSD, baseUSDFee) assert.Equal(t, big.NewInt(0), fee) }) diff --git a/core/services/llo/evm/report_codec_evm_abi_encode_unpacked.go b/core/services/llo/evm/report_codec_evm_abi_encode_unpacked.go index 64c57cb41a6..58a8acc98f7 100644 --- a/core/services/llo/evm/report_codec_evm_abi_encode_unpacked.go +++ b/core/services/llo/evm/report_codec_evm_abi_encode_unpacked.go @@ -43,10 +43,13 @@ type ReportFormatEVMABIEncodeOpts struct { FeedID common.Hash `json:"feedID"` // ABI defines the encoding of the payload. Each element maps to exactly // one stream (although sub-arrays may be specified for streams that - // produce a composite data type). Example ABIs: + // produce a composite data type). // - // ["int192","bool"] - // [["int192","int192","int192"]] + // EXAMPLE + // + // [{"streamID":123,"multiplier":10000,"type":"uint192"}, ...] + // + // See definition of ABIEncoder struct for more details. // // The total number of streams must be 2+n, where n is the number of // top-level elements in this ABI array (stream 0 is always the native @@ -54,6 +57,7 @@ type ReportFormatEVMABIEncodeOpts struct { ABI []ABIEncoder `json:"abi"` } +// TODO: test Decode/Encode func (r *ReportFormatEVMABIEncodeOpts) Decode(opts []byte) error { return json.Unmarshal(opts, r) } @@ -182,7 +186,7 @@ func (r ReportCodecEVMABIEncodeUnpacked) buildPayload(ctx context.Context, encod if err2 != nil { vStr = []byte(fmt.Sprintf("%v(failed to marshal: %s)", values[i], err2)) } - merr = errors.Join(merr, fmt.Errorf("failed to encode stream value %s at index %d with abi %q; %w", string(vStr), i, encoder.ABI, err)) + merr = errors.Join(merr, fmt.Errorf("failed to encode stream value %s at index %d with abi %q; %w", string(vStr), i, encoder.Type, err)) continue } payload = append(payload, b...) @@ -191,18 +195,18 @@ func (r ReportCodecEVMABIEncodeUnpacked) buildPayload(ctx context.Context, encod return payload, merr } -// type ABIElem struct { -// Field string `json:"field"` -// Multiplier *ubig.Big `json:"multiplier"` -// ABI string `json:"abi"` -// } - // An ABIEncoder encodes exactly one stream value into a byte slice type ABIEncoder struct { - StreamID llotypes.StreamID `json:"streamID"` - FieldName string `json:"fieldName"` - Multiplier *ubig.Big `json:"multiplier"` - ABI string `json:"abi"` // TODO: Rename to "type" for consistency with go-ethereum? + // StreamID is the ID of the stream that this encoder is responsible for. + // MANDATORY + StreamID llotypes.StreamID `json:"streamID"` + // Type is the ABI type of the stream value. E.g. "uint192", "int256", "bool", "string" etc. + // MANDATORY + Type string `json:"type"` + // Multiplier, if provided, will be multiplied with the stream value before + // encoding. + // OPTIONAL + Multiplier *ubig.Big `json:"multiplier"` } // getNormalizedMultiplier returns the multiplier as a decimal.Decimal, defaulting @@ -227,27 +231,9 @@ func (a ABIEncoder) Encode(value llo.StreamValue) ([]byte, error) { if sv == nil { return nil, fmt.Errorf("expected non-nil *Decimal; got: %v", sv) } - return packBigInt(a.applyMultiplier(sv.Decimal()), a.ABI) - case *llo.Quote: - if sv == nil { - return nil, fmt.Errorf("expected non-nil *Quote; got: %v", sv) - } - var val decimal.Decimal - switch a.FieldName { - case "": - return nil, fmt.Errorf("\"fieldName\" must be specified when encoding *Quote type; got: \"fieldName\":%q", a.FieldName) - case "benchmark": - val = sv.Benchmark - case "bid": - val = sv.Bid - case "ask": - val = sv.Ask - default: - return nil, fmt.Errorf("unhandled field name; supported field names are 'benchmark', 'bid', 'ask'; got: %q", a.FieldName) - } - return packBigInt(a.applyMultiplier(val), a.ABI) + return packBigInt(a.applyMultiplier(sv.Decimal()), a.Type) default: - return nil, fmt.Errorf("unhandled type; supported types are *llo.Decimal or *llo.Quote; got: %T", value) + return nil, fmt.Errorf("unhandled type; supported types are: *llo.Decimal; got: %T", value) } } @@ -274,23 +260,3 @@ func packBigInt(val *big.Int, t string) ([]byte, error) { return packedData, nil } - -// EXAMPLE -// -// [{"streamID":123,"fieldName":"benchmark","multiplier":10000,"type":"uint192"}, ...] - -// func abiEncode(abi interface{}, value interface{}) ([]byte, error) { -// // TODO: How to handle just taking benchmark, or bid, etc? -// benchmarkType, err := abi.NewType(abi[0], "", []abi.ArgumentMarshaling{}) -// if err != nil { -// panic(fmt.Sprintf("Unexpected error during abi.NewType: %s", err)) -// } -// return result -// } - -// // abi.Arguments([]abi.Argument{ -// // {Name: "feedId", Type: mustNewType("bytes32")} -// default: -// return nil, fmt.Errorf("expected *Decimal or *Quote; got: %T", value) -// } -// } diff --git a/core/services/llo/evm/report_codec_evm_abi_encode_unpacked_test.go b/core/services/llo/evm/report_codec_evm_abi_encode_unpacked_test.go index 03ea4b51706..c106e15b456 100644 --- a/core/services/llo/evm/report_codec_evm_abi_encode_unpacked_test.go +++ b/core/services/llo/evm/report_codec_evm_abi_encode_unpacked_test.go @@ -4,6 +4,7 @@ import ( "fmt" "math/big" "math/rand/v2" + "reflect" "testing" "github.com/ethereum/go-ethereum/accounts/abi" @@ -34,6 +35,43 @@ func AllTrue(arr []bool) bool { return true } +func TestReportFormatEVMABIEncodeOpts_Decode_Encode_properties(t *testing.T) { + properties := gopter.NewProperties(nil) + + runTest := func(opts ReportFormatEVMABIEncodeOpts) bool { + encoded, err := opts.Encode() + require.NoError(t, err) + + decoded := ReportFormatEVMABIEncodeOpts{} + err = decoded.Decode(encoded) + require.NoError(t, err) + + return decoded.BaseUSDFee.Equal(opts.BaseUSDFee) && decoded.ExpirationWindow == opts.ExpirationWindow && decoded.FeedID == opts.FeedID && assert.Equal(t, opts.ABI, decoded.ABI) + } + properties.Property("Encodes values", prop.ForAll( + runTest, + gen.StrictStruct(reflect.TypeOf(&ReportFormatEVMABIEncodeOpts{}), map[string]gopter.Gen{ + "BaseUSDFee": genBaseUSDFee(), + "ExpirationWindow": genExpirationWindow(), + "FeedID": genFeedID(), + "ABI": genABI(), + }))) + + properties.TestingRun(t) +} + +func genABI() gopter.Gen { + return gen.SliceOf(genABIEncoder()) +} + +func genABIEncoder() gopter.Gen { + return gen.StrictStruct(reflect.TypeOf(&ABIEncoder{}), map[string]gopter.Gen{ + "StreamID": gen.UInt32().Map(func(i uint32) llotypes.StreamID { return llotypes.StreamID(i) }), + "Multiplier": genMultiplier(), + "Type": gen.AnyString(), + }) +} + func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) { ctx := tests.Context(t) codec := ReportCodecEVMABIEncodeUnpacked{} @@ -41,7 +79,7 @@ func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) { properties := gopter.NewProperties(nil) t.Run("DEX-based asset schema example", func(t *testing.T) { - runTest := func(sampleFeedID common.Hash, sampleObservationsTimestamp, sampleValidAfterSeconds, sampleExpirationWindow uint32, priceMultiplier, marketDepthMultiplier *ubig.Big, sampleBaseUSDFee, sampleLinkBenchmarkPrice, sampleNativeBenchmarkPrice, sampleBenchmarkPrice, sampleBaseMarketDepth, sampleQuoteMarketDepth decimal.Decimal) bool { + runTest := func(sampleFeedID common.Hash, sampleObservationsTimestamp, sampleValidAfterSeconds, sampleExpirationWindow uint32, priceMultiplier, marketDepthMultiplier *ubig.Big, sampleBaseUSDFee, sampleLinkBenchmarkPrice, sampleNativeBenchmarkPrice, sampleDexBasedAssetPrice, sampleBaseMarketDepth, sampleQuoteMarketDepth decimal.Decimal) bool { report := llo.Report{ ConfigDigest: types.ConfigDigest{0x01}, SeqNr: 0x02, @@ -51,16 +89,16 @@ func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) { Values: []llo.StreamValue{ &llo.Quote{Bid: decimal.NewFromFloat(6.1), Benchmark: sampleLinkBenchmarkPrice, Ask: decimal.NewFromFloat(8.2332)}, // Link price &llo.Quote{Bid: decimal.NewFromFloat(9.4), Benchmark: sampleNativeBenchmarkPrice, Ask: decimal.NewFromFloat(11.33)}, // Native price - &llo.Quote{Bid: decimal.NewFromFloat(12.6), Benchmark: sampleBenchmarkPrice, Ask: decimal.NewFromFloat(14.9)}, // DEX-based asset price - llo.ToDecimal(sampleBaseMarketDepth), // Base market depth - llo.ToDecimal(sampleQuoteMarketDepth), // Quote market depth + llo.ToDecimal(sampleDexBasedAssetPrice), // DEX-based asset price + llo.ToDecimal(sampleBaseMarketDepth), // Base market depth + llo.ToDecimal(sampleQuoteMarketDepth), // Quote market depth }, Specimen: false, } linkQuoteStreamID := llotypes.StreamID(rand.Uint32()) ethQuoteStreamID := llotypes.StreamID(rand.Uint32()) - dexBasedAssetQuoteStreamID := llotypes.StreamID(rand.Uint32()) + dexBasedAssetDecimalStreamID := llotypes.StreamID(rand.Uint32()) baseMarketDepthStreamID := llotypes.StreamID(rand.Uint32()) quoteMarketDepthStreamID := llotypes.StreamID(rand.Uint32()) @@ -71,21 +109,20 @@ func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) { ABI: []ABIEncoder{ // benchmark price ABIEncoder{ - StreamID: dexBasedAssetQuoteStreamID, - FieldName: "benchmark", - ABI: "int192", + StreamID: dexBasedAssetDecimalStreamID, + Type: "int192", Multiplier: priceMultiplier, // TODO: Default multiplier? }, // base market depth ABIEncoder{ StreamID: baseMarketDepthStreamID, - ABI: "int192", + Type: "int192", Multiplier: marketDepthMultiplier, }, // quote market depth ABIEncoder{ StreamID: quoteMarketDepthStreamID, - ABI: "int192", + Type: "int192", Multiplier: marketDepthMultiplier, }, }, @@ -106,7 +143,7 @@ func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) { Aggregator: llotypes.AggregatorMedian, }, { - StreamID: dexBasedAssetQuoteStreamID, + StreamID: dexBasedAssetDecimalStreamID, Aggregator: llotypes.AggregatorQuote, }, { @@ -148,10 +185,10 @@ func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) { assert.Equal(t, sampleFeedID, (common.Hash)(values[0].([32]byte))), // feedId assert.Equal(t, uint32(sampleValidAfterSeconds+1), values[1].(uint32)), // validFromTimestamp assert.Equal(t, sampleObservationsTimestamp, values[2].(uint32)), // observationsTimestamp - assert.Equal(t, expectedLinkFee, values[3].(*big.Int)), // linkFee - assert.Equal(t, expectedNativeFee, values[4].(*big.Int)), // nativeFee + assert.Equal(t, expectedLinkFee.String(), values[3].(*big.Int).String()), // linkFee + assert.Equal(t, expectedNativeFee.String(), values[4].(*big.Int).String()), // nativeFee assert.Equal(t, uint32(sampleObservationsTimestamp+sampleExpirationWindow), values[5].(uint32)), // expiresAt - assert.Equal(t, sampleBenchmarkPrice.Mul(decimal.NewFromBigInt(priceMultiplier.ToInt(), 0)).BigInt(), values[6].(*big.Int)), // price + assert.Equal(t, sampleDexBasedAssetPrice.Mul(decimal.NewFromBigInt(priceMultiplier.ToInt(), 0)).BigInt(), values[6].(*big.Int)), // price assert.Equal(t, sampleBaseMarketDepth.Mul(decimal.NewFromBigInt(marketDepthMultiplier.ToInt(), 0)).BigInt(), values[7].(*big.Int)), // baseMarketDepth assert.Equal(t, sampleQuoteMarketDepth.Mul(decimal.NewFromBigInt(marketDepthMultiplier.ToInt(), 0)).BigInt(), values[8].(*big.Int)), // quoteMarketDepth }) @@ -172,6 +209,8 @@ func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) { genBaseMarketDepth(), genQuoteMarketDepth(), )) + + properties.TestingRun(t) }) } @@ -196,39 +235,45 @@ func genExpirationWindow() gopter.Gen { } func genPriceMultiplier() gopter.Gen { - return gen.UInt32().Map(func(i uint32) *ubig.Big { - return ubig.NewI(int64(i)) - }) + return genMultiplier() } func genMarketDepthMultiplier() gopter.Gen { + return genMultiplier() +} + +func genMultiplier() gopter.Gen { return gen.UInt32().Map(func(i uint32) *ubig.Big { return ubig.NewI(int64(i)) }) } +func genDecimal() gopter.Gen { + return gen.Float32Range(-2e32, 2e32).Map(decimal.NewFromFloat32) +} + func genBaseUSDFee() gopter.Gen { - return gen.Float32().Map(decimal.NewFromFloat32) + return genDecimal() } func genLinkBenchmarkPrice() gopter.Gen { - return gen.Float32().Map(decimal.NewFromFloat32) + return genDecimal() } func genNativeBenchmarkPrice() gopter.Gen { - return gen.Float32().Map(decimal.NewFromFloat32) + return genDecimal() } func genBenchmarkPrice() gopter.Gen { - return gen.Float32().Map(decimal.NewFromFloat32) + return genDecimal() } func genBaseMarketDepth() gopter.Gen { - return gen.Float32().Map(decimal.NewFromFloat32) + return genDecimal() } func genQuoteMarketDepth() gopter.Gen { - return gen.Float32().Map(decimal.NewFromFloat32) + return genDecimal() } func mustNewABIType(t string) abi.Type {