From 69ab2458db9aff5d61e262d6b149e7b5479a6918 Mon Sep 17 00:00:00 2001 From: Oliver Townsend Date: Fri, 11 Oct 2024 15:51:58 -0700 Subject: [PATCH] Add new custom calldata DA oracle --- .changeset/metal-forks-arrive.md | 5 + .../chains/evm/config/toml/daoracle/config.go | 7 +- .../gas/rollups/custom_calldata_l1_oracle.go | 159 ++++++++++++++++++ .../rollups/custom_calldata_l1_oracle_test.go | 55 ++++++ core/config/docs/chains-evm.toml | 2 +- docs/CONFIG.md | 2 +- 6 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 .changeset/metal-forks-arrive.md create mode 100644 core/chains/evm/gas/rollups/custom_calldata_l1_oracle.go create mode 100644 core/chains/evm/gas/rollups/custom_calldata_l1_oracle_test.go diff --git a/.changeset/metal-forks-arrive.md b/.changeset/metal-forks-arrive.md new file mode 100644 index 00000000000..784f7e0dc05 --- /dev/null +++ b/.changeset/metal-forks-arrive.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Adds new custom calldata DA oracle #wip diff --git a/core/chains/evm/config/toml/daoracle/config.go b/core/chains/evm/config/toml/daoracle/config.go index 425b87ba090..d1c7c0293b5 100644 --- a/core/chains/evm/config/toml/daoracle/config.go +++ b/core/chains/evm/config/toml/daoracle/config.go @@ -5,9 +5,10 @@ import "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" type OracleType string const ( - OPStack = OracleType("opstack") - Arbitrum = OracleType("arbitrum") - ZKSync = OracleType("zksync") + OPStack = OracleType("opstack") + Arbitrum = OracleType("arbitrum") + ZKSync = OracleType("zksync") + CustomCalldata = OracleType("custom_calldata") ) type DAOracle struct { diff --git a/core/chains/evm/gas/rollups/custom_calldata_l1_oracle.go b/core/chains/evm/gas/rollups/custom_calldata_l1_oracle.go new file mode 100644 index 00000000000..22698fca26a --- /dev/null +++ b/core/chains/evm/gas/rollups/custom_calldata_l1_oracle.go @@ -0,0 +1,159 @@ +package rollups + +import ( + "context" + "encoding/hex" + "fmt" + "math/big" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" +) + +type customCalldataDAOracle struct { + services.StateMachine + client l1OracleClient + pollPeriod time.Duration + logger logger.SugaredLogger + + daOracleConfig evmconfig.DAOracle + daGasPriceMu sync.RWMutex + daGasPrice priceEntry + + chInitialized chan struct{} + chStop services.StopChan + chDone chan struct{} +} + +// NewCustomCalldataDAOracle creates a new custom calldata DA oracle. The CustomCalldataDAOracle fetches gas price from +// whatever function is specified in the DAOracle's CustomGasPriceCalldata field. This allows for more flexibility when +// chains have custom DA gas calculation methods. +func NewCustomCalldataDAOracle(lggr logger.Logger, ethClient l1OracleClient, daOracleConfig evmconfig.DAOracle) *customCalldataDAOracle { + return &customCalldataDAOracle{ + client: ethClient, + pollPeriod: PollPeriod, + logger: logger.Sugared(logger.Named(lggr, fmt.Sprintf("CustomCalldataDAOracle(%s)", daOracleConfig.OracleType()))), + + daOracleConfig: daOracleConfig, + + chInitialized: make(chan struct{}), + chStop: make(chan struct{}), + chDone: make(chan struct{}), + } +} + +func (o *customCalldataDAOracle) Name() string { + return o.logger.Name() +} + +func (o *customCalldataDAOracle) Start(_ context.Context) error { + return o.StartOnce(o.Name(), func() error { + go o.run() + <-o.chInitialized + return nil + }) +} + +func (o *customCalldataDAOracle) Close() error { + return o.StopOnce(o.Name(), func() error { + close(o.chStop) + <-o.chDone + return nil + }) +} + +func (o *customCalldataDAOracle) HealthReport() map[string]error { + return map[string]error{o.Name(): o.Healthy()} +} + +func (o *customCalldataDAOracle) run() { + defer close(o.chDone) + + o.refresh() + close(o.chInitialized) + + t := services.TickerConfig{ + Initial: o.pollPeriod, + JitterPct: services.DefaultJitter, + }.NewTicker(o.pollPeriod) + defer t.Stop() + + for { + select { + case <-o.chStop: + return + case <-t.C: + o.refresh() + } + } +} + +func (o *customCalldataDAOracle) refresh() { + err := o.refreshWithError() + if err != nil { + o.logger.Criticalw("Failed to refresh gas price", "err", err) + o.SvcErrBuffer.Append(err) + } +} + +func (o *customCalldataDAOracle) refreshWithError() error { + ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer cancel() + + price, err := o.getCustomCalldataGasPrice(ctx) + if err != nil { + return err + } + + o.daGasPriceMu.Lock() + defer o.daGasPriceMu.Unlock() + o.daGasPrice = priceEntry{price: assets.NewWei(price), timestamp: time.Now()} + return nil +} + +func (o *customCalldataDAOracle) GasPrice(_ context.Context) (daGasPrice *assets.Wei, err error) { + var timestamp time.Time + ok := o.IfStarted(func() { + o.daGasPriceMu.RLock() + daGasPrice = o.daGasPrice.price + timestamp = o.daGasPrice.timestamp + o.daGasPriceMu.RUnlock() + }) + if !ok { + return daGasPrice, fmt.Errorf("DAGasOracle is not started; cannot estimate gas") + } + if daGasPrice == nil { + return daGasPrice, fmt.Errorf("failed to get DA gas price; gas price not set") + } + // Validate the price has been updated within the pollPeriod * 2 + // Allowing double the poll period before declaring the price stale to give ample time for the refresh to process + if time.Since(timestamp) > o.pollPeriod*2 { + return daGasPrice, fmt.Errorf("gas price is stale") + } + return +} + +func (o *customCalldataDAOracle) getCustomCalldataGasPrice(ctx context.Context) (*big.Int, error) { + daOracleAddress := o.daOracleConfig.OracleAddress().Address() + calldata := strings.TrimPrefix(o.daOracleConfig.CustomGasPriceCalldata(), "0x") + calldataBytes, err := hex.DecodeString(calldata) + if err != nil { + return nil, fmt.Errorf("failed to decode custom fee method calldata: %w", err) + } + b, err := o.client.CallContract(ctx, ethereum.CallMsg{ + To: &daOracleAddress, + Data: calldataBytes, + }, nil) + if err != nil { + return nil, fmt.Errorf("custom fee method call failed: %w", err) + } + return new(big.Int).SetBytes(b), nil +} diff --git a/core/chains/evm/gas/rollups/custom_calldata_l1_oracle_test.go b/core/chains/evm/gas/rollups/custom_calldata_l1_oracle_test.go new file mode 100644 index 00000000000..35d8c7ea243 --- /dev/null +++ b/core/chains/evm/gas/rollups/custom_calldata_l1_oracle_test.go @@ -0,0 +1,55 @@ +package rollups + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml/daoracle" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +func TestOPL1Oracle_CalculateCustomCalldataGasPrice(t *testing.T) { + oracleAddress := common.HexToAddress("0x0000000000000000000000000000000044433322").String() + + t.Parallel() + + t.Run("correctly fetches gas price if chain has custom calldata", func(t *testing.T) { + ethClient := mocks.NewL1OracleClient(t) + expectedPriceHex := "0x0000000000000000000000000000000000000000000000000000000000000032" + + daOracle := CreateTestDAOracle(t, daoracle.OPStack, oracleAddress, "0x0000000000000000000000000000000000001234") + oracle := NewCustomCalldataDAOracle(logger.Test(t), ethClient, daOracle) + + ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + callMsg := args.Get(1).(ethereum.CallMsg) + blockNumber := args.Get(2).(*big.Int) + require.NotNil(t, callMsg.To) + require.Equal(t, oracleAddress, callMsg.To.String()) + require.Nil(t, blockNumber) + }).Return(hexutil.MustDecode(expectedPriceHex), nil).Once() + + price, err := oracle.getCustomCalldataGasPrice(tests.Context(t)) + require.NoError(t, err) + require.Equal(t, big.NewInt(50), price) + }) + + t.Run("throws error if custom calldata fails to decode", func(t *testing.T) { + ethClient := mocks.NewL1OracleClient(t) + + daOracle := CreateTestDAOracle(t, daoracle.OPStack, oracleAddress, "0xblahblahblah") + oracle := NewCustomCalldataDAOracle(logger.Test(t), ethClient, daOracle) + + _, err := oracle.getCustomCalldataGasPrice(tests.Context(t)) + require.Error(t, err) + }) +} diff --git a/core/config/docs/chains-evm.toml b/core/config/docs/chains-evm.toml index 683e59d74b8..7d0de98acea 100644 --- a/core/config/docs/chains-evm.toml +++ b/core/config/docs/chains-evm.toml @@ -264,7 +264,7 @@ TipCapDefault = '1 wei' # Default TipCapMin = '1 wei' # Default [EVM.GasEstimator.DAOracle] -# OracleType refers to the oracle family this config belongs to. Currently the available oracle types are: 'opstack', 'arbitrum', and 'zksync'. +# OracleType refers to the oracle family this config belongs to. Currently the available oracle types are: 'opstack', 'arbitrum', 'zksync', and 'custom_calldata'. OracleType = 'opstack' # Example # OracleAddress is the address of the oracle contract. OracleAddress = '0x420000000000000000000000000000000000000F' # Example diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 67818325b37..3c4ee3ee3d5 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -9321,7 +9321,7 @@ CustomGasPriceCalldata = '' # Default ```toml OracleType = 'opstack' # Example ``` -OracleType refers to the oracle family this config belongs to. Currently the available oracle types are: 'opstack', 'arbitrum', and 'zksync'. +OracleType refers to the oracle family this config belongs to. Currently the available oracle types are: 'opstack', 'arbitrum', 'zksync', and 'custom_calldata'. ### OracleAddress ```toml