Skip to content

Commit

Permalink
Changed Optimism L1 Oracle to support Mantle (smartcontractkit#14160)
Browse files Browse the repository at this point in the history
* edited op stack oracle to include tokenRatio for Mantle

* typo fix

* fixed syntax errors

* fixed compilation errors in test

* added unit tests for Mantle oracle changes

* typo fix

* added changeset file

* added hashtag to changeset

* changed unit test to include new chaintype

* changed test to include new chaintype

* typo fix

* addressed alphabetical order and error issues

* used batch call instead of separate calls for L1 Base Fee

* added test cases for RPC errors to Mantle
  • Loading branch information
ma33r authored Aug 22, 2024
1 parent 621e875 commit c98feb2
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-mugs-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": minor
---

Edited the Optimism Stack L1 Oracle to add support for Mantle #added
6 changes: 5 additions & 1 deletion core/chains/evm/config/chaintype/chaintype.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
ChainGnosis ChainType = "gnosis"
ChainHedera ChainType = "hedera"
ChainKroma ChainType = "kroma"
ChainMantle ChainType = "mantle"
ChainMetis ChainType = "metis"
ChainOptimismBedrock ChainType = "optimismBedrock"
ChainScroll ChainType = "scroll"
Expand All @@ -37,7 +38,7 @@ func (c ChainType) IsL2() bool {

func (c ChainType) IsValid() bool {
switch c {
case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMetis, ChainOptimismBedrock, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync:
case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMantle, ChainMetis, ChainOptimismBedrock, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync:
return true
}
return false
Expand All @@ -57,6 +58,8 @@ func ChainTypeFromSlug(slug string) ChainType {
return ChainHedera
case "kroma":
return ChainKroma
case "mantle":
return ChainMantle
case "metis":
return ChainMetis
case "optimismBedrock":
Expand Down Expand Up @@ -129,6 +132,7 @@ var ErrInvalidChainType = fmt.Errorf("must be one of %s or omitted", strings.Joi
string(ChainGnosis),
string(ChainHedera),
string(ChainKroma),
string(ChainMantle),
string(ChainMetis),
string(ChainOptimismBedrock),
string(ChainScroll),
Expand Down
4 changes: 2 additions & 2 deletions core/chains/evm/gas/rollups/l1_oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const (
PollPeriod = 6 * time.Second
)

var supportedChainTypes = []chaintype.ChainType{chaintype.ChainArbitrum, chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainZkSync}
var supportedChainTypes = []chaintype.ChainType{chaintype.ChainArbitrum, chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainZkSync, chaintype.ChainMantle}

func IsRollupWithL1Support(chainType chaintype.ChainType) bool {
return slices.Contains(supportedChainTypes, chainType)
Expand All @@ -56,7 +56,7 @@ func NewL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chai
var l1Oracle L1Oracle
var err error
switch chainType {
case chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll:
case chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainMantle:
l1Oracle, err = NewOpStackL1GasOracle(lggr, ethClient, chainType)
case chaintype.ChainArbitrum:
l1Oracle, err = NewArbitrumL1GasOracle(lggr, ethClient)
Expand Down
1 change: 1 addition & 0 deletions core/chains/evm/gas/rollups/l1_oracle_abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ const OPBaseFeeScalarAbiString = `[{"inputs":[],"name":"baseFeeScalar","outputs"
const OPBlobBaseFeeAbiString = `[{"inputs":[],"name":"blobBaseFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]`
const OPBlobBaseFeeScalarAbiString = `[{"inputs":[],"name":"blobBaseFeeScalar","outputs":[{"internalType":"uint32","name":"","type":"uint32"}],"stateMutability":"view","type":"function"}]`
const OPDecimalsAbiString = `[{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"}]`
const MantleTokenRatioAbiString = `[{"inputs":[],"name":"tokenRatio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]`
87 changes: 85 additions & 2 deletions core/chains/evm/gas/rollups/op_l1_oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type optimismL1Oracle struct {
blobBaseFeeCalldata []byte
blobBaseFeeScalarCalldata []byte
decimalsCalldata []byte
tokenRatioCalldata []byte
isEcotoneCalldata []byte
isEcotoneMethodAbi abi.ABI
isFjordCalldata []byte
Expand Down Expand Up @@ -87,7 +88,11 @@ const (
// decimals is a hex encoded call to:
// `function decimals() public pure returns (uint256);`
decimalsMethod = "decimals"
// OPGasOracleAddress is the address of the precompiled contract that exists on Optimism and Base.
// tokenRatio fetches the tokenRatio used for Mantle's gas price calculation
// tokenRatio is a hex encoded call to:
// `function tokenRatio() public pure returns (uint256);`
tokenRatioMethod = "tokenRatio"
// OPGasOracleAddress is the address of the precompiled contract that exists on Optimism, Base and Mantle.
OPGasOracleAddress = "0x420000000000000000000000000000000000000F"
// KromaGasOracleAddress is the address of the precompiled contract that exists on Kroma.
KromaGasOracleAddress = "0x4200000000000000000000000000000000000005"
Expand All @@ -98,7 +103,7 @@ const (
func NewOpStackL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chaintype.ChainType) (*optimismL1Oracle, error) {
var precompileAddress string
switch chainType {
case chaintype.ChainOptimismBedrock:
case chaintype.ChainOptimismBedrock, chaintype.ChainMantle:
precompileAddress = OPGasOracleAddress
case chaintype.ChainKroma:
precompileAddress = KromaGasOracleAddress
Expand Down Expand Up @@ -187,6 +192,16 @@ func newOpStackL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainTy
return nil, fmt.Errorf("failed to parse GasPriceOracle %s() calldata for chain: %s; %w", decimalsMethod, chainType, err)
}

// Encode calldata for tokenRatio method
tokenRatioMethodAbi, err := abi.JSON(strings.NewReader(MantleTokenRatioAbiString))
if err != nil {
return nil, fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for chain: %s; %w", tokenRatioMethod, chainType, err)
}
tokenRatioCalldata, err := tokenRatioMethodAbi.Pack(tokenRatioMethod)
if err != nil {
return nil, fmt.Errorf("failed to parse GasPriceOracle %s() calldata for chain: %s; %w", tokenRatioMethod, chainType, err)
}

return &optimismL1Oracle{
client: ethClient,
pollPeriod: PollPeriod,
Expand All @@ -208,6 +223,7 @@ func newOpStackL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainTy
blobBaseFeeCalldata: blobBaseFeeCalldata,
blobBaseFeeScalarCalldata: blobBaseFeeScalarCalldata,
decimalsCalldata: decimalsCalldata,
tokenRatioCalldata: tokenRatioCalldata,
isEcotoneCalldata: isEcotoneCalldata,
isEcotoneMethodAbi: isEcotoneMethodAbi,
isFjordCalldata: isFjordCalldata,
Expand Down Expand Up @@ -346,6 +362,10 @@ func (o *optimismL1Oracle) GetGasCost(ctx context.Context, tx *gethtypes.Transac
}

func (o *optimismL1Oracle) GetDAGasPrice(ctx context.Context) (*big.Int, error) {
if o.chainType == chaintype.ChainMantle {
return o.getMantleGasPrice(ctx)
}

err := o.checkForUpgrade(ctx)
if err != nil {
return nil, err
Expand Down Expand Up @@ -443,6 +463,69 @@ func (o *optimismL1Oracle) getV1GasPrice(ctx context.Context) (*big.Int, error)
return new(big.Int).SetBytes(b), nil
}

// Returns the gas price for Mantle. The formula is the same as Optimism Bedrock (getV1GasPrice), but the tokenRatio parameter is multiplied
func (o *optimismL1Oracle) getMantleGasPrice(ctx context.Context) (*big.Int, error) {
// call oracle to get l1BaseFee and tokenRatio
rpcBatchCalls := []rpc.BatchElem{
{
Method: "eth_call",
Args: []any{
map[string]interface{}{
"from": common.Address{},
"to": o.l1OracleAddress,
"data": hexutil.Bytes(o.l1BaseFeeCalldata),
},
"latest",
},
Result: new(string),
},
{
Method: "eth_call",
Args: []any{
map[string]interface{}{
"from": common.Address{},
"to": o.l1OracleAddress,
"data": hexutil.Bytes(o.tokenRatioCalldata),
},
"latest",
},
Result: new(string),
},
}

err := o.client.BatchCallContext(ctx, rpcBatchCalls)
if err != nil {
return nil, fmt.Errorf("fetch gas price parameters batch call failed: %w", err)
}
if rpcBatchCalls[0].Error != nil {
return nil, fmt.Errorf("%s call failed in a batch: %w", l1BaseFeeMethod, err)
}
if rpcBatchCalls[1].Error != nil {
return nil, fmt.Errorf("%s call failed in a batch: %w", tokenRatioMethod, err)
}

// Extract values from responses
l1BaseFeeResult := *(rpcBatchCalls[0].Result.(*string))
tokenRatioResult := *(rpcBatchCalls[1].Result.(*string))

// Decode the responses into bytes
l1BaseFeeBytes, err := hexutil.Decode(l1BaseFeeResult)
if err != nil {
return nil, fmt.Errorf("failed to decode %s rpc result: %w", l1BaseFeeMethod, err)
}
tokenRatioBytes, err := hexutil.Decode(tokenRatioResult)
if err != nil {
return nil, fmt.Errorf("failed to decode %s rpc result: %w", tokenRatioMethod, err)
}

// Convert bytes to big int for calculations
l1BaseFee := new(big.Int).SetBytes(l1BaseFeeBytes)
tokenRatio := new(big.Int).SetBytes(tokenRatioBytes)

// multiply l1BaseFee and tokenRatio and return
return new(big.Int).Mul(l1BaseFee, tokenRatio), nil
}

// Returns the scaled gas price using baseFeeScalar, l1BaseFee, blobBaseFeeScalar, and blobBaseFee fields from the oracle
// Confirmed the same calculation is used to determine gas price for both Ecotone and Fjord
func (o *optimismL1Oracle) getEcotoneFjordGasPrice(ctx context.Context) (*big.Int, error) {
Expand Down
88 changes: 88 additions & 0 deletions core/chains/evm/gas/rollups/op_l1_oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,94 @@ func TestOPL1Oracle_ReadV1GasPrice(t *testing.T) {
}
}

func TestOPL1Oracle_ReadMantleGasPrice(t *testing.T) {
l1BaseFee := big.NewInt(100)
tokenRatio := big.NewInt(40)
oracleAddress := common.HexToAddress("0x1234").String()

t.Parallel()
t.Run("correctly fetches gas price if chain is Mantle", func(t *testing.T) {
// Encode calldata for l1BaseFee method
l1BaseFeeMethodAbi, err := abi.JSON(strings.NewReader(L1BaseFeeAbiString))
require.NoError(t, err)
l1BaseFeeCalldata, err := l1BaseFeeMethodAbi.Pack(l1BaseFeeMethod)
require.NoError(t, err)

// Encode calldata for tokenRatio method
tokenRatioMethodAbi, err := abi.JSON(strings.NewReader(MantleTokenRatioAbiString))
require.NoError(t, err)
tokenRatioCalldata, err := tokenRatioMethodAbi.Pack(tokenRatioMethod)
require.NoError(t, err)

ethClient := mocks.NewL1OracleClient(t)
ethClient.On("BatchCallContext", mock.Anything, mock.IsType([]rpc.BatchElem{})).Run(func(args mock.Arguments) {
rpcElements := args.Get(1).([]rpc.BatchElem)
require.Equal(t, 2, len(rpcElements))
for _, rE := range rpcElements {
require.Equal(t, "eth_call", rE.Method)
require.Equal(t, oracleAddress, rE.Args[0].(map[string]interface{})["to"])
require.Equal(t, "latest", rE.Args[1])
}
require.Equal(t, hexutil.Bytes(l1BaseFeeCalldata), rpcElements[0].Args[0].(map[string]interface{})["data"])
require.Equal(t, hexutil.Bytes(tokenRatioCalldata), rpcElements[1].Args[0].(map[string]interface{})["data"])

res1 := common.BigToHash(l1BaseFee).Hex()
res2 := common.BigToHash(tokenRatio).Hex()

rpcElements[0].Result = &res1
rpcElements[1].Result = &res2
}).Return(nil).Once()

oracle, err := newOpStackL1GasOracle(logger.Test(t), ethClient, chaintype.ChainMantle, oracleAddress)
require.NoError(t, err)

gasPrice, err := oracle.GetDAGasPrice(tests.Context(t))
require.NoError(t, err)

assert.Equal(t, new(big.Int).Mul(l1BaseFee, tokenRatio), gasPrice)
})

t.Run("fetching Mantle price but rpc returns bad data", func(t *testing.T) {
ethClient := mocks.NewL1OracleClient(t)
ethClient.On("BatchCallContext", mock.Anything, mock.IsType([]rpc.BatchElem{})).Run(func(args mock.Arguments) {
rpcElements := args.Get(1).([]rpc.BatchElem)
var badData = "zzz"
rpcElements[0].Result = &badData
rpcElements[1].Result = &badData
}).Return(nil).Once()

oracle, err := newOpStackL1GasOracle(logger.Test(t), ethClient, chaintype.ChainMantle, oracleAddress)
require.NoError(t, err)
_, err = oracle.GetDAGasPrice(tests.Context(t))
assert.Error(t, err)
})

t.Run("fetching Mantle price but rpc parent call errors", func(t *testing.T) {
ethClient := mocks.NewL1OracleClient(t)
ethClient.On("BatchCallContext", mock.Anything, mock.IsType([]rpc.BatchElem{})).Return(fmt.Errorf("revert")).Once()

oracle, err := newOpStackL1GasOracle(logger.Test(t), ethClient, chaintype.ChainMantle, oracleAddress)
require.NoError(t, err)
_, err = oracle.GetDAGasPrice(tests.Context(t))
assert.Error(t, err)
})

t.Run("fetching Mantle price but one of the sub rpc call errors", func(t *testing.T) {
ethClient := mocks.NewL1OracleClient(t)
ethClient.On("BatchCallContext", mock.Anything, mock.IsType([]rpc.BatchElem{})).Run(func(args mock.Arguments) {
rpcElements := args.Get(1).([]rpc.BatchElem)
res := common.BigToHash(l1BaseFee).Hex()
rpcElements[0].Result = &res
rpcElements[1].Error = fmt.Errorf("revert")
}).Return(nil).Once()

oracle, err := newOpStackL1GasOracle(logger.Test(t), ethClient, chaintype.ChainMantle, oracleAddress)
require.NoError(t, err)
_, err = oracle.GetDAGasPrice(tests.Context(t))
assert.Error(t, err)
})
}

func setupUpgradeCheck(t *testing.T, oracleAddress string, isFjord, isEcotone bool) *mocks.L1OracleClient {
trueHex := "0x0000000000000000000000000000000000000000000000000000000000000001"
falseHex := "0x0000000000000000000000000000000000000000000000000000000000000000"
Expand Down
4 changes: 2 additions & 2 deletions core/services/chainlink/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1304,7 +1304,7 @@ func TestConfig_Validate(t *testing.T) {
- 1: 10 errors:
- ChainType: invalid value (Foo): must not be set with this chain id
- Nodes: missing: must have at least one node
- ChainType: invalid value (Foo): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted
- ChainType: invalid value (Foo): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted
- HeadTracker.HistoryDepth: invalid value (30): must be greater than or equal to FinalizedBlockOffset
- GasEstimator.BumpThreshold: invalid value (0): cannot be 0 if auto-purge feature is enabled for Foo
- Transactions.AutoPurge.Threshold: missing: needs to be set if auto-purge feature is enabled for Foo
Expand All @@ -1317,7 +1317,7 @@ func TestConfig_Validate(t *testing.T) {
- 2: 5 errors:
- ChainType: invalid value (Arbitrum): only "optimismBedrock" can be used with this chain id
- Nodes: missing: must have at least one node
- ChainType: invalid value (Arbitrum): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted
- ChainType: invalid value (Arbitrum): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted
- FinalityDepth: invalid value (0): must be greater than or equal to 1
- MinIncomingConfirmations: invalid value (0): must be greater than or equal to 1
- 3.Nodes: 5 errors:
Expand Down

0 comments on commit c98feb2

Please sign in to comment.