-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
EvmFeeEstimator return Optimistic Rollup's L1BaseFee #10557
Changes from 14 commits
af3e893
33af306
e5f5aff
d6e8274
d5ab581
56efe1d
41f5bd4
055aeb1
20272a4
e5d1295
008a455
19282f2
7074410
06b61ba
42c5a00
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
package chainoracles | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"math/big" | ||
"sync" | ||
"time" | ||
|
||
"github.com/ethereum/go-ethereum" | ||
"github.com/ethereum/go-ethereum/common" | ||
"golang.org/x/exp/slices" | ||
|
||
"github.com/smartcontractkit/chainlink/v2/core/assets" | ||
evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" | ||
"github.com/smartcontractkit/chainlink/v2/core/config" | ||
"github.com/smartcontractkit/chainlink/v2/core/logger" | ||
"github.com/smartcontractkit/chainlink/v2/core/utils" | ||
) | ||
|
||
//go:generate mockery --quiet --name ethClient --output ./mocks/ --case=underscore --structname ETHClient | ||
type ethClient interface { | ||
CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) | ||
} | ||
|
||
// Reads L2-specific precompiles and caches the l1GasPrice set by the L2. | ||
type l1GasPriceOracle struct { | ||
client ethClient | ||
pollPeriod time.Duration | ||
logger logger.Logger | ||
address string | ||
callArgs string | ||
|
||
l1GasPriceMu sync.RWMutex | ||
l1GasPrice *assets.Wei | ||
|
||
chInitialised chan struct{} | ||
chStop utils.StopChan | ||
chDone chan struct{} | ||
utils.StartStopOnce | ||
} | ||
|
||
const ( | ||
// ArbGasInfoAddress is the address of the "Precompiled contract that exists in every Arbitrum chain." | ||
// https://github.com/OffchainLabs/nitro/blob/f7645453cfc77bf3e3644ea1ac031eff629df325/contracts/src/precompiles/ArbGasInfo.sol | ||
ArbGasInfoAddress = "0x000000000000000000000000000000000000006C" | ||
// ArbGasInfo_getL1BaseFeeEstimate is the a hex encoded call to: | ||
// `function getL1BaseFeeEstimate() external view returns (uint256);` | ||
ArbGasInfo_getL1BaseFeeEstimate = "f5d6ded7" | ||
|
||
// GasOracleAddress is the address of the precompiled contract that exists on OP stack chain. | ||
// This is the case for Optimism and Base. | ||
OPGasOracleAddress = "0x420000000000000000000000000000000000000F" | ||
// GasOracle_l1BaseFee is the a hex encoded call to: | ||
// `function l1BaseFee() external view returns (uint256);` | ||
OPGasOracle_l1BaseFee = "519b4bd3" | ||
|
||
// Interval at which to poll for L1BaseFee. A good starting point is the L1 block time. | ||
PollPeriod = 12 * time.Second | ||
) | ||
|
||
var supportedChainTypes = []config.ChainType{config.ChainArbitrum, config.ChainOptimismBedrock} | ||
|
||
func IsL1OracleChain(chainType config.ChainType) bool { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This name can confuse someone into thinking if this chain itself is an L1 chain. |
||
return slices.Contains(supportedChainTypes, chainType) | ||
} | ||
|
||
func NewL1GasPriceOracle(lggr logger.Logger, ethClient ethClient, chainType config.ChainType) L1Oracle { | ||
var address, callArgs string | ||
switch chainType { | ||
case config.ChainArbitrum: | ||
address = ArbGasInfoAddress | ||
callArgs = ArbGasInfo_getL1BaseFeeEstimate | ||
case config.ChainOptimismBedrock: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since Base also uses OptimismBedorck chaintype, can we confirm the address and the hex code are the same? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, Base uses the same precompiles as Optimism: https://basescan.org/address/0x420000000000000000000000000000000000000F#readProxyContract |
||
address = OPGasOracleAddress | ||
callArgs = OPGasOracle_l1BaseFee | ||
default: | ||
panic(fmt.Sprintf("Received unspported chaintype %s", chainType)) | ||
} | ||
|
||
return &l1GasPriceOracle{ | ||
client: ethClient, | ||
pollPeriod: PollPeriod, | ||
logger: lggr.Named(fmt.Sprintf("L1GasPriceOracle(%s)", chainType)), | ||
address: address, | ||
callArgs: callArgs, | ||
chInitialised: make(chan struct{}), | ||
chStop: make(chan struct{}), | ||
chDone: make(chan struct{}), | ||
} | ||
} | ||
|
||
func (o *l1GasPriceOracle) Name() string { | ||
return o.logger.Name() | ||
} | ||
|
||
func (o *l1GasPriceOracle) Start(ctx context.Context) error { | ||
return o.StartOnce(o.Name(), func() error { | ||
go o.run() | ||
<-o.chInitialised | ||
return nil | ||
}) | ||
} | ||
func (o *l1GasPriceOracle) Close() error { | ||
return o.StopOnce(o.Name(), func() error { | ||
close(o.chStop) | ||
<-o.chDone | ||
return nil | ||
}) | ||
} | ||
|
||
func (o *l1GasPriceOracle) Ready() error { return o.StartStopOnce.Ready() } | ||
|
||
func (o *l1GasPriceOracle) HealthReport() map[string]error { | ||
return map[string]error{o.Name(): o.StartStopOnce.Healthy()} | ||
} | ||
|
||
func (o *l1GasPriceOracle) run() { | ||
defer close(o.chDone) | ||
|
||
t := o.refresh() | ||
close(o.chInitialised) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of from here, close this from within refresh(), once we successfully are able to set l1GasPrice there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Start() call is blocked on the |
||
|
||
for { | ||
select { | ||
case <-o.chStop: | ||
return | ||
case <-t.C: | ||
t = o.refresh() | ||
} | ||
} | ||
} | ||
|
||
func (o *l1GasPriceOracle) refresh() (t *time.Timer) { | ||
t = time.NewTimer(utils.WithJitter(o.pollPeriod)) | ||
|
||
ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) | ||
defer cancel() | ||
|
||
precompile := common.HexToAddress(o.address) | ||
b, err := o.client.CallContract(ctx, ethereum.CallMsg{ | ||
To: &precompile, | ||
Data: common.Hex2Bytes(o.callArgs), | ||
}, nil) | ||
if err != nil { | ||
o.logger.Errorf("gas oracle contract call failed: %v", err) | ||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add an error log here too |
||
} | ||
|
||
if len(b) != 32 { // returns uint256; | ||
o.logger.Criticalf("return data length (%d) different than expected (%d)", len(b), 32) | ||
return | ||
} | ||
price := new(big.Int).SetBytes(b) | ||
|
||
o.l1GasPriceMu.Lock() | ||
defer o.l1GasPriceMu.Unlock() | ||
o.l1GasPrice = assets.NewWei(price) | ||
return | ||
} | ||
|
||
func (o *l1GasPriceOracle) L1GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err error) { | ||
ok := o.IfStarted(func() { | ||
o.l1GasPriceMu.RLock() | ||
l1GasPrice = o.l1GasPrice | ||
o.l1GasPriceMu.RUnlock() | ||
}) | ||
if !ok { | ||
return l1GasPrice, errors.New("L1GasPriceOracle is not started; cannot estimate gas") | ||
} | ||
if l1GasPrice == nil { | ||
return l1GasPrice, errors.New("failed to get l1 gas price; gas price not set") | ||
} | ||
return | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package chainoracles | ||
|
||
import ( | ||
"fmt" | ||
"math/big" | ||
"testing" | ||
|
||
"github.com/ethereum/go-ethereum" | ||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/mock" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/smartcontractkit/chainlink/v2/core/assets" | ||
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/chainoracles/mocks" | ||
"github.com/smartcontractkit/chainlink/v2/core/config" | ||
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils" | ||
"github.com/smartcontractkit/chainlink/v2/core/logger" | ||
) | ||
|
||
func TestL1GasPriceOracle(t *testing.T) { | ||
t.Parallel() | ||
|
||
t.Run("Unsupported ChainType returns nil", func(t *testing.T) { | ||
ethClient := mocks.NewETHClient(t) | ||
|
||
assert.Panicsf(t, func() { NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainCelo) }, "Received unspported chaintype %s", config.ChainCelo) | ||
}) | ||
|
||
t.Run("Calling L1GasPrice on unstarted L1Oracle returns error", func(t *testing.T) { | ||
ethClient := mocks.NewETHClient(t) | ||
|
||
oracle := NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainOptimismBedrock) | ||
|
||
_, err := oracle.L1GasPrice(testutils.Context(t)) | ||
assert.EqualError(t, err, "L1GasPriceOracle is not started; cannot estimate gas") | ||
}) | ||
|
||
t.Run("Calling L1GasPrice on started Arbitrum L1Oracle returns Arbitrum l1GasPrice", func(t *testing.T) { | ||
l1BaseFee := big.NewInt(100) | ||
|
||
ethClient := mocks.NewETHClient(t) | ||
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) | ||
assert.Equal(t, ArbGasInfoAddress, callMsg.To.String()) | ||
assert.Equal(t, ArbGasInfo_getL1BaseFeeEstimate, fmt.Sprintf("%x", callMsg.Data)) | ||
assert.Nil(t, blockNumber) | ||
}).Return(common.BigToHash(l1BaseFee).Bytes(), nil) | ||
|
||
oracle := NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainArbitrum) | ||
require.NoError(t, oracle.Start(testutils.Context(t))) | ||
t.Cleanup(func() { assert.NoError(t, oracle.Close()) }) | ||
|
||
gasPrice, err := oracle.L1GasPrice(testutils.Context(t)) | ||
require.NoError(t, err) | ||
|
||
assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) | ||
}) | ||
|
||
t.Run("Calling L1GasPrice on started OPStack L1Oracle returns OPStack l1GasPrice", func(t *testing.T) { | ||
l1BaseFee := big.NewInt(200) | ||
|
||
ethClient := mocks.NewETHClient(t) | ||
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) | ||
assert.Equal(t, OPGasOracleAddress, callMsg.To.String()) | ||
assert.Equal(t, OPGasOracle_l1BaseFee, fmt.Sprintf("%x", callMsg.Data)) | ||
assert.Nil(t, blockNumber) | ||
}).Return(common.BigToHash(l1BaseFee).Bytes(), nil) | ||
|
||
oracle := NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainOptimismBedrock) | ||
require.NoError(t, oracle.Start(testutils.Context(t))) | ||
t.Cleanup(func() { assert.NoError(t, oracle.Close()) }) | ||
|
||
gasPrice, err := oracle.L1GasPrice(testutils.Context(t)) | ||
require.NoError(t, err) | ||
|
||
assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) | ||
}) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Just wondering, if this name is appropriate.
This seems to be a package for Rollups.
Could rollups be a better name?