Skip to content

Commit

Permalink
Add new custom calldata DA oracle
Browse files Browse the repository at this point in the history
  • Loading branch information
ogtownsend committed Oct 11, 2024
1 parent 8b4686e commit 69ab245
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/metal-forks-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

Adds new custom calldata DA oracle #wip
7 changes: 4 additions & 3 deletions core/chains/evm/config/toml/daoracle/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
159 changes: 159 additions & 0 deletions core/chains/evm/gas/rollups/custom_calldata_l1_oracle.go
Original file line number Diff line number Diff line change
@@ -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
}
55 changes: 55 additions & 0 deletions core/chains/evm/gas/rollups/custom_calldata_l1_oracle_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
2 changes: 1 addition & 1 deletion core/config/docs/chains-evm.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 69ab245

Please sign in to comment.