Skip to content
This repository was archived by the owner on Mar 24, 2025. It is now read-only.

Commit 6470e30

Browse files
Alex Johnsondavidterpaychenyaoytechnicallyty
authored
feat: currency pair validation for defi assets (#587)
Co-authored-by: David Terpay <[email protected]> Co-authored-by: Chenyao Yu <[email protected]> Co-authored-by: Tyler <[email protected]>
1 parent db702c9 commit 6470e30

File tree

16 files changed

+366
-28
lines changed

16 files changed

+366
-28
lines changed

Diff for: abci/strategies/aggregator/mocks/mock_price_applier.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: abci/strategies/aggregator/mocks/mock_vote_aggregator.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: abci/types/mocks/mock_oracle_keeper.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: abci/ve/vote_extension_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ func (s *VoteExtensionTestSuite) TestExtendVoteStatus() {
677677

678678
s.Run("test price transformation failures", func() {
679679
mockMetrics := metricsmocks.NewMetrics(s.T())
680-
transformationError := fmt.Errorf("incorrectly formatted CurrencyPair: BTCETH")
680+
transformationError := fmt.Errorf("incorrectly formatted CurrencyPair: \"BTCETH\"")
681681
mockClient := mocks.NewOracleClient(s.T())
682682
pamock := aggregatormocks.NewPriceApplier(s.T())
683683
handler := ve.NewVoteExtensionHandler(

Diff for: cmd/client/main.go

-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"google.golang.org/grpc/credentials/insecure"
1616

1717
slinkygrpc "github.com/skip-mev/slinky/pkg/grpc"
18-
1918
"github.com/skip-mev/slinky/service/servers/oracle/types"
2019
)
2120

Diff for: cmd/slinky/main.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,19 @@ import (
88
"os/signal"
99
"syscall"
1010

11-
"github.com/skip-mev/slinky/providers/apis/marketmap"
12-
1311
_ "net/http/pprof" //nolint: gosec
1412

1513
"github.com/spf13/cobra"
1614
"go.uber.org/zap"
1715

16+
"github.com/skip-mev/slinky/cmd/build"
1817
cmdconfig "github.com/skip-mev/slinky/cmd/slinky/config"
1918
"github.com/skip-mev/slinky/oracle"
2019
"github.com/skip-mev/slinky/oracle/config"
21-
22-
"github.com/skip-mev/slinky/cmd/build"
2320
oraclemetrics "github.com/skip-mev/slinky/oracle/metrics"
2421
"github.com/skip-mev/slinky/pkg/log"
2522
oraclemath "github.com/skip-mev/slinky/pkg/math/oracle"
23+
"github.com/skip-mev/slinky/providers/apis/marketmap"
2624
oraclefactory "github.com/skip-mev/slinky/providers/factories/oracle"
2725
mmservicetypes "github.com/skip-mev/slinky/service/clients/marketmap/types"
2826
oracleserver "github.com/skip-mev/slinky/service/servers/oracle"

Diff for: pkg/types/currency_pair.go

+156-9
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import (
66
)
77

88
const (
9-
ethereum = "ETHEREUM"
10-
MaxCPFieldLength = 128
9+
ethereum = "ETHEREUM"
10+
MaxCPFieldLength = 256
11+
fieldSeparator = ","
12+
expectedSplitLength = 3
1113
)
1214

1315
// NewCurrencyPair returns a new CurrencyPair with the given base and quote strings.
@@ -18,19 +20,135 @@ func NewCurrencyPair(base, quote string) CurrencyPair {
1820
}
1921
}
2022

23+
// IsLegacyAssetString returns true if the asset string is of the following format:
24+
// - contains no instances of fieldSeparator.
25+
func IsLegacyAssetString(asset string) bool {
26+
return !strings.Contains(asset, fieldSeparator)
27+
}
28+
29+
// coreValidate performs checks that are universal across any ticker format style, namely:
30+
// - check that base and quote are not empty.
31+
// - check that the length of the base and quote fields do not exceed MaxCPFieldLength.
32+
func (cp *CurrencyPair) coreValidate() error {
33+
if cp.Base == "" {
34+
return fmt.Errorf("base asset cannot be empty")
35+
}
36+
37+
if cp.Quote == "" {
38+
return fmt.Errorf("quote asset cannot be empty")
39+
}
40+
41+
if len(cp.Base) > MaxCPFieldLength {
42+
return fmt.Errorf("base asset exceeds max length of %d", MaxCPFieldLength)
43+
}
44+
45+
if len(cp.Quote) > MaxCPFieldLength {
46+
return fmt.Errorf("quote asset exceeds max length of %d", MaxCPFieldLength)
47+
}
48+
49+
return nil
50+
}
51+
2152
// ValidateBasic checks that the Base / Quote strings in the CurrencyPair are formatted correctly, i.e.
22-
// Base + Quote are non-empty, and are in upper-case.
53+
// For base and quote asset:
54+
// check if the asset is formatted in the legacy validation form:
55+
// - if so, check that fields are not empty and all upper case
56+
// - else, check that the format is in the following form: tokenName,tokenAddress,chainID
2357
func (cp *CurrencyPair) ValidateBasic() error {
58+
if err := cp.coreValidate(); err != nil {
59+
return err
60+
}
61+
62+
if IsLegacyAssetString(cp.Base) {
63+
err := ValidateLegacyAssetString(cp.Base)
64+
if err != nil {
65+
return fmt.Errorf("base asset %q is invalid: %w", cp.Base, err)
66+
}
67+
} else {
68+
err := ValidateDefiAssetString(cp.Base)
69+
if err != nil {
70+
return fmt.Errorf("base defi asset %q is invalid: %w", cp.Base, err)
71+
}
72+
}
73+
74+
// check quote asset
75+
if IsLegacyAssetString(cp.Quote) {
76+
err := ValidateLegacyAssetString(cp.Quote)
77+
if err != nil {
78+
return fmt.Errorf("quote asset %q is invalid: %w", cp.Quote, err)
79+
}
80+
} else {
81+
err := ValidateDefiAssetString(cp.Quote)
82+
if err != nil {
83+
return fmt.Errorf("quote defi asset %q is invalid: %w", cp.Quote, err)
84+
}
85+
}
86+
87+
return nil
88+
}
89+
90+
// ValidateLegacyAssetString checks if the asset string is formatted correctly, i.e.
91+
// - asset string is fully uppercase
92+
// - asset string does not contain the `fieldSeparator`
93+
//
94+
// NOTE: this function assumes that coreValidate() has already been run.
95+
func ValidateLegacyAssetString(asset string) error {
96+
// check formatting of asset
97+
if strings.ToUpper(asset) != asset {
98+
return fmt.Errorf("incorrectly formatted asset string, expected: %q got: %q", strings.ToUpper(asset), asset)
99+
}
100+
101+
if !IsLegacyAssetString(asset) {
102+
return fmt.Errorf("incorrectly formatted asset string, asset %q should not contain the %q character", asset,
103+
fieldSeparator)
104+
}
105+
106+
return nil
107+
}
108+
109+
// ValidateDefiAssetString checks that the asset string is formatted properly as a defi asset (tokenName,tokenAddress,chainID)
110+
// - check that the length of fields separated by fieldSeparator is expectedSplitLength
111+
// - check that the first split (tokenName) is formatted properly as a LegacyAssetString.
112+
//
113+
// NOTE: this function assumes that coreValidate() has already been run.
114+
func ValidateDefiAssetString(asset string) error {
115+
token, _, _, err := SplitDefiAssetString(asset)
116+
if err != nil {
117+
return err
118+
}
119+
120+
// first element is a ticker, so we require it to pass legacy asset validation:
121+
if err := ValidateLegacyAssetString(token); err != nil {
122+
return fmt.Errorf("token field %q is invalid: %w", token, err)
123+
}
124+
125+
return nil
126+
}
127+
128+
// SplitDefiAssetString splits a defi asset by the fieldSeparator and checks that it is the proper length.
129+
// returns the split string as (token, address, chainID).
130+
func SplitDefiAssetString(defiString string) (token, address, chainID string, err error) {
131+
split := strings.Split(defiString, fieldSeparator)
132+
if len(split) != expectedSplitLength {
133+
return "", "", "", fmt.Errorf("asset fields have wrong length, expected: %d got: %d", expectedSplitLength, len(split))
134+
}
135+
return split[0], split[1], split[2], nil
136+
}
137+
138+
// LegacyValidateBasic checks that the Base / Quote strings in the CurrencyPair are formatted correctly, i.e.
139+
// Base + Quote are non-empty, and are in upper-case.
140+
func (cp *CurrencyPair) LegacyValidateBasic() error {
24141
// strings must be valid
25142
if cp.Base == "" || cp.Quote == "" {
26143
return fmt.Errorf("empty quote or base string")
27144
}
28145
// check formatting of base / quote
29146
if strings.ToUpper(cp.Base) != cp.Base {
30-
return fmt.Errorf("incorrectly formatted base string, expected: %s got: %s", strings.ToUpper(cp.Base), cp.Base)
147+
return fmt.Errorf("incorrectly formatted base string, expected: %q got: %q", strings.ToUpper(cp.Base), cp.Base)
31148
}
32149
if strings.ToUpper(cp.Quote) != cp.Quote {
33-
return fmt.Errorf("incorrectly formatted quote string, expected: %s got: %s", strings.ToUpper(cp.Quote), cp.Quote)
150+
return fmt.Errorf("incorrectly formatted quote string, expected: %q got: %q", strings.ToUpper(cp.Quote),
151+
cp.Quote)
34152
}
35153

36154
if len(cp.Base) > MaxCPFieldLength || len(cp.Quote) > MaxCPFieldLength {
@@ -59,21 +177,50 @@ func CurrencyPairString(base, quote string) string {
59177
return cp.String()
60178
}
61179

180+
// CurrencyPairFromString creates a currency pair from a string. Non-capitalized inputs are sanitized and the resulting
181+
// currency pair is validated.
62182
func CurrencyPairFromString(s string) (CurrencyPair, error) {
63183
split := strings.Split(s, "/")
64184
if len(split) != 2 {
65-
return CurrencyPair{}, fmt.Errorf("incorrectly formatted CurrencyPair: %s", s)
185+
return CurrencyPair{}, fmt.Errorf("incorrectly formatted CurrencyPair: %q", s)
186+
}
187+
188+
base, err := sanitizeAssetString(split[0])
189+
if err != nil {
190+
return CurrencyPair{}, err
66191
}
192+
193+
quote, err := sanitizeAssetString(split[1])
194+
if err != nil {
195+
return CurrencyPair{}, err
196+
}
197+
67198
cp := CurrencyPair{
68-
Base: strings.ToUpper(split[0]),
69-
Quote: strings.ToUpper(split[1]),
199+
Base: base,
200+
Quote: quote,
70201
}
71202

72203
return cp, cp.ValidateBasic()
73204
}
74205

206+
func sanitizeAssetString(s string) (string, error) {
207+
if IsLegacyAssetString(s) {
208+
s = strings.ToUpper(s)
209+
} else {
210+
token, address, chainID, err := SplitDefiAssetString(s)
211+
if err != nil {
212+
return "", fmt.Errorf("incorrectly formatted asset: %q: %w", s, err)
213+
}
214+
215+
token = strings.ToUpper(token)
216+
s = strings.Join([]string{token, address, chainID}, fieldSeparator)
217+
}
218+
219+
return s, nil
220+
}
221+
75222
// LegacyDecimals returns the number of decimals that the quote will be reported to. If the quote is Ethereum, then
76-
// the number of decimals is 18. Otherwise, the decimals will be reorted to 8.
223+
// the number of decimals is 18. Otherwise, the decimals will be reported as 8.
77224
func (cp *CurrencyPair) LegacyDecimals() int {
78225
if strings.ToUpper(cp.Quote) == ethereum {
79226
return 18

0 commit comments

Comments
 (0)