Skip to content

Commit

Permalink
feat(relconfig): decimals config validation (#2919)
Browse files Browse the repository at this point in the history
* decimals implementation

* add test

* add test

* lint

* [goreleaser]

* handle native gas token

* combine into Validate func

* commnent

* [goreleaser]

* better call to loadconfig and abstract away validate

* abstract away validate

* [goreleaser]

---------

Co-authored-by: Trajan0x <trajan0x@users.noreply.github.com>
  • Loading branch information
golangisfun123 and trajan0x authored Aug 5, 2024
1 parent 6b4f1ed commit 48de090
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 10 deletions.
5 changes: 3 additions & 2 deletions services/rfq/relayer/cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ var runCommand = &cli.Command{
Flags: []cli.Flag{configFlag, &commandline.LogLevel},
Action: func(c *cli.Context) (err error) {
commandline.SetLogLevel(c)

metricsProvider := metrics.Get()

cfg, err := relconfig.LoadConfig(core.ExpandOrReturnPath(c.String(configFlag.Name)))
if err != nil {
return fmt.Errorf("could not read config file: %w", err)
}

metricsProvider := metrics.Get()

relayer, err := service.NewRelayer(c.Context, metricsProvider, cfg)
if err != nil {
return fmt.Errorf("could not create relayer: %w", err)
Expand Down
62 changes: 58 additions & 4 deletions services/rfq/relayer/relconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,28 @@
package relconfig

import (
"context"
"fmt"
"math"
"os"
"strconv"
"strings"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/jftuga/ellipsis"
"github.com/synapsecns/sanguine/core/metrics"
"github.com/synapsecns/sanguine/ethergo/signer/config"
submitterConfig "github.com/synapsecns/sanguine/ethergo/submitter/config"
cctpConfig "github.com/synapsecns/sanguine/services/cctp-relayer/config"
"github.com/synapsecns/sanguine/services/rfq/contracts/ierc20"
"github.com/synapsecns/sanguine/services/rfq/relayer/chain"
"gopkg.in/yaml.v2"

"path/filepath"

omniClient "github.com/synapsecns/sanguine/services/omnirpc/client"
)

// Config represents the configuration for the relayer.
Expand Down Expand Up @@ -199,15 +206,18 @@ func LoadConfig(path string) (config Config, err error) {
if err != nil {
return Config{}, fmt.Errorf("could not unmarshall config %s: %w", ellipsis.Shorten(string(input), 30), err)
}
err = config.Validate()
omniClient := omniClient.NewOmnirpcClient(config.OmniRPCURL, metrics.NewNullHandler(), omniClient.WithCaptureReqRes())
err = config.Validate(context.Background(), omniClient)
if err != nil {
return config, fmt.Errorf("error validating config: %w", err)
return Config{}, fmt.Errorf("config validation failed: %w", err)
}

return config, nil
}

// Validate validates the config.
func (c Config) Validate() (err error) {
// Validate validates the config. Omniclient may be nil, but if not then it will also check the chain to see if the decimals
// match the actual token decimals.
func (c Config) Validate(ctx context.Context, omniclient omniClient.RPCClient) (err error) {
maintenancePctSums := map[string]float64{}
initialPctSums := map[string]float64{}
for _, chainCfg := range c.Chains {
Expand All @@ -228,5 +238,49 @@ func (c Config) Validate() (err error) {
return fmt.Errorf("total initial percent does not total 100 for %s: %f", token, sum)
}
}

if omniclient != nil {
err = c.validateTokenDecimals(ctx, omniclient)
if err != nil {
return fmt.Errorf("error validating token decimals: %w", err)
}
}

return nil
}

// ValidateTokenDecimals calls decimals() on the ERC20s to ensure that the decimals in the config match the actual token decimals.
func (c Config) validateTokenDecimals(ctx context.Context, omniClient omniClient.RPCClient) (err error) {
for chainID, chainCfg := range c.Chains {
for tokenName, tokenCFG := range chainCfg.Tokens {
chainClient, err := omniClient.GetChainClient(ctx, chainID)
if err != nil {
return fmt.Errorf("could not get chain client for chain %d: %w", chainID, err)
}

// Check if the token is the gas token. SHOULD BE 18.
if tokenCFG.Address == chain.EthAddress.String() {
if tokenCFG.Decimals != 18 {
return fmt.Errorf("decimals mismatch for token %s on chain %d: expected 18, got %d", tokenName, chainID, tokenCFG.Decimals)
}
continue
}

ierc20, err := ierc20.NewIERC20(common.HexToAddress(tokenCFG.Address), chainClient)
if err != nil {
return fmt.Errorf("could not create caller for token %s at address %s on chain %d: %w", tokenName, tokenCFG.Address, chainID, err)
}

actualDecimals, err := ierc20.Decimals(&bind.CallOpts{Context: ctx})
if err != nil {
return fmt.Errorf("could not get decimals for token %s on chain %d: %w", tokenName, chainID, err)
}

if actualDecimals != tokenCFG.Decimals {
return fmt.Errorf("decimals mismatch for token %s on chain %d: expected %d, got %d", tokenName, chainID, tokenCFG.Decimals, actualDecimals)
}
}
}

return nil
}
82 changes: 78 additions & 4 deletions services/rfq/relayer/relconfig/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package relconfig_test

import (
"context"
"testing"
"time"

Expand Down Expand Up @@ -372,7 +373,7 @@ func TestValidation(t *testing.T) {
},
},
}
err := cfg.Validate()
err := cfg.Validate(context.Background(), nil)
assert.Nil(t, err)
})

Expand All @@ -399,7 +400,7 @@ func TestValidation(t *testing.T) {
},
},
}
err := cfg.Validate()
err := cfg.Validate(context.Background(), nil)
assert.NotNil(t, err)
assert.Equal(t, "total initial percent does not total 100 for USDC: 101.000000", err.Error())
})
Expand Down Expand Up @@ -427,7 +428,7 @@ func TestValidation(t *testing.T) {
},
},
}
err := cfg.Validate()
err := cfg.Validate(context.Background(), nil)
assert.NotNil(t, err)
assert.Equal(t, "total maintenance percent exceeds 100 for USDC: 100.100000", err.Error())
})
Expand All @@ -453,7 +454,7 @@ func TestValidation(t *testing.T) {
},
},
}
err := cfg.Validate()
err := cfg.Validate(context.Background(), nil)
assert.Nil(t, err)
})
}
Expand Down Expand Up @@ -504,3 +505,76 @@ func TestDecodeTokenID(t *testing.T) {
})
}
}

