Skip to content

Commit

Permalink
Do not support composite types
Browse files Browse the repository at this point in the history
  • Loading branch information
samsondav committed Dec 4, 2024
1 parent 915f197 commit 48f3df2
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 80 deletions.
3 changes: 2 additions & 1 deletion core/services/llo/evm/fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
23 changes: 21 additions & 2 deletions core/services/llo/evm/fees_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand Down
74 changes: 20 additions & 54 deletions core/services/llo/evm/report_codec_evm_abi_encode_unpacked.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,21 @@ 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
// token price and stream 1 is the link token price).
ABI []ABIEncoder `json:"abi"`
}

// TODO: test Decode/Encode
func (r *ReportFormatEVMABIEncodeOpts) Decode(opts []byte) error {
return json.Unmarshal(opts, r)
}
Expand Down Expand Up @@ -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...)
Expand All @@ -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
Expand All @@ -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)
}
}

Expand All @@ -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)
// }
// }
91 changes: 68 additions & 23 deletions core/services/llo/evm/report_codec_evm_abi_encode_unpacked_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"math/big"
"math/rand/v2"
"reflect"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi"
Expand Down Expand Up @@ -34,14 +35,51 @@ 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{}

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,
Expand All @@ -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())

Expand All @@ -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,
},
},
Expand All @@ -106,7 +143,7 @@ func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) {
Aggregator: llotypes.AggregatorMedian,
},
{
StreamID: dexBasedAssetQuoteStreamID,
StreamID: dexBasedAssetDecimalStreamID,
Aggregator: llotypes.AggregatorQuote,
},
{
Expand Down Expand Up @@ -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
})
Expand All @@ -172,6 +209,8 @@ func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) {
genBaseMarketDepth(),
genQuoteMarketDepth(),
))

properties.TestingRun(t)
})
}

Expand All @@ -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 {
Expand Down

0 comments on commit 48f3df2

Please sign in to comment.