From 528e80a751838ad4ae5b6df8385a71a8572c072b 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 +- .../evm/gas/rollups/arbitrum_l1_oracle.go | 4 +- .../gas/rollups/custom_calldata_l1_oracle.go | 159 ++++++++++++++++++ .../rollups/custom_calldata_l1_oracle_test.go | 55 ++++++ core/chains/evm/gas/rollups/l1_oracle.go | 4 +- core/chains/evm/gas/rollups/op_l1_oracle.go | 4 +- .../evm/gas/rollups/zkSync_l1_oracle.go | 4 +- core/config/docs/chains-evm.toml | 2 +- docs/CONFIG.md | 2 +- 10 files changed, 233 insertions(+), 13 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/arbitrum_l1_oracle.go b/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go index e332d31125f..f87bdf2117d 100644 --- a/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go +++ b/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go @@ -28,7 +28,7 @@ type ArbL1GasOracle interface { // Reads L2-specific precompiles and caches the l1GasPrice set by the L2. type arbitrumL1Oracle struct { services.StateMachine - client l1OracleClient + client daOracleClient pollPeriod time.Duration logger logger.SugaredLogger @@ -65,7 +65,7 @@ const ( ArbGasInfo_getPricesInArbGas = "02199f34" ) -func NewArbitrumL1GasOracle(lggr logger.Logger, ethClient l1OracleClient) (*arbitrumL1Oracle, error) { +func NewArbitrumL1GasOracle(lggr logger.Logger, ethClient daOracleClient) (*arbitrumL1Oracle, error) { var l1GasPriceAddress, gasPriceMethod, l1GasCostAddress, gasCostMethod string var l1GasPriceMethodAbi, l1GasCostMethodAbi abi.ABI var gasPriceErr, gasCostErr error 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..c5930424dc3 --- /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 daOracleClient + 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 daOracleClient, 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/chains/evm/gas/rollups/l1_oracle.go b/core/chains/evm/gas/rollups/l1_oracle.go index 281c3f17a63..390bf8e4ff0 100644 --- a/core/chains/evm/gas/rollups/l1_oracle.go +++ b/core/chains/evm/gas/rollups/l1_oracle.go @@ -28,7 +28,7 @@ type L1Oracle interface { GasPrice(ctx context.Context) (*assets.Wei, error) } -type l1OracleClient interface { +type daOracleClient interface { CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error } @@ -49,7 +49,7 @@ func IsRollupWithL1Support(chainType chaintype.ChainType) bool { return slices.Contains(supportedChainTypes, chainType) } -func NewL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chaintype.ChainType, daOracle evmconfig.DAOracle) (L1Oracle, error) { +func NewL1GasOracle(lggr logger.Logger, ethClient daOracleClient, chainType chaintype.ChainType, daOracle evmconfig.DAOracle) (L1Oracle, error) { if !IsRollupWithL1Support(chainType) { return nil, nil } diff --git a/core/chains/evm/gas/rollups/op_l1_oracle.go b/core/chains/evm/gas/rollups/op_l1_oracle.go index 4f1e8c67cb7..2495458c86f 100644 --- a/core/chains/evm/gas/rollups/op_l1_oracle.go +++ b/core/chains/evm/gas/rollups/op_l1_oracle.go @@ -26,7 +26,7 @@ import ( // Reads L2-specific precompiles and caches the l1GasPrice set by the L2. type optimismL1Oracle struct { services.StateMachine - client l1OracleClient + client daOracleClient pollPeriod time.Duration logger logger.SugaredLogger @@ -87,7 +87,7 @@ const ( decimalsMethod = "decimals" ) -func NewOpStackL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chaintype.ChainType, daOracle evmconfig.DAOracle) (*optimismL1Oracle, error) { +func NewOpStackL1GasOracle(lggr logger.Logger, ethClient daOracleClient, chainType chaintype.ChainType, daOracle evmconfig.DAOracle) (*optimismL1Oracle, error) { getL1FeeMethodAbi, err := abi.JSON(strings.NewReader(GetL1FeeAbiString)) if err != nil { return nil, fmt.Errorf("failed to parse L1 gas cost method ABI for chain: %s", chainType) diff --git a/core/chains/evm/gas/rollups/zkSync_l1_oracle.go b/core/chains/evm/gas/rollups/zkSync_l1_oracle.go index 94d2e05ac02..30f87729311 100644 --- a/core/chains/evm/gas/rollups/zkSync_l1_oracle.go +++ b/core/chains/evm/gas/rollups/zkSync_l1_oracle.go @@ -22,7 +22,7 @@ import ( // Reads L2-specific precompiles and caches the l1GasPrice set by the L2. type zkSyncL1Oracle struct { services.StateMachine - client l1OracleClient + client daOracleClient pollPeriod time.Duration logger logger.SugaredLogger @@ -56,7 +56,7 @@ const ( ZksyncGasInfo_getGasPriceL2 = "0xfe173b97" ) -func NewZkSyncL1GasOracle(lggr logger.Logger, ethClient l1OracleClient) *zkSyncL1Oracle { +func NewZkSyncL1GasOracle(lggr logger.Logger, ethClient daOracleClient) *zkSyncL1Oracle { return &zkSyncL1Oracle{ client: ethClient, pollPeriod: PollPeriod, 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