const usdcAddr = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
const arbAddr = "0x912CE59144191C1204E64559FE8253a0e49E6548"
const opAddr = "0x4200000000000000000000000000000000000042"

func (v *ValidateDecimalsSuite) TestValidateWrongDecimals() {
cfg := relconfig.Config{
Chains: map[int]relconfig.ChainConfig{
1: {
Tokens: map[string]relconfig.TokenConfig{
"USDC": {
Address: usdcAddr,
Decimals: 18, // WRONG
},
},
},
},
}
err := cfg.Validate(v.GetTestContext(), v.omniClient)
// we should error because the decimals are wrong
v.Require().Error(err)
}

func (v *ValidateDecimalsSuite) TestValidateCorrectDecimals() {
cfg := relconfig.Config{
Chains: map[int]relconfig.ChainConfig{
1: {
Tokens: map[string]relconfig.TokenConfig{
"USDC": {
Address: usdcAddr,
Decimals: 6,
},
},
},
},
}
err := cfg.Validate(v.GetTestContext(), v.omniClient)
v.Require().NoError(err)
}

func (v *ValidateDecimalsSuite) TestMixtureDecimals() {
cfg := relconfig.Config{
Chains: map[int]relconfig.ChainConfig{
1: {
Tokens: map[string]relconfig.TokenConfig{
"USDC": {
Address: usdcAddr,
Decimals: 6,
},
},
},
42161: {
Tokens: map[string]relconfig.TokenConfig{
"ARB": {
Address: arbAddr,
Decimals: 18,
},
},
},
10: {
Tokens: map[string]relconfig.TokenConfig{
"OP": {
Address: opAddr,
Decimals: 69,
},
},
},
},
}

err := cfg.Validate(v.GetTestContext(), v.omniClient)
v.Require().Error(err)
}
50 changes: 50 additions & 0 deletions services/rfq/relayer/relconfig/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package relconfig_test

import (
"testing"

"github.com/stretchr/testify/suite"
"github.com/synapsecns/sanguine/core"
"github.com/synapsecns/sanguine/core/metrics"
"github.com/synapsecns/sanguine/core/metrics/localmetrics"
"github.com/synapsecns/sanguine/core/testsuite"
omniClient "github.com/synapsecns/sanguine/services/omnirpc/client"
"github.com/synapsecns/sanguine/services/rfq/relayer/metadata"
)

func TestValidateDecimalsSuite(t *testing.T) {
suite.Run(t, NewTestSuite(t))
}

type ValidateDecimalsSuite struct {
*testsuite.TestSuite
// testBackends contains a list of all test backends
metricsHandler metrics.Handler
omniClient omniClient.RPCClient
}

// NewTestSuite creates a new test suite.
func NewTestSuite(tb testing.TB) *ValidateDecimalsSuite {
tb.Helper()
return &ValidateDecimalsSuite{
TestSuite: testsuite.NewTestSuite(tb),
}
}

func (v *ValidateDecimalsSuite) SetupSuite() {
v.TestSuite.SetupSuite()

var err error
// don't use metrics on ci for integration tests
isCI := core.GetEnvBool("CI", false)
metricsHandler := metrics.Null

if !isCI {
localmetrics.SetupTestJaeger(v.GetSuiteContext(), v.T())
metricsHandler = metrics.Jaeger
}
v.metricsHandler, err = metrics.NewByType(v.GetSuiteContext(), metadata.BuildInfo(), metricsHandler)
v.Require().NoError(err)

v.omniClient = omniClient.NewOmnirpcClient("https://rpc.omnirpc.io", v.metricsHandler, omniClient.WithCaptureReqRes())
}

0 comments on commit 48de090

Please sign in to comment.