diff --git a/.changeset/curly-onions-tell.md b/.changeset/curly-onions-tell.md new file mode 100644 index 0000000000..249f616c01 --- /dev/null +++ b/.changeset/curly-onions-tell.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#changed Make Mantle use default OP stack l1 gas oracle in core diff --git a/.changeset/great-timers-agree.md b/.changeset/great-timers-agree.md new file mode 100644 index 0000000000..bfa27761fb --- /dev/null +++ b/.changeset/great-timers-agree.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Updated gas limit estimation feature to set From address #internal diff --git a/.changeset/green-eagles-deliver.md b/.changeset/green-eagles-deliver.md new file mode 100644 index 0000000000..179b93b108 --- /dev/null +++ b/.changeset/green-eagles-deliver.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Handle zkEVM node level OOC error as TerminallyStuck #internal diff --git a/.changeset/loud-windows-call.md b/.changeset/loud-windows-call.md new file mode 100644 index 0000000000..6dc8d6fac7 --- /dev/null +++ b/.changeset/loud-windows-call.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Added gas limit estimation feature to EVM gas estimators. Introduced a new config `EVM.GasEstimator.EstimateLimit` to toggle this feature. #added diff --git a/.changeset/seven-kiwis-run.md b/.changeset/seven-kiwis-run.md new file mode 100644 index 0000000000..3b56117c46 --- /dev/null +++ b/.changeset/seven-kiwis-run.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Added custom client error messages for Mantle to capture InsufficientEth and Fatal errors. #added diff --git a/.changeset/shiny-hornets-pretend.md b/.changeset/shiny-hornets-pretend.md new file mode 100644 index 0000000000..ff9946f4e7 --- /dev/null +++ b/.changeset/shiny-hornets-pretend.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Introduce new gas estimator #internal diff --git a/.changeset/two-mugs-complain.md b/.changeset/two-mugs-complain.md new file mode 100644 index 0000000000..77cdcbfe9e --- /dev/null +++ b/.changeset/two-mugs-complain.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Edited the Optimism Stack L1 Oracle to add support for Mantle #added diff --git a/.mockery.yaml b/.mockery.yaml index 34b886d187..fa36213ff7 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -65,6 +65,9 @@ packages: feeEstimatorClient: config: mockname: FeeEstimatorClient + feeHistoryEstimatorClient: + config: + mockname: FeeHistoryEstimatorClient EvmEstimator: github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups: interfaces: diff --git a/common/fee/models.go b/common/fee/models.go index 0568a2f143..0cc479d356 100644 --- a/common/fee/models.go +++ b/common/fee/models.go @@ -14,6 +14,7 @@ var ( ErrBumpFeeExceedsLimit = errors.New("fee bump exceeds limit") ErrBump = errors.New("fee bump failed") ErrConnectivity = errors.New("transaction propagation issue: transactions are not being mined") + ErrFeeLimitTooLow = errors.New("provided fee limit too low") ) func IsBumpErr(err error) bool { @@ -63,7 +64,7 @@ func CalculateBumpedFee( // Returns highest bumped fee price of originalFeePrice bumped by fixed units or percentage. func MaxBumpedFee(originalFeePrice *big.Int, feeBumpPercent uint16, feeBumpUnits *big.Int) *big.Int { return bigmath.Max( - addPercentage(originalFeePrice, feeBumpPercent), + AddPercentage(originalFeePrice, feeBumpPercent), new(big.Int).Add(originalFeePrice, feeBumpUnits), ) } diff --git a/common/fee/utils.go b/common/fee/utils.go index 26323e11e2..3d4b001e83 100644 --- a/common/fee/utils.go +++ b/common/fee/utils.go @@ -18,7 +18,7 @@ func ApplyMultiplier(feeLimit uint64, multiplier float32) (uint64, error) { } // Returns the input value increased by the given percentage. -func addPercentage(value *big.Int, percentage uint16) *big.Int { +func AddPercentage(value *big.Int, percentage uint16) *big.Int { bumped := new(big.Int) bumped.Mul(value, big.NewInt(int64(100+percentage))) bumped.Div(bumped, big.NewInt(100)) diff --git a/common/txmgr/broadcaster.go b/common/txmgr/broadcaster.go index be3d3ca2f6..1606f58ce0 100644 --- a/common/txmgr/broadcaster.go +++ b/common/txmgr/broadcaster.go @@ -20,6 +20,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink/v2/common/client" + commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" txmgrtypes "github.com/smartcontractkit/chainlink/v2/common/txmgr/types" "github.com/smartcontractkit/chainlink/v2/common/types" @@ -434,7 +435,11 @@ func (eb *Broadcaster[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE]) hand } attempt, _, _, retryable, err := eb.NewTxAttempt(ctx, *etx, eb.lggr) - if err != nil { + // Mark transaction as fatal if provided gas limit is set too low + if errors.Is(err, commonfee.ErrFeeLimitTooLow) { + etx.Error = null.StringFrom(commonfee.ErrFeeLimitTooLow.Error()) + return eb.saveFatallyErroredTransaction(eb.lggr, etx), false + } else if err != nil { return fmt.Errorf("processUnstartedTxs failed on NewAttempt: %w", err), retryable } diff --git a/core/chains/evm/client/chain_client.go b/core/chains/evm/client/chain_client.go index c27d294ebf..310528424d 100644 --- a/core/chains/evm/client/chain_client.go +++ b/core/chains/evm/client/chain_client.go @@ -84,6 +84,7 @@ type Client interface { SuggestGasPrice(ctx context.Context) (*big.Int, error) SuggestGasTipCap(ctx context.Context) (*big.Int, error) LatestBlockHeight(ctx context.Context) (*big.Int, error) + FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) HeaderByNumber(ctx context.Context, n *big.Int) (*types.Header, error) HeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) @@ -353,6 +354,14 @@ func (c *chainClient) LatestFinalizedBlock(ctx context.Context) (*evmtypes.Head, return c.multiNode.LatestFinalizedBlock(ctx) } +func (c *chainClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { + rpc, err := c.multiNode.SelectNodeRPC() + if err != nil { + return feeHistory, err + } + return rpc.FeeHistory(ctx, blockCount, rewardPercentiles) +} + func (c *chainClient) CheckTxValidity(ctx context.Context, from common.Address, to common.Address, data []byte) *SendError { msg := ethereum.CallMsg{ From: from, diff --git a/core/chains/evm/client/errors.go b/core/chains/evm/client/errors.go index 5980b0dd96..ff4bd15a7b 100644 --- a/core/chains/evm/client/errors.go +++ b/core/chains/evm/client/errors.go @@ -253,7 +253,7 @@ var zkSync = ClientErrors{ } var zkEvm = ClientErrors{ - TerminallyStuck: regexp.MustCompile(`(?:: |^)not enough .* counters to continue the execution$`), + TerminallyStuck: regexp.MustCompile(`(?:: |^)(?:not enough .* counters to continue the execution|out of counters at node level (?:.*))$`), } var aStar = ClientErrors{ diff --git a/core/chains/evm/client/errors_test.go b/core/chains/evm/client/errors_test.go index 47e8ee610b..49543b16e9 100644 --- a/core/chains/evm/client/errors_test.go +++ b/core/chains/evm/client/errors_test.go @@ -215,6 +215,7 @@ func Test_Eth_Errors(t *testing.T) { {"insufficient funds for gas + value. balance: 42719769622667482000, fee: 48098250000000, value: 42719769622667482000", true, "celo"}, {"client error insufficient eth", true, "tomlConfig"}, {"transaction would cause overdraft", true, "Geth"}, + {"failed to forward tx to sequencer, please try again. Error message: 'insufficient funds for gas * price + value'", true, "Mantle"}, } for _, test := range tests { err = evmclient.NewSendErrorS(test.message) @@ -316,6 +317,8 @@ func Test_Eth_Errors(t *testing.T) { {"failed to add tx to the pool: not enough step counters to continue the execution", true, "Xlayer"}, {"failed to add tx to the pool: not enough keccak counters to continue the execution", true, "zkEVM"}, {"failed to add tx to the pool: not enough keccak counters to continue the execution", true, "Xlayer"}, + {"RPC error response: failed to add tx to the pool: out of counters at node level (Steps)", true, "zkEVM"}, + {"RPC error response: failed to add tx to the pool: out of counters at node level (GasUsed, KeccakHashes, PoseidonHashes, PoseidonPaddings, MemAligns, Arithmetics, Binaries, Steps, Sha256Hashes)", true, "Xlayer"}, } for _, test := range tests { @@ -400,6 +403,8 @@ func Test_Eth_Errors_Fatal(t *testing.T) { {"Failed to serialize transaction: max priority fee per gas higher than 2^64-1", true, "zkSync"}, {"Failed to serialize transaction: oversized data. max: 1000000; actual: 1000000", true, "zkSync"}, + {"failed to forward tx to sequencer, please try again. Error message: 'invalid sender'", true, "Mantle"}, + {"client error fatal", true, "tomlConfig"}, {"invalid chain id for signer", true, "Treasure"}, } diff --git a/core/chains/evm/client/mocks/client.go b/core/chains/evm/client/mocks/client.go index 7b5220033b..da034d9577 100644 --- a/core/chains/evm/client/mocks/client.go +++ b/core/chains/evm/client/mocks/client.go @@ -780,6 +780,66 @@ func (_c *Client_EstimateGas_Call) RunAndReturn(run func(context.Context, ethere return _c } +// FeeHistory provides a mock function with given fields: ctx, blockCount, rewardPercentiles +func (_m *Client) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { + ret := _m.Called(ctx, blockCount, rewardPercentiles) + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 *ethereum.FeeHistory + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)); ok { + return rf(ctx, blockCount, rewardPercentiles) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) *ethereum.FeeHistory); ok { + r0 = rf(ctx, blockCount, rewardPercentiles) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethereum.FeeHistory) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, []float64) error); ok { + r1 = rf(ctx, blockCount, rewardPercentiles) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type Client_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +// - ctx context.Context +// - blockCount uint64 +// - rewardPercentiles []float64 +func (_e *Client_Expecter) FeeHistory(ctx interface{}, blockCount interface{}, rewardPercentiles interface{}) *Client_FeeHistory_Call { + return &Client_FeeHistory_Call{Call: _e.mock.On("FeeHistory", ctx, blockCount, rewardPercentiles)} +} + +func (_c *Client_FeeHistory_Call) Run(run func(ctx context.Context, blockCount uint64, rewardPercentiles []float64)) *Client_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].([]float64)) + }) + return _c +} + +func (_c *Client_FeeHistory_Call) Return(feeHistory *ethereum.FeeHistory, err error) *Client_FeeHistory_Call { + _c.Call.Return(feeHistory, err) + return _c +} + +func (_c *Client_FeeHistory_Call) RunAndReturn(run func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)) *Client_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + // FilterLogs provides a mock function with given fields: ctx, q func (_m *Client) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { ret := _m.Called(ctx, q) diff --git a/core/chains/evm/client/mocks/rpc_client.go b/core/chains/evm/client/mocks/rpc_client.go index 06f79efd55..5567b3f897 100644 --- a/core/chains/evm/client/mocks/rpc_client.go +++ b/core/chains/evm/client/mocks/rpc_client.go @@ -889,6 +889,66 @@ func (_c *RPCClient_EstimateGas_Call) RunAndReturn(run func(context.Context, int return _c } +// FeeHistory provides a mock function with given fields: ctx, blockCount, rewardPercentiles +func (_m *RPCClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { + ret := _m.Called(ctx, blockCount, rewardPercentiles) + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 *ethereum.FeeHistory + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)); ok { + return rf(ctx, blockCount, rewardPercentiles) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) *ethereum.FeeHistory); ok { + r0 = rf(ctx, blockCount, rewardPercentiles) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethereum.FeeHistory) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, []float64) error); ok { + r1 = rf(ctx, blockCount, rewardPercentiles) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RPCClient_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type RPCClient_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +// - ctx context.Context +// - blockCount uint64 +// - rewardPercentiles []float64 +func (_e *RPCClient_Expecter) FeeHistory(ctx interface{}, blockCount interface{}, rewardPercentiles interface{}) *RPCClient_FeeHistory_Call { + return &RPCClient_FeeHistory_Call{Call: _e.mock.On("FeeHistory", ctx, blockCount, rewardPercentiles)} +} + +func (_c *RPCClient_FeeHistory_Call) Run(run func(ctx context.Context, blockCount uint64, rewardPercentiles []float64)) *RPCClient_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].([]float64)) + }) + return _c +} + +func (_c *RPCClient_FeeHistory_Call) Return(feeHistory *ethereum.FeeHistory, err error) *RPCClient_FeeHistory_Call { + _c.Call.Return(feeHistory, err) + return _c +} + +func (_c *RPCClient_FeeHistory_Call) RunAndReturn(run func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)) *RPCClient_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + // FilterEvents provides a mock function with given fields: ctx, query func (_m *RPCClient) FilterEvents(ctx context.Context, query ethereum.FilterQuery) ([]coretypes.Log, error) { ret := _m.Called(ctx, query) diff --git a/core/chains/evm/client/null_client.go b/core/chains/evm/client/null_client.go index 3129bcff9b..5b1a4d7e1b 100644 --- a/core/chains/evm/client/null_client.go +++ b/core/chains/evm/client/null_client.go @@ -235,3 +235,7 @@ func (nc *NullClient) LatestFinalizedBlock(_ context.Context) (*evmtypes.Head, e func (nc *NullClient) CheckTxValidity(_ context.Context, _ common.Address, _ common.Address, _ []byte) *SendError { return nil } + +func (nc *NullClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { + return nil, nil +} diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index 72071199d4..295e24f7c9 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -103,6 +103,7 @@ type RPCClient interface { SuggestGasTipCap(ctx context.Context) (t *big.Int, err error) TransactionReceiptGeth(ctx context.Context, txHash common.Hash) (r *types.Receipt, err error) GetInterceptedChainInfo() (latest, highestUserObservations commonclient.ChainInfo) + FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) } const rpcSubscriptionMethodNewHeads = "newHeads" @@ -599,6 +600,7 @@ func (r *rpcClient) TransactionReceiptGeth(ctx context.Context, txHash common.Ha return } + func (r *rpcClient) TransactionByHash(ctx context.Context, txHash common.Hash) (tx *types.Transaction, err error) { ctx, cancel, ws, http := r.makeLiveQueryCtxAndSafeGetClients(ctx, r.rpcTimeout) defer cancel() @@ -1119,6 +1121,29 @@ func (r *rpcClient) BalanceAt(ctx context.Context, account common.Address, block return } +func (r *rpcClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { + ctx, cancel, ws, http := r.makeLiveQueryCtxAndSafeGetClients(ctx, r.rpcTimeout) + defer cancel() + lggr := r.newRqLggr().With("blockCount", blockCount, "rewardPercentiles", rewardPercentiles) + + lggr.Debug("RPC call: evmclient.Client#FeeHistory") + start := time.Now() + if http != nil { + feeHistory, err = http.geth.FeeHistory(ctx, blockCount, nil, rewardPercentiles) + err = r.wrapHTTP(err) + } else { + feeHistory, err = ws.geth.FeeHistory(ctx, blockCount, nil, rewardPercentiles) + err = r.wrapWS(err) + } + duration := time.Since(start) + + r.logResult(lggr, err, duration, r.getRPCDomain(), "FeeHistory", + "feeHistory", feeHistory, + ) + + return +} + // CallArgs represents the data used to call the balance method of a contract. // "To" is the address of the ERC contract. "Data" is the message sent // to the contract. "From" is the sender address. diff --git a/core/chains/evm/client/simulated_backend_client.go b/core/chains/evm/client/simulated_backend_client.go index 7dfd39f444..3ec1bff577 100644 --- a/core/chains/evm/client/simulated_backend_client.go +++ b/core/chains/evm/client/simulated_backend_client.go @@ -156,6 +156,10 @@ func (c *SimulatedBackendClient) LINKBalance(ctx context.Context, address common panic("not implemented") } +func (c *SimulatedBackendClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { + panic("not implemented") +} + // TransactionReceipt returns the transaction receipt for the given transaction hash. func (c *SimulatedBackendClient) TransactionReceipt(ctx context.Context, receipt common.Hash) (*types.Receipt, error) { return c.b.TransactionReceipt(ctx, receipt) diff --git a/core/chains/evm/config/chain_scoped_gas_estimator.go b/core/chains/evm/config/chain_scoped_gas_estimator.go index 689d5e38b8..54c7c36063 100644 --- a/core/chains/evm/config/chain_scoped_gas_estimator.go +++ b/core/chains/evm/config/chain_scoped_gas_estimator.go @@ -1,6 +1,8 @@ package config import ( + "time" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" @@ -36,6 +38,10 @@ func (g *gasEstimatorConfig) BlockHistory() BlockHistory { return &blockHistoryConfig{c: g.c.BlockHistory, blockDelay: g.blockDelay, bumpThreshold: g.c.BumpThreshold} } +func (g *gasEstimatorConfig) FeeHistory() FeeHistory { + return &feeHistoryConfig{c: g.c.FeeHistory} +} + func (g *gasEstimatorConfig) EIP1559DynamicFees() bool { return *g.c.EIP1559DynamicFees } @@ -108,6 +114,10 @@ func (g *gasEstimatorConfig) LimitJobType() LimitJobType { return &limitJobTypeConfig{c: g.c.LimitJobType} } +func (g *gasEstimatorConfig) EstimateLimit() bool { + return *g.c.EstimateLimit +} + type limitJobTypeConfig struct { c toml.GasLimitJobType } @@ -172,3 +182,11 @@ func (b *blockHistoryConfig) TransactionPercentile() uint16 { func (b *blockHistoryConfig) BlockDelay() uint16 { return *b.blockDelay } + +type feeHistoryConfig struct { + c toml.FeeHistoryEstimator +} + +func (u *feeHistoryConfig) CacheTimeout() time.Duration { + return u.c.CacheTimeout.Duration() +} diff --git a/core/chains/evm/config/chaintype/chaintype.go b/core/chains/evm/config/chaintype/chaintype.go index 07ea620624..35dd214b1f 100644 --- a/core/chains/evm/config/chaintype/chaintype.go +++ b/core/chains/evm/config/chaintype/chaintype.go @@ -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" @@ -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 @@ -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": @@ -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), diff --git a/core/chains/evm/config/config.go b/core/chains/evm/config/config.go index 3ccdfeea8b..3d00fe86a4 100644 --- a/core/chains/evm/config/config.go +++ b/core/chains/evm/config/config.go @@ -117,6 +117,7 @@ type AutoPurgeConfig interface { type GasEstimator interface { BlockHistory() BlockHistory + FeeHistory() FeeHistory LimitJobType() LimitJobType EIP1559DynamicFees() bool @@ -136,6 +137,7 @@ type GasEstimator interface { PriceMin() *assets.Wei Mode() string PriceMaxKey(gethcommon.Address) *assets.Wei + EstimateLimit() bool } type LimitJobType interface { @@ -157,6 +159,10 @@ type BlockHistory interface { TransactionPercentile() uint16 } +type FeeHistory interface { + CacheTimeout() time.Duration +} + type Workflow interface { FromAddress() *types.EIP55Address ForwarderAddress() *types.EIP55Address diff --git a/core/chains/evm/config/config_test.go b/core/chains/evm/config/config_test.go index 617a1605d8..e0dec00e68 100644 --- a/core/chains/evm/config/config_test.go +++ b/core/chains/evm/config/config_test.go @@ -220,6 +220,13 @@ func TestChainScopedConfig_BlockHistory(t *testing.T) { assert.Equal(t, uint16(1), bh.BlockDelay()) assert.Equal(t, uint16(4), bh.EIP1559FeeCapBufferBlocks()) } +func TestChainScopedConfig_FeeHistory(t *testing.T) { + t.Parallel() + cfg := testutils.NewTestChainScopedConfig(t, nil) + + u := cfg.EVM().GasEstimator().FeeHistory() + assert.Equal(t, 10*time.Second, u.CacheTimeout()) +} func TestChainScopedConfig_GasEstimator(t *testing.T) { t.Parallel() @@ -243,6 +250,7 @@ func TestChainScopedConfig_GasEstimator(t *testing.T) { assert.Equal(t, assets.GWei(100), ge.FeeCapDefault()) assert.Equal(t, assets.NewWeiI(1), ge.TipCapDefault()) assert.Equal(t, assets.NewWeiI(1), ge.TipCapMin()) + assert.Equal(t, false, ge.EstimateLimit()) } func TestChainScopedConfig_BSCDefaults(t *testing.T) { diff --git a/core/chains/evm/config/mocks/gas_estimator.go b/core/chains/evm/config/mocks/gas_estimator.go index b8e813e806..70b9c18d0b 100644 --- a/core/chains/evm/config/mocks/gas_estimator.go +++ b/core/chains/evm/config/mocks/gas_estimator.go @@ -298,6 +298,51 @@ func (_c *GasEstimator_EIP1559DynamicFees_Call) RunAndReturn(run func() bool) *G return _c } +// EstimateLimit provides a mock function with given fields: +func (_m *GasEstimator) EstimateLimit() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for EstimateLimit") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// GasEstimator_EstimateLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EstimateLimit' +type GasEstimator_EstimateLimit_Call struct { + *mock.Call +} + +// EstimateLimit is a helper method to define mock.On call +func (_e *GasEstimator_Expecter) EstimateLimit() *GasEstimator_EstimateLimit_Call { + return &GasEstimator_EstimateLimit_Call{Call: _e.mock.On("EstimateLimit")} +} + +func (_c *GasEstimator_EstimateLimit_Call) Run(run func()) *GasEstimator_EstimateLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *GasEstimator_EstimateLimit_Call) Return(_a0 bool) *GasEstimator_EstimateLimit_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *GasEstimator_EstimateLimit_Call) RunAndReturn(run func() bool) *GasEstimator_EstimateLimit_Call { + _c.Call.Return(run) + return _c +} + // FeeCapDefault provides a mock function with given fields: func (_m *GasEstimator) FeeCapDefault() *assets.Wei { ret := _m.Called() @@ -345,6 +390,53 @@ func (_c *GasEstimator_FeeCapDefault_Call) RunAndReturn(run func() *assets.Wei) return _c } +// FeeHistory provides a mock function with given fields: +func (_m *GasEstimator) FeeHistory() config.FeeHistory { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 config.FeeHistory + if rf, ok := ret.Get(0).(func() config.FeeHistory); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(config.FeeHistory) + } + } + + return r0 +} + +// GasEstimator_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type GasEstimator_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +func (_e *GasEstimator_Expecter) FeeHistory() *GasEstimator_FeeHistory_Call { + return &GasEstimator_FeeHistory_Call{Call: _e.mock.On("FeeHistory")} +} + +func (_c *GasEstimator_FeeHistory_Call) Run(run func()) *GasEstimator_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *GasEstimator_FeeHistory_Call) Return(_a0 config.FeeHistory) *GasEstimator_FeeHistory_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *GasEstimator_FeeHistory_Call) RunAndReturn(run func() config.FeeHistory) *GasEstimator_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + // LimitDefault provides a mock function with given fields: func (_m *GasEstimator) LimitDefault() uint64 { ret := _m.Called() diff --git a/core/chains/evm/config/toml/config.go b/core/chains/evm/config/toml/config.go index ac7841ac49..90854d90cf 100644 --- a/core/chains/evm/config/toml/config.go +++ b/core/chains/evm/config/toml/config.go @@ -554,6 +554,7 @@ type GasEstimator struct { LimitMultiplier *decimal.Decimal LimitTransfer *uint64 LimitJobType GasLimitJobType `toml:",omitempty"` + EstimateLimit *bool BumpMin *assets.Wei BumpPercent *uint16 @@ -567,6 +568,7 @@ type GasEstimator struct { TipCapMin *assets.Wei BlockHistory BlockHistoryEstimator `toml:",omitempty"` + FeeHistory FeeHistoryEstimator `toml:",omitempty"` } func (e *GasEstimator) ValidateConfig() (err error) { @@ -641,6 +643,9 @@ func (e *GasEstimator) setFrom(f *GasEstimator) { if v := f.LimitTransfer; v != nil { e.LimitTransfer = v } + if v := f.EstimateLimit; v != nil { + e.EstimateLimit = v + } if v := f.PriceDefault; v != nil { e.PriceDefault = v } @@ -658,6 +663,7 @@ func (e *GasEstimator) setFrom(f *GasEstimator) { } e.LimitJobType.setFrom(&f.LimitJobType) e.BlockHistory.setFrom(&f.BlockHistory) + e.FeeHistory.setFrom(&f.FeeHistory) } type GasLimitJobType struct { @@ -720,6 +726,16 @@ func (e *BlockHistoryEstimator) setFrom(f *BlockHistoryEstimator) { } } +type FeeHistoryEstimator struct { + CacheTimeout *commonconfig.Duration +} + +func (u *FeeHistoryEstimator) setFrom(f *FeeHistoryEstimator) { + if v := f.CacheTimeout; v != nil { + u.CacheTimeout = v + } +} + type KeySpecificConfig []KeySpecific func (ks KeySpecificConfig) ValidateConfig() (err error) { diff --git a/core/chains/evm/config/toml/defaults/fallback.toml b/core/chains/evm/config/toml/defaults/fallback.toml index e3136323f6..fb8eed3949 100644 --- a/core/chains/evm/config/toml/defaults/fallback.toml +++ b/core/chains/evm/config/toml/defaults/fallback.toml @@ -48,6 +48,7 @@ EIP1559DynamicFees = false FeeCapDefault = '100 gwei' TipCapDefault = '1' TipCapMin = '1' +EstimateLimit = false [GasEstimator.BlockHistory] BatchSize = 25 @@ -56,6 +57,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 diff --git a/core/chains/evm/gas/fee_history_estimator.go b/core/chains/evm/gas/fee_history_estimator.go new file mode 100644 index 0000000000..ba3192be10 --- /dev/null +++ b/core/chains/evm/gas/fee_history_estimator.go @@ -0,0 +1,440 @@ +package gas + +import ( + "context" + "fmt" + "math/big" + "strconv" + "sync" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + bigmath "github.com/smartcontractkit/chainlink-common/pkg/utils/big_math" + + commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" + feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" +) + +// metrics are thread safe +var ( + promFeeHistoryEstimatorGasPrice = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "gas_price_updater", + Help: "Sets latest gas price (in Wei)", + }, + []string{"evmChainID"}, + ) + promFeeHistoryEstimatorBaseFee = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "base_fee_updater", + Help: "Sets latest BaseFee (in Wei)", + }, + []string{"evmChainID"}, + ) + promFeeHistoryEstimatorMaxPriorityFeePerGas = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "max_priority_fee_per_gas_updater", + Help: "Sets latest MaxPriorityFeePerGas (in Wei)", + }, + []string{"evmChainID"}, + ) + promFeeHistoryEstimatorMaxFeePerGas = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "max_fee_per_gas_updater", + Help: "Sets latest MaxFeePerGas (in Wei)", + }, + []string{"evmChainID"}, + ) +) + +const ( + MinimumBumpPercentage = 10 // based on geth's spec + ConnectivityPercentile = 85 + BaseFeeBufferPercentage = 40 +) + +type FeeHistoryEstimatorConfig struct { + BumpPercent uint16 + CacheTimeout time.Duration + + EIP1559 bool + BlockHistorySize uint64 + RewardPercentile float64 +} + +type feeHistoryEstimatorClient interface { + SuggestGasPrice(ctx context.Context) (*big.Int, error) + FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) +} + +type FeeHistoryEstimator struct { + services.StateMachine + + client feeHistoryEstimatorClient + logger logger.Logger + config FeeHistoryEstimatorConfig + chainID *big.Int + + gasPriceMu sync.RWMutex + gasPrice *assets.Wei + + dynamicPriceMu sync.RWMutex + dynamicPrice DynamicFee + + priorityFeeThresholdMu sync.RWMutex + priorityFeeThreshold *assets.Wei + + l1Oracle rollups.L1Oracle + + wg *sync.WaitGroup + stopCh services.StopChan + refreshCh chan struct{} +} + +func NewFeeHistoryEstimator(lggr logger.Logger, client feeHistoryEstimatorClient, cfg FeeHistoryEstimatorConfig, chainID *big.Int, l1Oracle rollups.L1Oracle) *FeeHistoryEstimator { + return &FeeHistoryEstimator{ + client: client, + logger: logger.Named(lggr, "FeeHistoryEstimator"), + config: cfg, + chainID: chainID, + l1Oracle: l1Oracle, + wg: new(sync.WaitGroup), + stopCh: make(chan struct{}), + refreshCh: make(chan struct{}), + } +} + +func (f *FeeHistoryEstimator) Start(context.Context) error { + return f.StartOnce("FeeHistoryEstimator", func() error { + if f.config.BumpPercent < MinimumBumpPercentage { + return fmt.Errorf("BumpPercent: %s is less than minimum allowed percentage: %s", + strconv.FormatUint(uint64(f.config.BumpPercent), 10), strconv.Itoa(MinimumBumpPercentage)) + } + if f.config.EIP1559 && f.config.RewardPercentile > ConnectivityPercentile { + return fmt.Errorf("RewardPercentile: %s is greater than maximum allowed percentile: %s", + strconv.FormatUint(uint64(f.config.RewardPercentile), 10), strconv.Itoa(ConnectivityPercentile)) + } + f.wg.Add(1) + go f.run() + + return nil + }) +} + +func (f *FeeHistoryEstimator) Close() error { + return f.StopOnce("FeeHistoryEstimator", func() error { + close(f.stopCh) + f.wg.Wait() + return nil + }) +} + +func (f *FeeHistoryEstimator) run() { + defer f.wg.Done() + + t := services.TickerConfig{ + JitterPct: services.DefaultJitter, + }.NewTicker(f.config.CacheTimeout) + + for { + select { + case <-f.stopCh: + return + case <-f.refreshCh: + t.Reset() + case <-t.C: + if f.config.EIP1559 { + if err := f.RefreshDynamicPrice(); err != nil { + f.logger.Error(err) + } + } else { + if _, err := f.RefreshGasPrice(); err != nil { + f.logger.Error(err) + } + } + } + } +} + +// GetLegacyGas will fetch the cached gas price value. +func (f *FeeHistoryEstimator) GetLegacyGas(ctx context.Context, _ []byte, gasLimit uint64, maxPrice *assets.Wei, opts ...feetypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint64, err error) { + chainSpecificGasLimit = gasLimit + if gasPrice, err = f.getGasPrice(); err != nil { + return + } + + if gasPrice.Cmp(maxPrice) > 0 { + f.logger.Warnf("estimated gas price: %s is greater than the maximum gas price configured: %s, returning the maximum price instead.", gasPrice, maxPrice) + return maxPrice, chainSpecificGasLimit, nil + } + return +} + +// RefreshGasPrice will use eth_gasPrice to fetch and cache the latest gas price from the RPC. +func (f *FeeHistoryEstimator) RefreshGasPrice() (*assets.Wei, error) { + ctx, cancel := f.stopCh.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer cancel() + + gasPrice, err := f.client.SuggestGasPrice(ctx) + if err != nil { + return nil, err + } + + promFeeHistoryEstimatorGasPrice.WithLabelValues(f.chainID.String()).Set(float64(gasPrice.Int64())) + + gasPriceWei := assets.NewWei(gasPrice) + + f.logger.Debugf("fetched new gas price: %v", gasPriceWei) + + f.gasPriceMu.Lock() + defer f.gasPriceMu.Unlock() + f.gasPrice = gasPriceWei + return f.gasPrice, nil +} + +func (f *FeeHistoryEstimator) getGasPrice() (*assets.Wei, error) { + f.gasPriceMu.RLock() + defer f.gasPriceMu.RUnlock() + if f.gasPrice == nil { + return f.gasPrice, fmt.Errorf("gas price not set") + } + return f.gasPrice, nil +} + +// GetDynamicFee will fetch the cached dynamic prices. +func (f *FeeHistoryEstimator) GetDynamicFee(ctx context.Context, maxPrice *assets.Wei) (fee DynamicFee, err error) { + if fee, err = f.getDynamicPrice(); err != nil { + return + } + + if fee.FeeCap.Cmp(maxPrice) > 0 { + f.logger.Warnf("estimated maxFeePerGas: %v is greater than the maximum price configured: %v, returning the maximum price instead.", + fee.FeeCap, maxPrice) + fee.FeeCap = maxPrice + if fee.TipCap.Cmp(maxPrice) > 0 { + f.logger.Warnf("estimated maxPriorityFeePerGas: %v is greater than the maximum price configured: %v, returning the maximum price instead.", + fee.TipCap, maxPrice) + fee.TipCap = maxPrice + } + } + + return +} + +// RefreshDynamicPrice uses eth_feeHistory to fetch the baseFee of the next block and the Nth maxPriorityFeePerGas percentiles +// of the past X blocks. It also fetches the highest 85th maxPriorityFeePerGas percentile of the past X blocks, which represents +// the highest percentile we're willing to pay. A buffer is added on top of the latest baseFee to catch fluctuations in the next +// blocks. On Ethereum the increase is baseFee * 1.125 per block, however in some chains that may vary. +func (f *FeeHistoryEstimator) RefreshDynamicPrice() error { + ctx, cancel := f.stopCh.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer cancel() + + // RewardPercentile will be used for maxPriorityFeePerGas estimations and connectivityPercentile to set the highest threshold for bumping. + feeHistory, err := f.client.FeeHistory(ctx, max(f.config.BlockHistorySize, 1), []float64{f.config.RewardPercentile, ConnectivityPercentile}) + if err != nil { + return err + } + + // eth_feeHistory doesn't return the latest baseFee of the range but rather the latest + 1, because it can be derived from the existing + // values. Source: https://github.com/ethereum/go-ethereum/blob/b0f66e34ca2a4ea7ae23475224451c8c9a569826/eth/gasprice/feehistory.go#L235 + // nextBlock is the latest returned + 1 to be aligned with the base fee value. + nextBaseFee := assets.NewWei(feeHistory.BaseFee[len(feeHistory.BaseFee)-1]) + nextBlock := big.NewInt(0).Add(feeHistory.OldestBlock, big.NewInt(int64(f.config.BlockHistorySize))) + + // If BlockHistorySize is 0 it means priority fees will be ignored from the calculations, so we set them to 0. + // If it's not we exclude 0 priced priority fees from the RPC response, even though some networks allow them. For empty blocks, eth_feeHistory + // returns priority fees with 0 values so it's safer to discard them in order to pick values from a more representative sample. + maxPriorityFeePerGas := assets.NewWeiI(0) + priorityFeeThresholdWei := assets.NewWeiI(0) + if f.config.BlockHistorySize > 0 { + var nonZeroRewardsLen int64 = 0 + priorityFee := big.NewInt(0) + priorityFeeThreshold := big.NewInt(0) + for _, reward := range feeHistory.Reward { + // reward needs to have values for two percentiles + if len(reward) < 2 { + return fmt.Errorf("reward size incorrect: %d", len(reward)) + } + // We'll calculate the average of non-zero priority fees + if reward[0].Cmp(big.NewInt(0)) > 0 { + priorityFee = priorityFee.Add(priorityFee, reward[0]) + nonZeroRewardsLen += 1 + } + // We take the max value for the bumping threshold + if reward[1].Cmp(big.NewInt(0)) > 0 { + priorityFeeThreshold = bigmath.Max(priorityFeeThreshold, reward[1]) + } + } + + if nonZeroRewardsLen == 0 || priorityFeeThreshold.Cmp(big.NewInt(0)) == 0 { + return nil + } + priorityFeeThresholdWei = assets.NewWei(priorityFeeThreshold) + maxPriorityFeePerGas = assets.NewWei(priorityFee.Div(priorityFee, big.NewInt(nonZeroRewardsLen))) + } + // baseFeeBufferPercentage is added on top as a safety to catch fluctuations in the next blocks. + maxFeePerGas := nextBaseFee.AddPercentage(BaseFeeBufferPercentage).Add(maxPriorityFeePerGas) + + promFeeHistoryEstimatorBaseFee.WithLabelValues(f.chainID.String()).Set(float64(nextBaseFee.Int64())) + promFeeHistoryEstimatorMaxPriorityFeePerGas.WithLabelValues(f.chainID.String()).Set(float64(maxPriorityFeePerGas.Int64())) + promFeeHistoryEstimatorMaxFeePerGas.WithLabelValues(f.chainID.String()).Set(float64(maxFeePerGas.Int64())) + + f.logger.Debugf("Fetched new dynamic prices, nextBlock#: %v - oldestBlock#: %v - nextBaseFee: %v - maxFeePerGas: %v - maxPriorityFeePerGas: %v - maxPriorityFeeThreshold: %v", + nextBlock, feeHistory.OldestBlock, nextBaseFee, maxFeePerGas, maxPriorityFeePerGas, priorityFeeThresholdWei) + + f.priorityFeeThresholdMu.Lock() + f.priorityFeeThreshold = priorityFeeThresholdWei + f.priorityFeeThresholdMu.Unlock() + + f.dynamicPriceMu.Lock() + defer f.dynamicPriceMu.Unlock() + f.dynamicPrice.FeeCap = maxFeePerGas + f.dynamicPrice.TipCap = maxPriorityFeePerGas + return nil +} + +func (f *FeeHistoryEstimator) getDynamicPrice() (fee DynamicFee, err error) { + f.dynamicPriceMu.RLock() + defer f.dynamicPriceMu.RUnlock() + if f.dynamicPrice.FeeCap == nil || f.dynamicPrice.TipCap == nil { + return fee, fmt.Errorf("dynamic price not set") + } + return f.dynamicPrice, nil +} + +// BumpLegacyGas provides a bumped gas price value by bumping the previous one by BumpPercent. +// If the original value is higher than the max price it returns an error as there is no room for bumping. +// It aggregates the market, bumped, and max gas price to provide a correct value. +func (f *FeeHistoryEstimator) BumpLegacyGas(ctx context.Context, originalGasPrice *assets.Wei, gasLimit uint64, maxPrice *assets.Wei, _ []EvmPriorAttempt) (*assets.Wei, uint64, error) { + // Sanitize original fee input + if originalGasPrice == nil || originalGasPrice.Cmp(maxPrice) >= 0 { + return nil, 0, fmt.Errorf("%w: error while retrieving original gas price: originalGasPrice: %s. Maximum price configured: %s", + commonfee.ErrBump, originalGasPrice, maxPrice) + } + + currentGasPrice, err := f.RefreshGasPrice() + if err != nil { + return nil, 0, err + } + f.IfStarted(func() { f.refreshCh <- struct{}{} }) + + bumpedGasPrice := originalGasPrice.AddPercentage(f.config.BumpPercent) + bumpedGasPrice, err = LimitBumpedFee(originalGasPrice, currentGasPrice, bumpedGasPrice, maxPrice) + if err != nil { + return nil, 0, fmt.Errorf("failed to limit gas price: %w", err) + } + + f.logger.Debugw("bumped gas price", "originalGasPrice", originalGasPrice, "marketGasPrice", currentGasPrice, "bumpedGasPrice", bumpedGasPrice) + + return bumpedGasPrice, gasLimit, nil +} + +// BumpDynamicFee provides a bumped dynamic fee by bumping the previous one by BumpPercent. +// If the original values are higher than the max price it returns an error as there is no room for bumping. If maxPriorityFeePerGas is bumped +// above the priority fee threshold then there is a good chance there is a connectivity issue and we shouldn't bump. +// Both maxFeePerGas as well as maxPriorityFeePerGas need to be bumped otherwise the RPC won't accept the transaction and throw an error. +// See: https://github.com/ethereum/go-ethereum/issues/24284 +// It aggregates the market, bumped, and max price to provide a correct value, for both maxFeePerGas as well as maxPriorityFerPergas. +func (f *FeeHistoryEstimator) BumpDynamicFee(ctx context.Context, originalFee DynamicFee, maxPrice *assets.Wei, _ []EvmPriorAttempt) (bumped DynamicFee, err error) { + // For chains that don't have a mempool there is no concept of gas bumping so we force-call RefreshDynamicPrice to update the underlying base fee value + if f.config.BlockHistorySize == 0 { + if !f.IfStarted(func() { + if refreshErr := f.RefreshDynamicPrice(); refreshErr != nil { + err = refreshErr + return + } + f.refreshCh <- struct{}{} + bumped, err = f.GetDynamicFee(ctx, maxPrice) + }) { + return bumped, fmt.Errorf("estimator not started") + } + return bumped, err + } + + // Sanitize original fee input + // According to geth's spec we need to bump both maxFeePerGas and maxPriorityFeePerGas for the new attempt to be accepted by the RPC + if originalFee.FeeCap == nil || + originalFee.TipCap == nil || + ((originalFee.TipCap.Cmp(originalFee.FeeCap)) > 0) || + (originalFee.FeeCap.Cmp(maxPrice) >= 0) { + return bumped, fmt.Errorf("%w: error while retrieving original dynamic fees: (originalFeePerGas: %s - originalPriorityFeePerGas: %s). Maximum price configured: %s", + commonfee.ErrBump, originalFee.FeeCap, originalFee.TipCap, maxPrice) + } + + currentDynamicPrice, err := f.getDynamicPrice() + if err != nil { + return + } + + bumpedMaxPriorityFeePerGas := originalFee.TipCap.AddPercentage(f.config.BumpPercent) + bumpedMaxFeePerGas := originalFee.FeeCap.AddPercentage(f.config.BumpPercent) + + bumpedMaxPriorityFeePerGas, err = LimitBumpedFee(originalFee.TipCap, currentDynamicPrice.TipCap, bumpedMaxPriorityFeePerGas, maxPrice) + if err != nil { + return bumped, fmt.Errorf("failed to limit maxPriorityFeePerGas: %w", err) + } + + priorityFeeThreshold, e := f.getPriorityFeeThreshold() + if e != nil { + return bumped, e + } + + if bumpedMaxPriorityFeePerGas.Cmp(priorityFeeThreshold) > 0 { + return bumped, fmt.Errorf("bumpedMaxPriorityFeePerGas: %s is above market's %sth percentile: %s, bumping is halted", + bumpedMaxPriorityFeePerGas, strconv.Itoa(ConnectivityPercentile), priorityFeeThreshold) + } + + bumpedMaxFeePerGas, err = LimitBumpedFee(originalFee.FeeCap, currentDynamicPrice.FeeCap, bumpedMaxFeePerGas, maxPrice) + if err != nil { + return bumped, fmt.Errorf("failed to limit maxFeePerGas: %w", err) + } + + bumpedFee := DynamicFee{FeeCap: bumpedMaxFeePerGas, TipCap: bumpedMaxPriorityFeePerGas} + f.logger.Debugw("bumped dynamic fee", "originalFee", originalFee, "marketFee", currentDynamicPrice, "bumpedFee", bumpedFee) + + return bumpedFee, nil +} + +// LimitBumpedFee selects the maximum value between the bumped attempt and the current fee, if there is one. If the result is higher than the max price it gets capped. +// Geth's implementation has a hard 10% minimum limit for the bumped values, otherwise it rejects the transaction with an error. +// See: https://github.com/ethereum/go-ethereum/blob/bff330335b94af3643ac2fb809793f77de3069d4/core/tx_list.go#L298 +// +// Note: for chains that support EIP-1559 but we still choose to send Legacy transactions to them, the limit is still enforcable due to the fact that Legacy transactions +// are treated the same way as Dynamic transactions under the hood. For chains that don't support EIP-1559 at all, the limit isn't enforcable but a 10% minimum bump percentage +// makes sense anyway. +func LimitBumpedFee(originalFee *assets.Wei, currentFee *assets.Wei, bumpedFee *assets.Wei, maxPrice *assets.Wei) (*assets.Wei, error) { + if currentFee != nil { + bumpedFee = assets.WeiMax(currentFee, bumpedFee) + } + bumpedFee = assets.WeiMin(bumpedFee, maxPrice) + + // The first check is added for the following edge case: + // If originalFee is below 10 wei, then adding the minimum bump percentage won't have any effect on the final value because of rounding down. + // Similarly for bumpedFee, it can have the exact same value as the originalFee, even if we bumped, given an originalFee of less than 10 wei + // and a small enough BumpPercent. + if bumpedFee.Cmp(originalFee) == 0 || + bumpedFee.Cmp(originalFee.AddPercentage(MinimumBumpPercentage)) < 0 { + return nil, fmt.Errorf("%w: %s is bumped less than minimum allowed percentage(%s) from originalFee: %s - maxPrice: %s", + commonfee.ErrBump, bumpedFee, strconv.Itoa(MinimumBumpPercentage), originalFee, maxPrice) + } + return bumpedFee, nil +} + +func (f *FeeHistoryEstimator) getPriorityFeeThreshold() (*assets.Wei, error) { + f.priorityFeeThresholdMu.RLock() + defer f.priorityFeeThresholdMu.RUnlock() + if f.priorityFeeThreshold == nil { + return f.priorityFeeThreshold, fmt.Errorf("priorityFeeThreshold not set") + } + return f.priorityFeeThreshold, nil +} + +func (f *FeeHistoryEstimator) Name() string { return f.logger.Name() } +func (f *FeeHistoryEstimator) L1Oracle() rollups.L1Oracle { return f.l1Oracle } +func (f *FeeHistoryEstimator) HealthReport() map[string]error { return map[string]error{f.Name(): nil} } +func (f *FeeHistoryEstimator) OnNewLongestChain(context.Context, *evmtypes.Head) {} diff --git a/core/chains/evm/gas/fee_history_estimator_test.go b/core/chains/evm/gas/fee_history_estimator_test.go new file mode 100644 index 0000000000..6e42e0e209 --- /dev/null +++ b/core/chains/evm/gas/fee_history_estimator_test.go @@ -0,0 +1,513 @@ +package gas_test + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" +) + +func TestFeeHistoryEstimatorLifecycle(t *testing.T) { + t.Parallel() + var gasLimit uint64 = 21000 + maxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("fails if you fetch gas price before the estimator starts", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{ + BumpPercent: 20, + RewardPercentile: 60, + EIP1559: false, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + _, _, err := u.GetLegacyGas(tests.Context(t), nil, gasLimit, maxPrice) + assert.ErrorContains(t, err, "gas price not set") + }) + + t.Run("fails to start if BumpPercent is lower than the minimum cap", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 9} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + assert.ErrorContains(t, u.Start(tests.Context(t)), "BumpPercent") + }) + + t.Run("fails to start if RewardPercentile is higher than ConnectivityPercentile in EIP-1559", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{ + BumpPercent: 20, + RewardPercentile: 99, + EIP1559: true, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + assert.ErrorContains(t, u.Start(tests.Context(t)), "RewardPercentile") + }) + + t.Run("starts if configs are correct", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(10), nil).Maybe() + + cfg := gas.FeeHistoryEstimatorConfig{ + BumpPercent: 20, + RewardPercentile: 10, + CacheTimeout: 10 * time.Second, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + err := u.Start(tests.Context(t)) + assert.NoError(t, err) + err = u.Close() + assert.NoError(t, err) + }) +} + +func TestFeeHistoryEstimatorGetLegacyGas(t *testing.T) { + t.Parallel() + + var gasLimit uint64 = 21000 + maxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("fetches a new gas price when first called", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(10), nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + _, err := u.RefreshGasPrice() + assert.NoError(t, err) + gasPrice, _, err := u.GetLegacyGas(tests.Context(t), nil, gasLimit, maxPrice) + assert.NoError(t, err) + assert.Equal(t, assets.NewWeiI(10), gasPrice) + }) + + t.Run("will return max price if estimation exceeds it", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(10), nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{} + + maxPrice := assets.NewWeiI(1) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + _, err := u.RefreshGasPrice() + assert.NoError(t, err) + gas1, _, err := u.GetLegacyGas(tests.Context(t), nil, gasLimit, maxPrice) + assert.NoError(t, err) + assert.Equal(t, maxPrice, gas1) + }) + + t.Run("fails if gas price has not been set yet", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{} + + maxPrice := assets.NewWeiI(1) + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + _, _, err := u.GetLegacyGas(tests.Context(t), nil, gasLimit, maxPrice) + assert.Error(t, err) + assert.ErrorContains(t, err, "gas price not set") + }) +} + +func TestFeeHistoryEstimatorBumpLegacyGas(t *testing.T) { + t.Parallel() + + var gasLimit uint64 = 21000 + maxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("bumps a previous attempt by BumpPercent", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalGasPrice := assets.NewWeiI(10) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(10), nil) + + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 50, CacheTimeout: 5 * time.Second} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + servicetest.RunHealthy(t, u) + gasPrice, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, assets.NewWeiI(15), gasPrice) + }) + + t.Run("fails if the original attempt is nil, or equal or higher than the max price", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + + cfg := gas.FeeHistoryEstimatorConfig{} + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + + var originalPrice *assets.Wei + _, _, err := u.BumpLegacyGas(tests.Context(t), originalPrice, gasLimit, maxPrice, nil) + assert.Error(t, err) + + originalPrice = assets.NewWeiI(100) + _, _, err = u.BumpLegacyGas(tests.Context(t), originalPrice, gasLimit, maxPrice, nil) + assert.Error(t, err) + }) + + t.Run("returns market gas price if bumped original fee is lower", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(80), nil).Once() + originalGasPrice := assets.NewWeiI(10) + + cfg := gas.FeeHistoryEstimatorConfig{} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + gas, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, assets.NewWeiI(80), gas) + }) + + t.Run("returns max gas price if bumped original fee is higher", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(1), nil).Once() + originalGasPrice := assets.NewWeiI(10) + + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 50} + + maxPrice := assets.NewWeiI(14) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + gas, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, maxPrice, gas) + }) + + t.Run("returns max gas price if the aggregation of max and original bumped fee is higher", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(1), nil).Once() + originalGasPrice := assets.NewWeiI(10) + + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 50} + + maxPrice := assets.NewWeiI(14) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + gas, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, maxPrice, gas) + }) + + t.Run("fails if the bumped gas price is lower than the minimum bump percentage", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(100), nil).Once() + originalGasPrice := assets.NewWeiI(100) + + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 20} + + // Price will be capped by the max price + maxPrice := assets.NewWeiI(101) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + _, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.Error(t, err) + }) +} + +func TestFeeHistoryEstimatorGetDynamicFee(t *testing.T) { + t.Parallel() + + maxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("fetches a new dynamic fee when first called", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + baseFee := big.NewInt(5) + maxPriorityFeePerGas1 := big.NewInt(33) + maxPriorityFeePerGas2 := big.NewInt(20) + + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas1, big.NewInt(5)}, {maxPriorityFeePerGas2, big.NewInt(5)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee, baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + blockHistoryLength := 2 + cfg := gas.FeeHistoryEstimatorConfig{BlockHistorySize: uint64(blockHistoryLength)} + avrgPriorityFee := big.NewInt(0) + avrgPriorityFee.Add(maxPriorityFeePerGas1, maxPriorityFeePerGas2).Div(avrgPriorityFee, big.NewInt(int64(blockHistoryLength))) + maxFee := (*assets.Wei)(baseFee).AddPercentage(gas.BaseFeeBufferPercentage).Add((*assets.Wei)(avrgPriorityFee)) + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + dynamicFee, err := u.GetDynamicFee(tests.Context(t), maxPrice) + assert.NoError(t, err) + assert.Equal(t, maxFee, dynamicFee.FeeCap) + assert.Equal(t, (*assets.Wei)(avrgPriorityFee), dynamicFee.TipCap) + }) + + t.Run("fails if dynamic prices have not been set yet", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{} + + maxPrice := assets.NewWeiI(1) + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + _, err := u.GetDynamicFee(tests.Context(t), maxPrice) + assert.Error(t, err) + assert.ErrorContains(t, err, "dynamic price not set") + }) + + t.Run("will return max price if tip cap or fee cap exceed it", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + baseFee := big.NewInt(1) + maxPriorityFeePerGas := big.NewInt(3) + maxPrice := assets.NewWeiI(2) + + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(5)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{BlockHistorySize: 1} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + dynamicFee, err := u.GetDynamicFee(tests.Context(t), maxPrice) + assert.NoError(t, err) + assert.Equal(t, maxPrice, dynamicFee.FeeCap) + assert.Equal(t, maxPrice, dynamicFee.TipCap) + }) +} + +func TestFeeHistoryEstimatorBumpDynamicFee(t *testing.T) { + t.Parallel() + + globalMaxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("bumps a previous attempt by BumpPercent", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(10), + } + + // These values will be ignored because they are lower prices than the originalFee + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{big.NewInt(5), big.NewInt(50)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{big.NewInt(5)}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 2, + BumpPercent: 50, + } + + expectedFeeCap := originalFee.FeeCap.AddPercentage(cfg.BumpPercent) + expectedTipCap := originalFee.TipCap.AddPercentage(cfg.BumpPercent) + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + dynamicFee, err := u.BumpDynamicFee(tests.Context(t), originalFee, globalMaxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, expectedFeeCap, dynamicFee.FeeCap) + assert.Equal(t, expectedTipCap, dynamicFee.TipCap) + }) + + t.Run("fails if the original attempt is invalid", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + maxPrice := assets.NewWeiI(20) + cfg := gas.FeeHistoryEstimatorConfig{BlockHistorySize: 1} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + // nil original fee + var originalFee gas.DynamicFee + _, err := u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.Error(t, err) + + // tip cap is higher than fee cap + originalFee = gas.DynamicFee{ + FeeCap: assets.NewWeiI(10), + TipCap: assets.NewWeiI(11), + } + _, err = u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.Error(t, err) + + // fee cap is equal or higher to max price + originalFee = gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(10), + } + _, err = u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.Error(t, err) + }) + + t.Run("returns market prices if bumped original fee is lower", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(10), + } + + // Market fees + baseFee := big.NewInt(5) + maxPriorityFeePerGas := big.NewInt(33) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(100)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + maxFee := (*assets.Wei)(baseFee).AddPercentage(gas.BaseFeeBufferPercentage).Add((*assets.Wei)(maxPriorityFeePerGas)) + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 1, + BumpPercent: 50, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + bumpedFee, err := u.BumpDynamicFee(tests.Context(t), originalFee, globalMaxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, (*assets.Wei)(maxPriorityFeePerGas), bumpedFee.TipCap) + assert.Equal(t, maxFee, bumpedFee.FeeCap) + }) + + t.Run("fails if connectivity percentile value is reached", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(10), + } + + // Market fees + baseFee := big.NewInt(5) + maxPriorityFeePerGas := big.NewInt(33) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(30)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 1, + BumpPercent: 50, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + _, err = u.BumpDynamicFee(tests.Context(t), originalFee, globalMaxPrice, nil) + assert.Error(t, err) + }) + + t.Run("returns max price if the aggregation of max and original bumped fee is higher", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(18), + } + + maxPrice := assets.NewWeiI(25) + // Market fees + baseFee := big.NewInt(1) + maxPriorityFeePerGas := big.NewInt(1) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(30)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 1, + BumpPercent: 50, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + bumpedFee, err := u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, maxPrice, bumpedFee.TipCap) + assert.Equal(t, maxPrice, bumpedFee.FeeCap) + }) + + t.Run("fails if the bumped gas price is lower than the minimum bump percentage", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(18), + } + + maxPrice := assets.NewWeiI(21) + // Market fees + baseFee := big.NewInt(1) + maxPriorityFeePerGas := big.NewInt(1) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(30)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 1, + BumpPercent: 50, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + _, err = u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.Error(t, err) + }) + + t.Run("ignores maxPriorityFeePerGas if there is no mempool and forces refetch", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(40), + TipCap: assets.NewWeiI(0), + } + + // Market fees + baseFee := big.NewInt(10) + maxPriorityFeePerGas := big.NewInt(0) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(0)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil) + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 0, + BumpPercent: 20, + CacheTimeout: 10 * time.Second, + EIP1559: true, + } + + maxFeePerGas := assets.NewWei(baseFee).AddPercentage(gas.BaseFeeBufferPercentage) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + servicetest.RunHealthy(t, u) + bumpedFee, err := u.BumpDynamicFee(tests.Context(t), originalFee, globalMaxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, assets.NewWeiI(0), (*assets.Wei)(maxPriorityFeePerGas)) + assert.Equal(t, maxFeePerGas, bumpedFee.FeeCap) + }) +} diff --git a/core/chains/evm/gas/helpers_test.go b/core/chains/evm/gas/helpers_test.go index 2c12ed426a..ae93d9e8c7 100644 --- a/core/chains/evm/gas/helpers_test.go +++ b/core/chains/evm/gas/helpers_test.go @@ -157,6 +157,7 @@ type MockGasEstimatorConfig struct { FeeCapDefaultF *assets.Wei LimitMaxF uint64 ModeF string + EstimateLimitF bool } func NewMockGasConfig() *MockGasEstimatorConfig { @@ -214,3 +215,7 @@ func (m *MockGasEstimatorConfig) LimitMax() uint64 { func (m *MockGasEstimatorConfig) Mode() string { return m.ModeF } + +func (m *MockGasEstimatorConfig) EstimateLimit() bool { + return m.EstimateLimitF +} diff --git a/core/chains/evm/gas/mocks/evm_fee_estimator.go b/core/chains/evm/gas/mocks/evm_fee_estimator.go index a9adc261ce..a7deca2c63 100644 --- a/core/chains/evm/gas/mocks/evm_fee_estimator.go +++ b/core/chains/evm/gas/mocks/evm_fee_estimator.go @@ -7,6 +7,8 @@ import ( assets "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + common "github.com/ethereum/go-ethereum/common" + context "context" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" @@ -145,14 +147,14 @@ func (_c *EvmFeeEstimator_Close_Call) RunAndReturn(run func() error) *EvmFeeEsti return _c } -// GetFee provides a mock function with given fields: ctx, calldata, feeLimit, maxFeePrice, opts -func (_m *EvmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...types.Opt) (gas.EvmFee, uint64, error) { +// GetFee provides a mock function with given fields: ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts +func (_m *EvmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress *common.Address, toAddress *common.Address, opts ...types.Opt) (gas.EvmFee, uint64, error) { _va := make([]interface{}, len(opts)) for _i := range opts { _va[_i] = opts[_i] } var _ca []interface{} - _ca = append(_ca, ctx, calldata, feeLimit, maxFeePrice) + _ca = append(_ca, ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress) _ca = append(_ca, _va...) ret := _m.Called(_ca...) @@ -163,23 +165,23 @@ func (_m *EvmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit var r0 gas.EvmFee var r1 uint64 var r2 error - if rf, ok := ret.Get(0).(func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) (gas.EvmFee, uint64, error)); ok { - return rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(0).(func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) (gas.EvmFee, uint64, error)); ok { + return rf(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } - if rf, ok := ret.Get(0).(func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) gas.EvmFee); ok { - r0 = rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(0).(func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) gas.EvmFee); ok { + r0 = rf(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { r0 = ret.Get(0).(gas.EvmFee) } - if rf, ok := ret.Get(1).(func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) uint64); ok { - r1 = rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(1).(func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) uint64); ok { + r1 = rf(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { r1 = ret.Get(1).(uint64) } - if rf, ok := ret.Get(2).(func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) error); ok { - r2 = rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(2).(func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) error); ok { + r2 = rf(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { r2 = ret.Error(2) } @@ -197,43 +199,45 @@ type EvmFeeEstimator_GetFee_Call struct { // - calldata []byte // - feeLimit uint64 // - maxFeePrice *assets.Wei +// - fromAddress *common.Address +// - toAddress *common.Address // - opts ...types.Opt -func (_e *EvmFeeEstimator_Expecter) GetFee(ctx interface{}, calldata interface{}, feeLimit interface{}, maxFeePrice interface{}, opts ...interface{}) *EvmFeeEstimator_GetFee_Call { +func (_e *EvmFeeEstimator_Expecter) GetFee(ctx interface{}, calldata interface{}, feeLimit interface{}, maxFeePrice interface{}, fromAddress interface{}, toAddress interface{}, opts ...interface{}) *EvmFeeEstimator_GetFee_Call { return &EvmFeeEstimator_GetFee_Call{Call: _e.mock.On("GetFee", - append([]interface{}{ctx, calldata, feeLimit, maxFeePrice}, opts...)...)} + append([]interface{}{ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress}, opts...)...)} } -func (_c *EvmFeeEstimator_GetFee_Call) Run(run func(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...types.Opt)) *EvmFeeEstimator_GetFee_Call { +func (_c *EvmFeeEstimator_GetFee_Call) Run(run func(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress *common.Address, toAddress *common.Address, opts ...types.Opt)) *EvmFeeEstimator_GetFee_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]types.Opt, len(args)-4) - for i, a := range args[4:] { + variadicArgs := make([]types.Opt, len(args)-6) + for i, a := range args[6:] { if a != nil { variadicArgs[i] = a.(types.Opt) } } - run(args[0].(context.Context), args[1].([]byte), args[2].(uint64), args[3].(*assets.Wei), variadicArgs...) + run(args[0].(context.Context), args[1].([]byte), args[2].(uint64), args[3].(*assets.Wei), args[4].(*common.Address), args[5].(*common.Address), variadicArgs...) }) return _c } -func (_c *EvmFeeEstimator_GetFee_Call) Return(fee gas.EvmFee, chainSpecificFeeLimit uint64, err error) *EvmFeeEstimator_GetFee_Call { - _c.Call.Return(fee, chainSpecificFeeLimit, err) +func (_c *EvmFeeEstimator_GetFee_Call) Return(fee gas.EvmFee, estimatedFeeLimit uint64, err error) *EvmFeeEstimator_GetFee_Call { + _c.Call.Return(fee, estimatedFeeLimit, err) return _c } -func (_c *EvmFeeEstimator_GetFee_Call) RunAndReturn(run func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) (gas.EvmFee, uint64, error)) *EvmFeeEstimator_GetFee_Call { +func (_c *EvmFeeEstimator_GetFee_Call) RunAndReturn(run func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) (gas.EvmFee, uint64, error)) *EvmFeeEstimator_GetFee_Call { _c.Call.Return(run) return _c } -// GetMaxCost provides a mock function with given fields: ctx, amount, calldata, feeLimit, maxFeePrice, opts -func (_m *EvmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...types.Opt) (*big.Int, error) { +// GetMaxCost provides a mock function with given fields: ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts +func (_m *EvmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress *common.Address, toAddress *common.Address, opts ...types.Opt) (*big.Int, error) { _va := make([]interface{}, len(opts)) for _i := range opts { _va[_i] = opts[_i] } var _ca []interface{} - _ca = append(_ca, ctx, amount, calldata, feeLimit, maxFeePrice) + _ca = append(_ca, ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress) _ca = append(_ca, _va...) ret := _m.Called(_ca...) @@ -243,19 +247,19 @@ func (_m *EvmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, ca var r0 *big.Int var r1 error - if rf, ok := ret.Get(0).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, ...types.Opt) (*big.Int, error)); ok { - return rf(ctx, amount, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(0).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) (*big.Int, error)); ok { + return rf(ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } - if rf, ok := ret.Get(0).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, ...types.Opt) *big.Int); ok { - r0 = rf(ctx, amount, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(0).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) *big.Int); ok { + r0 = rf(ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*big.Int) } } - if rf, ok := ret.Get(1).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, ...types.Opt) error); ok { - r1 = rf(ctx, amount, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(1).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) error); ok { + r1 = rf(ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { r1 = ret.Error(1) } @@ -274,21 +278,23 @@ type EvmFeeEstimator_GetMaxCost_Call struct { // - calldata []byte // - feeLimit uint64 // - maxFeePrice *assets.Wei +// - fromAddress *common.Address +// - toAddress *common.Address // - opts ...types.Opt -func (_e *EvmFeeEstimator_Expecter) GetMaxCost(ctx interface{}, amount interface{}, calldata interface{}, feeLimit interface{}, maxFeePrice interface{}, opts ...interface{}) *EvmFeeEstimator_GetMaxCost_Call { +func (_e *EvmFeeEstimator_Expecter) GetMaxCost(ctx interface{}, amount interface{}, calldata interface{}, feeLimit interface{}, maxFeePrice interface{}, fromAddress interface{}, toAddress interface{}, opts ...interface{}) *EvmFeeEstimator_GetMaxCost_Call { return &EvmFeeEstimator_GetMaxCost_Call{Call: _e.mock.On("GetMaxCost", - append([]interface{}{ctx, amount, calldata, feeLimit, maxFeePrice}, opts...)...)} + append([]interface{}{ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress}, opts...)...)} } -func (_c *EvmFeeEstimator_GetMaxCost_Call) Run(run func(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...types.Opt)) *EvmFeeEstimator_GetMaxCost_Call { +func (_c *EvmFeeEstimator_GetMaxCost_Call) Run(run func(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress *common.Address, toAddress *common.Address, opts ...types.Opt)) *EvmFeeEstimator_GetMaxCost_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]types.Opt, len(args)-5) - for i, a := range args[5:] { + variadicArgs := make([]types.Opt, len(args)-7) + for i, a := range args[7:] { if a != nil { variadicArgs[i] = a.(types.Opt) } } - run(args[0].(context.Context), args[1].(assets.Eth), args[2].([]byte), args[3].(uint64), args[4].(*assets.Wei), variadicArgs...) + run(args[0].(context.Context), args[1].(assets.Eth), args[2].([]byte), args[3].(uint64), args[4].(*assets.Wei), args[5].(*common.Address), args[6].(*common.Address), variadicArgs...) }) return _c } @@ -298,7 +304,7 @@ func (_c *EvmFeeEstimator_GetMaxCost_Call) Return(_a0 *big.Int, _a1 error) *EvmF return _c } -func (_c *EvmFeeEstimator_GetMaxCost_Call) RunAndReturn(run func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, ...types.Opt) (*big.Int, error)) *EvmFeeEstimator_GetMaxCost_Call { +func (_c *EvmFeeEstimator_GetMaxCost_Call) RunAndReturn(run func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) (*big.Int, error)) *EvmFeeEstimator_GetMaxCost_Call { _c.Call.Return(run) return _c } diff --git a/core/chains/evm/gas/mocks/fee_estimator_client.go b/core/chains/evm/gas/mocks/fee_estimator_client.go index 8e10107597..f5ca52ec92 100644 --- a/core/chains/evm/gas/mocks/fee_estimator_client.go +++ b/core/chains/evm/gas/mocks/fee_estimator_client.go @@ -241,6 +241,123 @@ func (_c *FeeEstimatorClient_ConfiguredChainID_Call) RunAndReturn(run func() *bi return _c } +// EstimateGas provides a mock function with given fields: ctx, call +func (_m *FeeEstimatorClient) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { + ret := _m.Called(ctx, call) + + if len(ret) == 0 { + panic("no return value specified for EstimateGas") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) (uint64, error)); ok { + return rf(ctx, call) + } + if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) uint64); ok { + r0 = rf(ctx, call) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg) error); ok { + r1 = rf(ctx, call) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeEstimatorClient_EstimateGas_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EstimateGas' +type FeeEstimatorClient_EstimateGas_Call struct { + *mock.Call +} + +// EstimateGas is a helper method to define mock.On call +// - ctx context.Context +// - call ethereum.CallMsg +func (_e *FeeEstimatorClient_Expecter) EstimateGas(ctx interface{}, call interface{}) *FeeEstimatorClient_EstimateGas_Call { + return &FeeEstimatorClient_EstimateGas_Call{Call: _e.mock.On("EstimateGas", ctx, call)} +} + +func (_c *FeeEstimatorClient_EstimateGas_Call) Run(run func(ctx context.Context, call ethereum.CallMsg)) *FeeEstimatorClient_EstimateGas_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(ethereum.CallMsg)) + }) + return _c +} + +func (_c *FeeEstimatorClient_EstimateGas_Call) Return(_a0 uint64, _a1 error) *FeeEstimatorClient_EstimateGas_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FeeEstimatorClient_EstimateGas_Call) RunAndReturn(run func(context.Context, ethereum.CallMsg) (uint64, error)) *FeeEstimatorClient_EstimateGas_Call { + _c.Call.Return(run) + return _c +} + +// FeeHistory provides a mock function with given fields: ctx, blockCount, rewardPercentiles +func (_m *FeeEstimatorClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { + ret := _m.Called(ctx, blockCount, rewardPercentiles) + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 *ethereum.FeeHistory + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)); ok { + return rf(ctx, blockCount, rewardPercentiles) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) *ethereum.FeeHistory); ok { + r0 = rf(ctx, blockCount, rewardPercentiles) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethereum.FeeHistory) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, []float64) error); ok { + r1 = rf(ctx, blockCount, rewardPercentiles) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeEstimatorClient_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type FeeEstimatorClient_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +// - ctx context.Context +// - blockCount uint64 +// - rewardPercentiles []float64 +func (_e *FeeEstimatorClient_Expecter) FeeHistory(ctx interface{}, blockCount interface{}, rewardPercentiles interface{}) *FeeEstimatorClient_FeeHistory_Call { + return &FeeEstimatorClient_FeeHistory_Call{Call: _e.mock.On("FeeHistory", ctx, blockCount, rewardPercentiles)} +} + +func (_c *FeeEstimatorClient_FeeHistory_Call) Run(run func(ctx context.Context, blockCount uint64, rewardPercentiles []float64)) *FeeEstimatorClient_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].([]float64)) + }) + return _c +} + +func (_c *FeeEstimatorClient_FeeHistory_Call) Return(feeHistory *ethereum.FeeHistory, err error) *FeeEstimatorClient_FeeHistory_Call { + _c.Call.Return(feeHistory, err) + return _c +} + +func (_c *FeeEstimatorClient_FeeHistory_Call) RunAndReturn(run func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)) *FeeEstimatorClient_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + // HeadByNumber provides a mock function with given fields: ctx, n func (_m *FeeEstimatorClient) HeadByNumber(ctx context.Context, n *big.Int) (*types.Head, error) { ret := _m.Called(ctx, n) @@ -300,6 +417,64 @@ func (_c *FeeEstimatorClient_HeadByNumber_Call) RunAndReturn(run func(context.Co return _c } +// SuggestGasPrice provides a mock function with given fields: ctx +func (_m *FeeEstimatorClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for SuggestGasPrice") + } + + var r0 *big.Int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeEstimatorClient_SuggestGasPrice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SuggestGasPrice' +type FeeEstimatorClient_SuggestGasPrice_Call struct { + *mock.Call +} + +// SuggestGasPrice is a helper method to define mock.On call +// - ctx context.Context +func (_e *FeeEstimatorClient_Expecter) SuggestGasPrice(ctx interface{}) *FeeEstimatorClient_SuggestGasPrice_Call { + return &FeeEstimatorClient_SuggestGasPrice_Call{Call: _e.mock.On("SuggestGasPrice", ctx)} +} + +func (_c *FeeEstimatorClient_SuggestGasPrice_Call) Run(run func(ctx context.Context)) *FeeEstimatorClient_SuggestGasPrice_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *FeeEstimatorClient_SuggestGasPrice_Call) Return(_a0 *big.Int, _a1 error) *FeeEstimatorClient_SuggestGasPrice_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FeeEstimatorClient_SuggestGasPrice_Call) RunAndReturn(run func(context.Context) (*big.Int, error)) *FeeEstimatorClient_SuggestGasPrice_Call { + _c.Call.Return(run) + return _c +} + // NewFeeEstimatorClient creates a new instance of FeeEstimatorClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewFeeEstimatorClient(t interface { diff --git a/core/chains/evm/gas/mocks/fee_history_estimator_client.go b/core/chains/evm/gas/mocks/fee_history_estimator_client.go new file mode 100644 index 0000000000..7486214501 --- /dev/null +++ b/core/chains/evm/gas/mocks/fee_history_estimator_client.go @@ -0,0 +1,157 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + big "math/big" + + ethereum "github.com/ethereum/go-ethereum" + + mock "github.com/stretchr/testify/mock" +) + +// FeeHistoryEstimatorClient is an autogenerated mock type for the feeHistoryEstimatorClient type +type FeeHistoryEstimatorClient struct { + mock.Mock +} + +type FeeHistoryEstimatorClient_Expecter struct { + mock *mock.Mock +} + +func (_m *FeeHistoryEstimatorClient) EXPECT() *FeeHistoryEstimatorClient_Expecter { + return &FeeHistoryEstimatorClient_Expecter{mock: &_m.Mock} +} + +// FeeHistory provides a mock function with given fields: ctx, blockCount, rewardPercentiles +func (_m *FeeHistoryEstimatorClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { + ret := _m.Called(ctx, blockCount, rewardPercentiles) + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 *ethereum.FeeHistory + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)); ok { + return rf(ctx, blockCount, rewardPercentiles) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) *ethereum.FeeHistory); ok { + r0 = rf(ctx, blockCount, rewardPercentiles) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethereum.FeeHistory) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, []float64) error); ok { + r1 = rf(ctx, blockCount, rewardPercentiles) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeHistoryEstimatorClient_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type FeeHistoryEstimatorClient_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +// - ctx context.Context +// - blockCount uint64 +// - rewardPercentiles []float64 +func (_e *FeeHistoryEstimatorClient_Expecter) FeeHistory(ctx interface{}, blockCount interface{}, rewardPercentiles interface{}) *FeeHistoryEstimatorClient_FeeHistory_Call { + return &FeeHistoryEstimatorClient_FeeHistory_Call{Call: _e.mock.On("FeeHistory", ctx, blockCount, rewardPercentiles)} +} + +func (_c *FeeHistoryEstimatorClient_FeeHistory_Call) Run(run func(ctx context.Context, blockCount uint64, rewardPercentiles []float64)) *FeeHistoryEstimatorClient_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].([]float64)) + }) + return _c +} + +func (_c *FeeHistoryEstimatorClient_FeeHistory_Call) Return(feeHistory *ethereum.FeeHistory, err error) *FeeHistoryEstimatorClient_FeeHistory_Call { + _c.Call.Return(feeHistory, err) + return _c +} + +func (_c *FeeHistoryEstimatorClient_FeeHistory_Call) RunAndReturn(run func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)) *FeeHistoryEstimatorClient_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + +// SuggestGasPrice provides a mock function with given fields: ctx +func (_m *FeeHistoryEstimatorClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for SuggestGasPrice") + } + + var r0 *big.Int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeHistoryEstimatorClient_SuggestGasPrice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SuggestGasPrice' +type FeeHistoryEstimatorClient_SuggestGasPrice_Call struct { + *mock.Call +} + +// SuggestGasPrice is a helper method to define mock.On call +// - ctx context.Context +func (_e *FeeHistoryEstimatorClient_Expecter) SuggestGasPrice(ctx interface{}) *FeeHistoryEstimatorClient_SuggestGasPrice_Call { + return &FeeHistoryEstimatorClient_SuggestGasPrice_Call{Call: _e.mock.On("SuggestGasPrice", ctx)} +} + +func (_c *FeeHistoryEstimatorClient_SuggestGasPrice_Call) Run(run func(ctx context.Context)) *FeeHistoryEstimatorClient_SuggestGasPrice_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *FeeHistoryEstimatorClient_SuggestGasPrice_Call) Return(_a0 *big.Int, _a1 error) *FeeHistoryEstimatorClient_SuggestGasPrice_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FeeHistoryEstimatorClient_SuggestGasPrice_Call) RunAndReturn(run func(context.Context) (*big.Int, error)) *FeeHistoryEstimatorClient_SuggestGasPrice_Call { + _c.Call.Return(run) + return _c +} + +// NewFeeHistoryEstimatorClient creates a new instance of FeeHistoryEstimatorClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFeeHistoryEstimatorClient(t interface { + mock.TestingT + Cleanup(func()) +}) *FeeHistoryEstimatorClient { + mock := &FeeHistoryEstimatorClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index c9607091fb..d844f8774a 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -26,6 +26,9 @@ import ( evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) +// EstimateGasBuffer is a multiplier applied to estimated gas when the EstimateLimit feature is enabled +const EstimateGasBuffer = float32(1.15) + // EvmFeeEstimator provides a unified interface that wraps EvmEstimator and can determine if legacy or dynamic fee estimation should be used type EvmFeeEstimator interface { services.Service @@ -33,11 +36,11 @@ type EvmFeeEstimator interface { // L1Oracle returns the L1 gas price oracle only if the chain has one, e.g. OP stack L2s and Arbitrum. L1Oracle() rollups.L1Oracle - GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint64, err error) + GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (fee EvmFee, estimatedFeeLimit uint64, err error) BumpFee(ctx context.Context, originalFee EvmFee, feeLimit uint64, maxFeePrice *assets.Wei, attempts []EvmPriorAttempt) (bumpedFee EvmFee, chainSpecificFeeLimit uint64, err error) // GetMaxCost returns the total value = max price x fee units + transferred value - GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (*big.Int, error) + GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (*big.Int, error) } type feeEstimatorClient interface { @@ -46,6 +49,9 @@ type feeEstimatorClient interface { CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error ConfiguredChainID() *big.Int HeadByNumber(ctx context.Context, n *big.Int) (*evmtypes.Head, error) + EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) + SuggestGasPrice(ctx context.Context) (*big.Int, error) + FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) } // NewEstimator returns the estimator for a given config @@ -70,6 +76,7 @@ func NewEstimator(lggr logger.Logger, ethClient feeEstimatorClient, cfg Config, "tipCapMin", geCfg.TipCapMin(), "priceMax", geCfg.PriceMax(), "priceMin", geCfg.PriceMin(), + "estimateLimit", geCfg.EstimateLimit(), ) df := geCfg.EIP1559DynamicFees() @@ -105,13 +112,25 @@ func NewEstimator(lggr logger.Logger, ethClient feeEstimatorClient, cfg Config, newEstimator = func(l logger.Logger) EvmEstimator { return NewSuggestedPriceEstimator(lggr, ethClient, geCfg, l1Oracle) } + case "FeeHistory": + newEstimator = func(l logger.Logger) EvmEstimator { + ccfg := FeeHistoryEstimatorConfig{ + BumpPercent: geCfg.BumpPercent(), + CacheTimeout: geCfg.FeeHistory().CacheTimeout(), + EIP1559: geCfg.EIP1559DynamicFees(), + BlockHistorySize: uint64(geCfg.BlockHistory().BlockHistorySize()), + RewardPercentile: float64(geCfg.BlockHistory().TransactionPercentile()), + } + return NewFeeHistoryEstimator(lggr, ethClient, ccfg, ethClient.ConfiguredChainID(), l1Oracle) + } + default: lggr.Warnf("GasEstimator: unrecognised mode '%s', falling back to FixedPriceEstimator", s) newEstimator = func(l logger.Logger) EvmEstimator { return NewFixedPriceEstimator(geCfg, ethClient, bh, lggr, l1Oracle) } } - return NewEvmFeeEstimator(lggr, newEstimator, df, geCfg), nil + return NewEvmFeeEstimator(lggr, newEstimator, df, geCfg, ethClient), nil } // DynamicFee encompasses both FeeCap and TipCap for EIP1559 transactions @@ -182,17 +201,19 @@ type evmFeeEstimator struct { EvmEstimator EIP1559Enabled bool geCfg GasEstimatorConfig + ethClient feeEstimatorClient } var _ EvmFeeEstimator = (*evmFeeEstimator)(nil) -func NewEvmFeeEstimator(lggr logger.Logger, newEstimator func(logger.Logger) EvmEstimator, eip1559Enabled bool, geCfg GasEstimatorConfig) EvmFeeEstimator { +func NewEvmFeeEstimator(lggr logger.Logger, newEstimator func(logger.Logger) EvmEstimator, eip1559Enabled bool, geCfg GasEstimatorConfig, ethClient feeEstimatorClient) EvmFeeEstimator { lggr = logger.Named(lggr, "WrappedEvmEstimator") return &evmFeeEstimator{ lggr: lggr, EvmEstimator: newEstimator(lggr), EIP1559Enabled: eip1559Enabled, geCfg: geCfg, + ethClient: ethClient, } } @@ -262,7 +283,10 @@ func (e *evmFeeEstimator) L1Oracle() rollups.L1Oracle { return e.EvmEstimator.L1Oracle() } -func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint64, err error) { +// GetFee returns an initial estimated gas price and gas limit for a transaction +// The gas limit provided by the caller can be adjusted by gas estimation or for 2D fees +func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (fee EvmFee, estimatedFeeLimit uint64, err error) { + var chainSpecificFeeLimit uint64 // get dynamic fee if e.EIP1559Enabled { var dynamicFee DynamicFee @@ -270,24 +294,23 @@ func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit if err != nil { return } - chainSpecificFeeLimit, err = commonfee.ApplyMultiplier(feeLimit, e.geCfg.LimitMultiplier()) fee.DynamicFeeCap = dynamicFee.FeeCap fee.DynamicTipCap = dynamicFee.TipCap - return - } - - // get legacy fee - fee.Legacy, chainSpecificFeeLimit, err = e.EvmEstimator.GetLegacyGas(ctx, calldata, feeLimit, maxFeePrice, opts...) - if err != nil { - return + chainSpecificFeeLimit = feeLimit + } else { + // get legacy fee + fee.Legacy, chainSpecificFeeLimit, err = e.EvmEstimator.GetLegacyGas(ctx, calldata, feeLimit, maxFeePrice, opts...) + if err != nil { + return + } } - chainSpecificFeeLimit, err = commonfee.ApplyMultiplier(chainSpecificFeeLimit, e.geCfg.LimitMultiplier()) + estimatedFeeLimit, err = e.estimateFeeLimit(ctx, chainSpecificFeeLimit, calldata, fromAddress, toAddress) return } -func (e *evmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (*big.Int, error) { - fees, gasLimit, err := e.GetFee(ctx, calldata, feeLimit, maxFeePrice, opts...) +func (e *evmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (*big.Int, error) { + fees, gasLimit, err := e.GetFee(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) if err != nil { return nil, err } @@ -338,6 +361,56 @@ func (e *evmFeeEstimator) BumpFee(ctx context.Context, originalFee EvmFee, feeLi return } +func (e *evmFeeEstimator) estimateFeeLimit(ctx context.Context, feeLimit uint64, calldata []byte, fromAddress, toAddress *common.Address) (estimatedFeeLimit uint64, err error) { + // Use the feeLimit * LimitMultiplier as the provided gas limit since this multiplier is applied on top of the caller specified gas limit + providedGasLimit, err := commonfee.ApplyMultiplier(feeLimit, e.geCfg.LimitMultiplier()) + if err != nil { + return estimatedFeeLimit, err + } + // Use provided fee limit by default if EstimateLimit is disabled + if !e.geCfg.EstimateLimit() { + return providedGasLimit, nil + } + // Create call msg for gas limit estimation + // Skip setting Gas to avoid capping the results of the estimation + callMsg := ethereum.CallMsg{ + To: toAddress, + Data: calldata, + } + if fromAddress != nil { + callMsg.From = *fromAddress + } + estimatedGas, estimateErr := e.ethClient.EstimateGas(ctx, callMsg) + if estimateErr != nil { + if providedGasLimit > 0 { + // Do not return error if estimate gas failed, we can still use the provided limit instead since it is an upper limit + e.lggr.Errorw("failed to estimate gas limit. falling back to the provided gas limit with multiplier", "callMsg", callMsg, "providedGasLimitWithMultiplier", providedGasLimit, "error", estimateErr) + return providedGasLimit, nil + } + return estimatedFeeLimit, fmt.Errorf("gas estimation failed and provided gas limit is 0: %w", estimateErr) + } + e.lggr.Debugw("estimated gas", "estimatedGas", estimatedGas, "providedGasLimitWithMultiplier", providedGasLimit) + // Return error if estimated gas without the buffer exceeds the provided gas limit, if provided + // Transaction would be destined to run out of gas and fail + if providedGasLimit > 0 && estimatedGas > providedGasLimit { + e.lggr.Errorw("estimated gas exceeds provided gas limit with multiplier", "estimatedGas", estimatedGas, "providedGasLimitWithMultiplier", providedGasLimit) + return estimatedFeeLimit, commonfee.ErrFeeLimitTooLow + } + // Apply EstimateGasBuffer to the estimated gas limit + estimatedFeeLimit, err = commonfee.ApplyMultiplier(estimatedGas, EstimateGasBuffer) + if err != nil { + return + } + // If provided gas limit is not 0, fallback to it if the buffer causes the estimated gas limit to exceed it + // The provided gas limit should be used as an upper bound to avoid unexpected behavior for products + if providedGasLimit > 0 && estimatedFeeLimit > providedGasLimit { + e.lggr.Debugw("estimated gas limit with buffer exceeds the provided gas limit with multiplier. falling back to the provided gas limit with multiplier", "estimatedGasLimit", estimatedFeeLimit, "providedGasLimitWithMultiplier", providedGasLimit) + estimatedFeeLimit = providedGasLimit + } + + return +} + // Config defines an interface for configuration in the gas package type Config interface { ChainType() chaintype.ChainType @@ -359,6 +432,7 @@ type GasEstimatorConfig interface { PriceMin() *assets.Wei PriceMax() *assets.Wei Mode() string + EstimateLimit() bool } type BlockHistoryConfig interface { diff --git a/core/chains/evm/gas/models_test.go b/core/chains/evm/gas/models_test.go index 92ea901596..f2afc26c85 100644 --- a/core/chains/evm/gas/models_test.go +++ b/core/chains/evm/gas/models_test.go @@ -1,6 +1,7 @@ package gas_test import ( + "errors" "math/big" "testing" @@ -12,12 +13,14 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" rollupMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" ) func TestWrappedEvmEstimator(t *testing.T) { @@ -35,9 +38,9 @@ func TestWrappedEvmEstimator(t *testing.T) { est := mocks.NewEvmEstimator(t) est.On("GetDynamicFee", mock.Anything, mock.Anything). - Return(dynamicFee, nil).Twice() + Return(dynamicFee, nil).Times(6) est.On("GetLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(legacyFee, gasLimit, nil).Twice() + Return(legacyFee, gasLimit, nil).Times(6) est.On("BumpDynamicFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(dynamicFee, nil).Once() est.On("BumpLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). @@ -49,6 +52,9 @@ func TestWrappedEvmEstimator(t *testing.T) { mockEstimatorName := "WrappedEvmEstimator" mockEvmEstimatorName := "WrappedEvmEstimator.MockEstimator" + fromAddress := testutils.NewAddress() + toAddress := testutils.NewAddress() + // L1Oracle returns the correct L1Oracle interface t.Run("L1Oracle", func(t *testing.T) { lggr := logger.Test(t) @@ -59,7 +65,7 @@ func TestWrappedEvmEstimator(t *testing.T) { getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } // expect nil - estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, nil) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, nil, nil) l1Oracle := estimator.L1Oracle() assert.Nil(t, l1Oracle) @@ -68,7 +74,7 @@ func TestWrappedEvmEstimator(t *testing.T) { oracle, err := rollups.NewL1GasOracle(lggr, nil, chaintype.ChainOptimismBedrock) require.NoError(t, err) // cast oracle to L1Oracle interface - estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) evmEstimator.On("L1Oracle").Return(oracle).Once() l1Oracle = estimator.L1Oracle() @@ -80,8 +86,8 @@ func TestWrappedEvmEstimator(t *testing.T) { lggr := logger.Test(t) // expect legacy fee data dynamicFees := false - estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) - fee, max, err := estimator.GetFee(ctx, nil, 0, nil) + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) + fee, max, err := estimator.GetFee(ctx, nil, 0, nil, nil, nil) require.NoError(t, err) assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), max) assert.True(t, legacyFee.Equal(fee.Legacy)) @@ -90,8 +96,8 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect dynamic fee data dynamicFees = true - estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) - fee, max, err = estimator.GetFee(ctx, nil, gasLimit, nil) + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) + fee, max, err = estimator.GetFee(ctx, nil, gasLimit, nil, nil, nil) require.NoError(t, err) assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), max) assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) @@ -103,7 +109,7 @@ func TestWrappedEvmEstimator(t *testing.T) { t.Run("BumpFee", func(t *testing.T) { lggr := logger.Test(t) dynamicFees := false - estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) // expect legacy fee data fee, max, err := estimator.BumpFee(ctx, gas.EvmFee{Legacy: assets.NewWeiI(0)}, 0, nil, nil) @@ -141,8 +147,8 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect legacy fee data dynamicFees := false - estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) - total, err := estimator.GetMaxCost(ctx, val, nil, gasLimit, nil) + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) + total, err := estimator.GetMaxCost(ctx, val, nil, gasLimit, nil, nil, nil) require.NoError(t, err) fee := new(big.Int).Mul(legacyFee.ToInt(), big.NewInt(int64(gasLimit))) fee, _ = new(big.Float).Mul(new(big.Float).SetInt(fee), big.NewFloat(float64(limitMultiplier))).Int(nil) @@ -150,8 +156,8 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect dynamic fee data dynamicFees = true - estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) - total, err = estimator.GetMaxCost(ctx, val, nil, gasLimit, nil) + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) + total, err = estimator.GetMaxCost(ctx, val, nil, gasLimit, nil, nil, nil) require.NoError(t, err) fee = new(big.Int).Mul(dynamicFee.FeeCap.ToInt(), big.NewInt(int64(gasLimit))) fee, _ = new(big.Float).Mul(new(big.Float).SetInt(fee), big.NewFloat(float64(limitMultiplier))).Int(nil) @@ -166,7 +172,7 @@ func TestWrappedEvmEstimator(t *testing.T) { estimator := gas.NewEvmFeeEstimator(lggr, func(logger.Logger) gas.EvmEstimator { return evmEstimator - }, false, geCfg) + }, false, geCfg, nil) require.Equal(t, mockEstimatorName, estimator.Name()) require.Equal(t, mockEvmEstimatorName, evmEstimator.Name()) @@ -185,7 +191,7 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator.On("L1Oracle", mock.Anything).Return(nil).Twice() - estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) err := estimator.Start(ctx) require.NoError(t, err) err = estimator.Close() @@ -193,7 +199,7 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator.On("L1Oracle", mock.Anything).Return(oracle).Twice() - estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) err = estimator.Start(ctx) require.NoError(t, err) err = estimator.Close() @@ -210,11 +216,11 @@ func TestWrappedEvmEstimator(t *testing.T) { oracle.On("Ready").Return(nil).Twice() getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } - estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) err := estimator.Ready() require.NoError(t, err) - estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) err = estimator.Ready() require.NoError(t, err) }) @@ -235,7 +241,7 @@ func TestWrappedEvmEstimator(t *testing.T) { oracle.On("HealthReport").Return(map[string]error{oracleKey: oracleError}).Once() getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } - estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) report := estimator.HealthReport() require.True(t, pkgerrors.Is(report[evmEstimatorKey], evmEstimatorError)) require.Nil(t, report[oracleKey]) @@ -243,10 +249,160 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator.On("L1Oracle").Return(oracle).Once() - estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) report = estimator.HealthReport() require.True(t, pkgerrors.Is(report[evmEstimatorKey], evmEstimatorError)) require.True(t, pkgerrors.Is(report[oracleKey], oracleError)) require.NotNil(t, report[mockEstimatorName]) }) + + t.Run("GetFee, estimate gas limit enabled, succeeds", func(t *testing.T) { + estimatedGasLimit := uint64(5) + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err := estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), limit) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.DynamicTipCap) + assert.Nil(t, fee.DynamicFeeCap) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err = estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), limit) + assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.DynamicTipCap)) + assert.Nil(t, fee.Legacy) + }) + + t.Run("GetFee, estimate gas limit enabled, estimate exceeds provided limit, returns error", func(t *testing.T) { + estimatedGasLimit := uint64(100) + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + _, _, err := estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.ErrorIs(t, err, commonfee.ErrFeeLimitTooLow) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + _, _, err = estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.ErrorIs(t, err, commonfee.ErrFeeLimitTooLow) + }) + + t.Run("GetFee, estimate gas limit enabled, buffer exceeds provided limit, fallsback to provided limit", func(t *testing.T) { + estimatedGasLimit := uint64(15) // same as provided limit + lggr := logger.Test(t) + dynamicFees := false // expect legacy fee data + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err := estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), limit) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.DynamicTipCap) + assert.Nil(t, fee.DynamicFeeCap) + + dynamicFees = true // expect dynamic fee data + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err = estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), limit) + assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.DynamicTipCap)) + assert.Nil(t, fee.Legacy) + }) + + t.Run("GetFee, estimate gas limit enabled, RPC fails and fallsback to provided gas limit", func(t *testing.T) { + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(0), errors.New("something broke")).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err := estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), limit) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.DynamicTipCap) + assert.Nil(t, fee.DynamicFeeCap) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err = estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), limit) + assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.DynamicTipCap)) + assert.Nil(t, fee.Legacy) + }) + + t.Run("GetFee, estimate gas limit enabled, provided fee limit 0, returns uncapped estimation", func(t *testing.T) { + est.On("GetDynamicFee", mock.Anything, mock.Anything). + Return(dynamicFee, nil).Once() + est.On("GetLegacyGas", mock.Anything, mock.Anything, uint64(0), mock.Anything). + Return(legacyFee, uint64(0), nil).Once() + estimatedGasLimit := uint64(100) // same as provided limit + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err := estimator.GetFee(ctx, []byte{}, uint64(0), nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), limit) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.DynamicTipCap) + assert.Nil(t, fee.DynamicFeeCap) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err = estimator.GetFee(ctx, []byte{}, 0, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), limit) + assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.DynamicTipCap)) + assert.Nil(t, fee.Legacy) + }) + + t.Run("GetFee, estimate gas limit enabled, provided fee limit 0, returns error on failure", func(t *testing.T) { + est.On("GetDynamicFee", mock.Anything, mock.Anything). + Return(dynamicFee, nil).Once() + est.On("GetLegacyGas", mock.Anything, mock.Anything, uint64(0), mock.Anything). + Return(legacyFee, uint64(0), nil).Once() + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(0), errors.New("something broke")).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + _, _, err := estimator.GetFee(ctx, []byte{}, 0, nil, &fromAddress, &toAddress) + require.Error(t, err) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + _, _, err = estimator.GetFee(ctx, []byte{}, 0, nil, &fromAddress, &toAddress) + require.Error(t, err) + }) } diff --git a/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go b/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go index c01244db70..d758dc711e 100644 --- a/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go +++ b/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go @@ -112,6 +112,10 @@ func (o *arbitrumL1Oracle) Name() string { return o.logger.Name() } +func (o *arbitrumL1Oracle) ChainType(_ context.Context) chaintype.ChainType { + return o.chainType +} + func (o *arbitrumL1Oracle) Start(ctx context.Context) error { return o.StartOnce(o.Name(), func() error { go o.run() diff --git a/core/chains/evm/gas/rollups/l1_oracle.go b/core/chains/evm/gas/rollups/l1_oracle.go index f707fab684..4195175598 100644 --- a/core/chains/evm/gas/rollups/l1_oracle.go +++ b/core/chains/evm/gas/rollups/l1_oracle.go @@ -26,6 +26,7 @@ type L1Oracle interface { GasPrice(ctx context.Context) (*assets.Wei, error) GetGasCost(ctx context.Context, tx *types.Transaction, blockNum *big.Int) (*assets.Wei, error) + ChainType(ctx context.Context) chaintype.ChainType } type l1OracleClient interface { @@ -43,7 +44,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) @@ -56,7 +57,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) diff --git a/core/chains/evm/gas/rollups/mocks/l1_oracle.go b/core/chains/evm/gas/rollups/mocks/l1_oracle.go index e82cb4ee90..25bb3a2db5 100644 --- a/core/chains/evm/gas/rollups/mocks/l1_oracle.go +++ b/core/chains/evm/gas/rollups/mocks/l1_oracle.go @@ -7,6 +7,8 @@ import ( assets "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + chaintype "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" + context "context" mock "github.com/stretchr/testify/mock" @@ -27,6 +29,52 @@ func (_m *L1Oracle) EXPECT() *L1Oracle_Expecter { return &L1Oracle_Expecter{mock: &_m.Mock} } +// ChainType provides a mock function with given fields: ctx +func (_m *L1Oracle) ChainType(ctx context.Context) chaintype.ChainType { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ChainType") + } + + var r0 chaintype.ChainType + if rf, ok := ret.Get(0).(func(context.Context) chaintype.ChainType); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(chaintype.ChainType) + } + + return r0 +} + +// L1Oracle_ChainType_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChainType' +type L1Oracle_ChainType_Call struct { + *mock.Call +} + +// ChainType is a helper method to define mock.On call +// - ctx context.Context +func (_e *L1Oracle_Expecter) ChainType(ctx interface{}) *L1Oracle_ChainType_Call { + return &L1Oracle_ChainType_Call{Call: _e.mock.On("ChainType", ctx)} +} + +func (_c *L1Oracle_ChainType_Call) Run(run func(ctx context.Context)) *L1Oracle_ChainType_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *L1Oracle_ChainType_Call) Return(_a0 chaintype.ChainType) *L1Oracle_ChainType_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *L1Oracle_ChainType_Call) RunAndReturn(run func(context.Context) chaintype.ChainType) *L1Oracle_ChainType_Call { + _c.Call.Return(run) + return _c +} + // Close provides a mock function with given fields: func (_m *L1Oracle) Close() error { ret := _m.Called() diff --git a/core/chains/evm/gas/rollups/op_l1_oracle.go b/core/chains/evm/gas/rollups/op_l1_oracle.go index 6805cd7095..11babc5ca5 100644 --- a/core/chains/evm/gas/rollups/op_l1_oracle.go +++ b/core/chains/evm/gas/rollups/op_l1_oracle.go @@ -50,6 +50,7 @@ type optimismL1Oracle struct { blobBaseFeeCalldata []byte blobBaseFeeScalarCalldata []byte decimalsCalldata []byte + tokenRatioCalldata []byte isEcotoneCalldata []byte isEcotoneMethodAbi abi.ABI isFjordCalldata []byte @@ -87,7 +88,7 @@ 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. + // 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" @@ -98,7 +99,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 @@ -219,6 +220,10 @@ func (o *optimismL1Oracle) Name() string { return o.logger.Name() } +func (o *optimismL1Oracle) ChainType(_ context.Context) chaintype.ChainType { + return o.chainType +} + func (o *optimismL1Oracle) Start(ctx context.Context) error { return o.StartOnce(o.Name(), func() error { go o.run() diff --git a/core/chains/evm/gas/rollups/zkSync_l1_oracle.go b/core/chains/evm/gas/rollups/zkSync_l1_oracle.go index 31d93bc587..c294123354 100644 --- a/core/chains/evm/gas/rollups/zkSync_l1_oracle.go +++ b/core/chains/evm/gas/rollups/zkSync_l1_oracle.go @@ -83,6 +83,10 @@ func (o *zkSyncL1Oracle) Name() string { return o.logger.Name() } +func (o *zkSyncL1Oracle) ChainType(_ context.Context) chaintype.ChainType { + return o.chainType +} + func (o *zkSyncL1Oracle) Start(ctx context.Context) error { return o.StartOnce(o.Name(), func() error { go o.run() diff --git a/core/chains/evm/txmgr/attempts.go b/core/chains/evm/txmgr/attempts.go index 8566adcb5c..c284ee77bd 100644 --- a/core/chains/evm/txmgr/attempts.go +++ b/core/chains/evm/txmgr/attempts.go @@ -58,7 +58,7 @@ func (c *evmTxAttemptBuilder) NewTxAttempt(ctx context.Context, etx Tx, lggr log // used for L2 re-estimation on broadcasting (note EIP1559 must be disabled otherwise this will fail with mismatched fees + tx type) func (c *evmTxAttemptBuilder) NewTxAttemptWithType(ctx context.Context, etx Tx, lggr logger.Logger, txType int, opts ...feetypes.Opt) (attempt TxAttempt, fee gas.EvmFee, feeLimit uint64, retryable bool, err error) { keySpecificMaxGasPriceWei := c.feeConfig.PriceMaxKey(etx.FromAddress) - fee, feeLimit, err = c.EvmFeeEstimator.GetFee(ctx, etx.EncodedPayload, etx.FeeLimit, keySpecificMaxGasPriceWei, opts...) + fee, feeLimit, err = c.EvmFeeEstimator.GetFee(ctx, etx.EncodedPayload, etx.FeeLimit, keySpecificMaxGasPriceWei, &etx.FromAddress, &etx.ToAddress, opts...) if err != nil { return attempt, fee, feeLimit, true, pkgerrors.Wrap(err, "failed to get fee") // estimator errors are retryable } @@ -71,8 +71,8 @@ func (c *evmTxAttemptBuilder) NewTxAttemptWithType(ctx context.Context, etx Tx, // used in the txm broadcaster + confirmer when tx ix rejected for too low fee or is not included in a timely manner func (c *evmTxAttemptBuilder) NewBumpTxAttempt(ctx context.Context, etx Tx, previousAttempt TxAttempt, priorAttempts []TxAttempt, lggr logger.Logger) (attempt TxAttempt, bumpedFee gas.EvmFee, bumpedFeeLimit uint64, retryable bool, err error) { keySpecificMaxGasPriceWei := c.feeConfig.PriceMaxKey(etx.FromAddress) - - bumpedFee, bumpedFeeLimit, err = c.EvmFeeEstimator.BumpFee(ctx, previousAttempt.TxFee, etx.FeeLimit, keySpecificMaxGasPriceWei, newEvmPriorAttempts(priorAttempts)) + // Use the fee limit from the previous attempt to maintain limits adjusted for 2D fees or by estimation + bumpedFee, bumpedFeeLimit, err = c.EvmFeeEstimator.BumpFee(ctx, previousAttempt.TxFee, previousAttempt.ChainSpecificFeeLimit, keySpecificMaxGasPriceWei, newEvmPriorAttempts(priorAttempts)) if err != nil { return attempt, bumpedFee, bumpedFeeLimit, true, pkgerrors.Wrap(err, "failed to bump fee") // estimator errors are retryable } diff --git a/core/chains/evm/txmgr/attempts_test.go b/core/chains/evm/txmgr/attempts_test.go index 6be8cd7067..5c43368fcc 100644 --- a/core/chains/evm/txmgr/attempts_test.go +++ b/core/chains/evm/txmgr/attempts_test.go @@ -339,7 +339,7 @@ func TestTxm_NewCustomTxAttempt_NonRetryableErrors(t *testing.T) { func TestTxm_EvmTxAttemptBuilder_RetryableEstimatorError(t *testing.T) { est := gasmocks.NewEvmFeeEstimator(t) - est.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{}, uint64(0), pkgerrors.New("fail")) + est.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{}, uint64(0), pkgerrors.New("fail")) est.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{}, uint64(0), pkgerrors.New("fail")) kst := ksmocks.NewEth(t) diff --git a/core/chains/evm/txmgr/broadcaster_test.go b/core/chains/evm/txmgr/broadcaster_test.go index cbbe522ba1..514d533159 100644 --- a/core/chains/evm/txmgr/broadcaster_test.go +++ b/core/chains/evm/txmgr/broadcaster_test.go @@ -29,8 +29,10 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" commonclient "github.com/smartcontractkit/chainlink/v2/common/client" + commmonfee "github.com/smartcontractkit/chainlink/v2/common/fee" txmgrcommon "github.com/smartcontractkit/chainlink/v2/common/txmgr" txmgrtypes "github.com/smartcontractkit/chainlink/v2/common/txmgr/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" @@ -69,7 +71,7 @@ func NewTestEthBroadcaster( estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(config.EVM().GasEstimator(), nil, ge.BlockHistory(), lggr, nil) - }, ge.EIP1559DynamicFees(), ge) + }, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, keyStore, estimator) ethBroadcaster := txmgrcommon.NewBroadcaster(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(config.EVM().GasEstimator()), config.EVM().Transactions(), gconfig.Database().Listener(), keyStore, txBuilder, nonceTracker, lggr, checkerFactory, nonceAutoSync, "") @@ -642,7 +644,7 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_OptimisticLockingOnEthTx(t *testi chStartEstimate := make(chan struct{}) chBlock := make(chan struct{}) - estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, ccfg.EVM().GasEstimator().PriceMaxKey(fromAddress)).Return(gas.EvmFee{Legacy: assets.GWei(32)}, uint64(500), nil).Run(func(_ mock.Arguments) { + estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, ccfg.EVM().GasEstimator().PriceMaxKey(fromAddress), mock.Anything, mock.Anything).Return(gas.EvmFee{Legacy: assets.GWei(32)}, uint64(500), nil).Run(func(_ mock.Arguments) { close(chStartEstimate) <-chBlock }).Once() @@ -1177,7 +1179,7 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_Errors(t *testing.T) { t.Run("callback set by ctor", func(t *testing.T) { estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), nil, evmcfg.EVM().GasEstimator().BlockHistory(), lggr, nil) - }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator()) + }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator(), ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), evmcfg.EVM().GasEstimator(), ethKeyStore, estimator) localNextNonce = getLocalNextNonce(t, nonceTracker, fromAddress) eb2 := txmgr.NewEvmBroadcaster(txStore, txmClient, txmgr.NewEvmTxmConfig(evmcfg.EVM()), txmgr.NewEvmTxmFeeConfig(evmcfg.EVM().GasEstimator()), evmcfg.EVM().Transactions(), cfg.Database().Listener(), ethKeyStore, txBuilder, lggr, &testCheckerFactory{}, false, "") @@ -1666,6 +1668,73 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_Errors(t *testing.T) { }) } +func TestEthBroadcaster_ProcessUnstartedEthTxs_GasEstimationError(t *testing.T) { + toAddress := testutils.NewAddress() + value := big.Int(assets.NewEthValue(142)) + gasLimit := uint64(242) + encodedPayload := []byte{0, 1} + + db := pgtest.NewSqlxDB(t) + cfg := configtest.NewTestGeneralConfig(t) + cfg.EVMConfigs()[0].GasEstimator.EstimateLimit = ptr(true) // Enabled gas limit estimation + limitMultiplier := float32(1.25) + cfg.EVMConfigs()[0].GasEstimator.LimitMultiplier = ptr(decimal.NewFromFloat32(limitMultiplier)) // Set LimitMultiplier for the buffer + txStore := cltest.NewTestTxStore(t, db) + + ethKeyStore := cltest.NewKeyStore(t, db).Eth() + _, fromAddress := cltest.MustInsertRandomKey(t, ethKeyStore) + + config := evmtest.NewChainScopedConfig(t, cfg) + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("PendingNonceAt", mock.Anything, fromAddress).Return(uint64(0), nil).Once() + lggr := logger.Test(t) + txmClient := txmgr.NewEvmTxmClient(ethClient, nil) + nonceTracker := txmgr.NewNonceTracker(lggr, txStore, txmClient) + ge := config.EVM().GasEstimator() + estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { + return gas.NewFixedPriceEstimator(ge, nil, ge.BlockHistory(), lggr, nil) + }, ge.EIP1559DynamicFees(), ge, ethClient) + txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ethKeyStore, estimator) + eb := txmgrcommon.NewBroadcaster(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(config.EVM().GasEstimator()), config.EVM().Transactions(), cfg.Database().Listener(), ethKeyStore, txBuilder, nonceTracker, lggr, &testCheckerFactory{}, false, "") + + // Mark instance as test + eb.XXXTestDisableUnstartedTxAutoProcessing() + servicetest.Run(t, eb) + ctx := tests.Context(t) + t.Run("gas limit lowered after estimation", func(t *testing.T) { + estimatedGasLimit := uint64(100) + etx := mustCreateUnstartedTx(t, txStore, fromAddress, toAddress, encodedPayload, gasLimit, value, testutils.FixtureChainID) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Once() + ethClient.On("SendTransactionReturnCode", mock.Anything, mock.MatchedBy(func(tx *gethTypes.Transaction) bool { + return tx.Nonce() == uint64(0) + }), fromAddress).Return(commonclient.Successful, nil).Once() + + // Do the thing + retryable, err := eb.ProcessUnstartedTxs(ctx, fromAddress) + assert.NoError(t, err) + assert.False(t, retryable) + + dbEtx, err := txStore.FindTxWithAttempts(ctx, etx.ID) + require.NoError(t, err) + attempt := dbEtx.TxAttempts[0] + require.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), attempt.ChainSpecificFeeLimit) + }) + t.Run("provided gas limit too low, transaction marked as fatal error", func(t *testing.T) { + etx := mustCreateUnstartedTx(t, txStore, fromAddress, toAddress, encodedPayload, gasLimit, value, testutils.FixtureChainID) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(float32(gasLimit)*limitMultiplier)+1, nil).Once() + + // Do the thing + retryable, err := eb.ProcessUnstartedTxs(ctx, fromAddress) + assert.NoError(t, err) + assert.False(t, retryable) + + dbEtx, err := txStore.FindTxWithAttempts(ctx, etx.ID) + require.NoError(t, err) + require.Equal(t, txmgrcommon.TxFatalError, dbEtx.State) + require.Equal(t, commmonfee.ErrFeeLimitTooLow.Error(), dbEtx.Error.String) + }) +} + func TestEthBroadcaster_ProcessUnstartedEthTxs_KeystoreErrors(t *testing.T) { toAddress := gethCommon.HexToAddress("0x6C03DDA95a2AEd917EeCc6eddD4b9D16E6380411") value := big.Int(assets.NewEthValue(142)) @@ -1760,15 +1829,15 @@ func TestEthBroadcaster_SyncNonce(t *testing.T) { kst := cltest.NewKeyStore(t, db).Eth() _, fromAddress := cltest.RandomKey{Disabled: false}.MustInsertWithState(t, kst) + ethClient := testutils.NewEthClientMockWithDefaultChain(t) estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), nil, evmcfg.EVM().GasEstimator().BlockHistory(), lggr, nil) - }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator()) + }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator(), ethClient) checkerFactory := &testCheckerFactory{} ge := evmcfg.EVM().GasEstimator() t.Run("does nothing if nonce sync is disabled", func(t *testing.T) { - ethClient := testutils.NewEthClientMockWithDefaultChain(t) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, estimator) kst := ksmocks.NewEth(t) @@ -1838,7 +1907,7 @@ func TestEthBroadcaster_HederaBroadcastValidation(t *testing.T) { ge := evmcfg.EVM().GasEstimator() estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), nil, ge.BlockHistory(), lggr, nil) - }, ge.EIP1559DynamicFees(), ge) + }, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ethKeyStore, estimator) checkerFactory := &txmgr.CheckerFactory{Client: ethClient} ctx := tests.Context(t) diff --git a/core/chains/evm/txmgr/confirmer_test.go b/core/chains/evm/txmgr/confirmer_test.go index 45d7ac9e12..82b668f168 100644 --- a/core/chains/evm/txmgr/confirmer_test.go +++ b/core/chains/evm/txmgr/confirmer_test.go @@ -128,7 +128,7 @@ func TestEthConfirmer_Lifecycle(t *testing.T) { newEst := func(logger.Logger) gas.EvmEstimator { return estimator } lggr := logger.Test(t) ge := config.EVM().GasEstimator() - feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) + feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ethKeyStore, feeEstimator) stuckTxDetector := txmgr.NewStuckTxDetector(lggr, testutils.FixtureChainID, "", assets.NewWei(assets.NewEth(100).ToInt()), config.EVM().Transactions().AutoPurge(), feeEstimator, txStore, ethClient) ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(ge), config.EVM().Transactions(), gconfig.Database(), ethKeyStore, txBuilder, lggr, stuckTxDetector) @@ -1646,7 +1646,7 @@ func TestEthConfirmer_RebroadcastWhereNecessary_WithConnectivityCheck(t *testing newEst := func(logger.Logger) gas.EvmEstimator { return estimator } estimator.On("BumpLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, uint64(0), pkgerrors.Wrapf(commonfee.ErrConnectivity, "transaction...")) ge := ccfg.EVM().GasEstimator() - feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) + feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, feeEstimator) addresses := []gethCommon.Address{fromAddress} kst.On("EnabledAddressesForChain", mock.Anything, &cltest.FixtureChainID).Return(addresses, nil).Maybe() @@ -1695,7 +1695,7 @@ func TestEthConfirmer_RebroadcastWhereNecessary_WithConnectivityCheck(t *testing newEst := func(logger.Logger) gas.EvmEstimator { return estimator } // Create confirmer with necessary state ge := ccfg.EVM().GasEstimator() - feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) + feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, feeEstimator) addresses := []gethCommon.Address{fromAddress} kst.On("EnabledAddressesForChain", mock.Anything, &cltest.FixtureChainID).Return(addresses, nil).Maybe() @@ -3195,7 +3195,7 @@ func TestEthConfirmer_ProcessStuckTransactions(t *testing.T) { fee := gas.EvmFee{Legacy: marketGasPrice} bumpedLegacy := assets.GWei(30) bumpedFee := gas.EvmFee{Legacy: bumpedLegacy} - feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything).Return(fee, uint64(0), nil) + feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything, mock.Anything, mock.Anything).Return(fee, uint64(0), nil) feeEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(bumpedFee, uint64(10_000), nil) autoPurgeThreshold := uint32(5) autoPurgeMinAttempts := uint32(3) @@ -3286,7 +3286,7 @@ func newEthConfirmer(t testing.TB, txStore txmgr.EvmTxStore, ethClient client.Cl ge := config.EVM().GasEstimator() estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(ge, nil, ge.BlockHistory(), lggr, nil) - }, ge.EIP1559DynamicFees(), ge) + }, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ks, estimator) stuckTxDetector := txmgr.NewStuckTxDetector(lggr, testutils.FixtureChainID, "", assets.NewWei(assets.NewEth(100).ToInt()), config.EVM().Transactions().AutoPurge(), estimator, txStore, ethClient) ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(ge), config.EVM().Transactions(), gconfig.Database(), ks, txBuilder, lggr, stuckTxDetector) diff --git a/core/chains/evm/txmgr/stuck_tx_detector.go b/core/chains/evm/txmgr/stuck_tx_detector.go index 5d621dc0b2..362bb6c0a5 100644 --- a/core/chains/evm/txmgr/stuck_tx_detector.go +++ b/core/chains/evm/txmgr/stuck_tx_detector.go @@ -25,7 +25,7 @@ import ( ) type stuckTxDetectorGasEstimator interface { - GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee gas.EvmFee, chainSpecificFeeLimit uint64, err error) + GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (fee gas.EvmFee, chainSpecificFeeLimit uint64, err error) } type stuckTxDetectorClient interface { @@ -199,7 +199,7 @@ func (d *stuckTxDetector) detectStuckTransactionsHeuristic(ctx context.Context, defer d.purgeBlockNumLock.RUnlock() // Get gas price from internal gas estimator // Send with max gas price time 2 to prevent the results from being capped. Need the market gas price here. - marketGasPrice, _, err := d.gasEstimator.GetFee(ctx, []byte{}, 0, d.maxPrice.Mul(big.NewInt(2))) + marketGasPrice, _, err := d.gasEstimator.GetFee(ctx, []byte{}, 0, d.maxPrice.Mul(big.NewInt(2)), nil, nil) if err != nil { return txs, fmt.Errorf("failed to get market gas price for overflow detection: %w", err) } diff --git a/core/chains/evm/txmgr/stuck_tx_detector_test.go b/core/chains/evm/txmgr/stuck_tx_detector_test.go index def49f8e11..eb22830ef3 100644 --- a/core/chains/evm/txmgr/stuck_tx_detector_test.go +++ b/core/chains/evm/txmgr/stuck_tx_detector_test.go @@ -73,7 +73,7 @@ func TestStuckTxDetector_LoadPurgeBlockNumMap(t *testing.T) { feeEstimator := gasmocks.NewEvmFeeEstimator(t) marketGasPrice := assets.GWei(15) fee := gas.EvmFee{Legacy: marketGasPrice} - feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything).Return(fee, uint64(0), nil) + feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything, mock.Anything, mock.Anything).Return(fee, uint64(0), nil) autoPurgeThreshold := uint32(5) autoPurgeMinAttempts := uint32(3) autoPurgeCfg := testAutoPurgeConfig{ @@ -194,7 +194,7 @@ func TestStuckTxDetector_DetectStuckTransactionsHeuristic(t *testing.T) { // Return 10 gwei as market gas price marketGasPrice := tenGwei fee := gas.EvmFee{Legacy: marketGasPrice} - feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything).Return(fee, uint64(0), nil) + feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything, mock.Anything, mock.Anything).Return(fee, uint64(0), nil) ethClient := testutils.NewEthClientMockWithDefaultChain(t) autoPurgeThreshold := uint32(5) autoPurgeMinAttempts := uint32(3) diff --git a/core/chains/evm/txmgr/test_helpers.go b/core/chains/evm/txmgr/test_helpers.go index 3b3584a988..b1317cb421 100644 --- a/core/chains/evm/txmgr/test_helpers.go +++ b/core/chains/evm/txmgr/test_helpers.go @@ -73,6 +73,10 @@ func (g *TestGasEstimatorConfig) BlockHistory() evmconfig.BlockHistory { return &TestBlockHistoryConfig{} } +func (g *TestGasEstimatorConfig) FeeHistory() evmconfig.FeeHistory { + return &TestFeeHistoryConfig{} +} + func (g *TestGasEstimatorConfig) EIP1559DynamicFees() bool { return false } func (g *TestGasEstimatorConfig) LimitDefault() uint64 { return 42 } func (g *TestGasEstimatorConfig) BumpPercent() uint16 { return 42 } @@ -89,6 +93,7 @@ func (g *TestGasEstimatorConfig) LimitTransfer() uint64 { return 42 } func (g *TestGasEstimatorConfig) PriceMax() *assets.Wei { return assets.NewWeiI(42) } func (g *TestGasEstimatorConfig) PriceMin() *assets.Wei { return assets.NewWeiI(42) } func (g *TestGasEstimatorConfig) Mode() string { return "FixedPrice" } +func (g *TestGasEstimatorConfig) EstimateLimit() bool { return false } func (g *TestGasEstimatorConfig) LimitJobType() evmconfig.LimitJobType { return &TestLimitJobTypeConfig{} } @@ -120,6 +125,12 @@ func (b *TestBlockHistoryConfig) BlockHistorySize() uint16 { return 42 func (b *TestBlockHistoryConfig) EIP1559FeeCapBufferBlocks() uint16 { return 42 } func (b *TestBlockHistoryConfig) TransactionPercentile() uint16 { return 42 } +type TestFeeHistoryConfig struct { + evmconfig.FeeHistory +} + +func (b *TestFeeHistoryConfig) CacheTimeout() time.Duration { return 0 * time.Second } + type transactionsConfig struct { evmconfig.Transactions e *TestEvmConfig diff --git a/core/config/docs/chains-evm.toml b/core/config/docs/chains-evm.toml index 73ea0ebb35..bffc9ddf0c 100644 --- a/core/config/docs/chains-evm.toml +++ b/core/config/docs/chains-evm.toml @@ -194,6 +194,8 @@ LimitMax = 8_000_000 # Default LimitMultiplier = '1.0' # Default # LimitTransfer is the gas limit used for an ordinary ETH transfer. LimitTransfer = 21_000 # Default +# EstimateLimit enables estimating gas limits for transactions. This feature respects the gas limit provided during transaction creation as an upper bound. +EstimateLimit = false # Default # BumpMin is the minimum fixed amount of wei by which gas is bumped on each transaction attempt. BumpMin = '5 gwei' # Default # BumpPercent is the percentage by which to bump gas on a transaction that has exceeded `BumpThreshold`. The larger of `BumpPercent` and `BumpMin` is taken for gas bumps. @@ -310,6 +312,15 @@ EIP1559FeeCapBufferBlocks = 13 # Example # Setting it lower will tend to set lower gas prices. TransactionPercentile = 60 # Default +[EVM.GasEstimator.FeeHistory] +# CacheTimeout is the time to wait in order to refresh the cached values stored in the FeeHistory estimator. A small jitter is applied so the timeout won't be exactly the same each time. +# +# You want this value to be close to the block time. For slower chains, like Ethereum, you can set it to 12s, the same as the block time. For faster chains you can skip a block or two +# and set it to two times the block time i.e. on Optimism you can set it to 4s. Ideally, you don't want to go lower than 1s since the RTT times of the RPC requests will be comparable to +# the timeout. The estimator is already adding a buffer to account for a potential increase in prices within one or two blocks. On the other hand, slower frequency will fail to refresh +# the prices and end up in stale values. +CacheTimeout = '10s' # Default + # The head tracker continually listens for new heads from the chain. # # In addition to these settings, it log warnings if `EVM.NoNewHeadsThreshold` is exceeded without any new blocks being emitted. diff --git a/core/internal/features/features_test.go b/core/internal/features/features_test.go index 1259597379..7be0307798 100644 --- a/core/internal/features/features_test.go +++ b/core/internal/features/features_test.go @@ -1339,7 +1339,7 @@ func TestIntegration_BlockHistoryEstimator(t *testing.T) { chain := evmtest.MustGetDefaultChain(t, legacyChains) estimator := chain.GasEstimator() - gasPrice, gasLimit, err := estimator.GetFee(testutils.Context(t), nil, 500_000, maxGasPrice) + gasPrice, gasLimit, err := estimator.GetFee(testutils.Context(t), nil, 500_000, maxGasPrice, nil, nil) require.NoError(t, err) assert.Equal(t, uint64(500000), gasLimit) assert.Equal(t, "41.5 gwei", gasPrice.Legacy.String()) @@ -1360,7 +1360,7 @@ func TestIntegration_BlockHistoryEstimator(t *testing.T) { newHeads.TrySend(h43) gomega.NewWithT(t).Eventually(func() string { - gasPrice, _, err := estimator.GetFee(testutils.Context(t), nil, 500000, maxGasPrice) + gasPrice, _, err := estimator.GetFee(testutils.Context(t), nil, 500000, maxGasPrice, nil, nil) require.NoError(t, err) return gasPrice.Legacy.String() }, testutils.WaitTimeout(t), cltest.DBPollingInterval).Should(gomega.Equal("45 gwei")) diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index e9ed223b9b..9afc0aa942 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -520,6 +520,7 @@ func TestConfig_Marshal(t *testing.T) { LimitMax: ptr[uint64](17), LimitMultiplier: mustDecimal("1.234"), LimitTransfer: ptr[uint64](100), + EstimateLimit: ptr(false), TipCapDefault: assets.NewWeiI(2), TipCapMin: assets.NewWeiI(1), PriceDefault: assets.NewWeiI(math.MaxInt64), @@ -543,6 +544,9 @@ func TestConfig_Marshal(t *testing.T) { EIP1559FeeCapBufferBlocks: ptr[uint16](13), TransactionPercentile: ptr[uint16](15), }, + FeeHistory: evmcfg.FeeHistoryEstimator{ + CacheTimeout: &second, + }, }, KeySpecific: []evmcfg.KeySpecific{ @@ -1024,6 +1028,7 @@ LimitDefault = 12 LimitMax = 17 LimitMultiplier = '1.234' LimitTransfer = 100 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 10 BumpThreshold = 6 @@ -1049,6 +1054,9 @@ CheckInclusionPercentile = 19 EIP1559FeeCapBufferBlocks = 13 TransactionPercentile = 15 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '1s' + [EVM.HeadTracker] HistoryDepth = 15 MaxBufferSize = 17 @@ -1304,7 +1312,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 @@ -1317,7 +1325,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: diff --git a/core/services/chainlink/testdata/config-full.toml b/core/services/chainlink/testdata/config-full.toml index 9df2e70508..c10d59f339 100644 --- a/core/services/chainlink/testdata/config-full.toml +++ b/core/services/chainlink/testdata/config-full.toml @@ -316,6 +316,7 @@ LimitDefault = 12 LimitMax = 17 LimitMultiplier = '1.234' LimitTransfer = 100 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 10 BumpThreshold = 6 @@ -341,6 +342,9 @@ CheckInclusionPercentile = 19 EIP1559FeeCapBufferBlocks = 13 TransactionPercentile = 15 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '1s' + [EVM.HeadTracker] HistoryDepth = 15 MaxBufferSize = 17 diff --git a/core/services/chainlink/testdata/config-multi-chain-effective.toml b/core/services/chainlink/testdata/config-multi-chain-effective.toml index e560099c2c..edb8d0a249 100644 --- a/core/services/chainlink/testdata/config-multi-chain-effective.toml +++ b/core/services/chainlink/testdata/config-multi-chain-effective.toml @@ -303,6 +303,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -318,6 +319,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -403,6 +407,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -418,6 +423,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -497,6 +505,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -512,6 +521,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 diff --git a/core/services/keeper/upkeep_executer_test.go b/core/services/keeper/upkeep_executer_test.go index c70a92c725..55926242a2 100644 --- a/core/services/keeper/upkeep_executer_test.go +++ b/core/services/keeper/upkeep_executer_test.go @@ -48,7 +48,7 @@ func mockEstimator(t *testing.T) gas.EvmFeeEstimator { // note: estimator will only return 1 of legacy or dynamic fees (not both) // assumed to call legacy estimator only estimator := gasmocks.NewEvmFeeEstimator(t) - estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(gas.EvmFee{ + estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(gas.EvmFee{ Legacy: assets.GWei(60), }, uint32(60), nil) return estimator diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/commit_store_reader_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/commit_store_reader_test.go index 6b075be50c..e9266c4fb2 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/commit_store_reader_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/commit_store_reader_test.go @@ -286,7 +286,7 @@ func TestCommitStoreReaders(t *testing.T) { } gasPrice := big.NewInt(10) daPrice := big.NewInt(20) - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, assets.NewWei(maxGasPrice)).Return(gas.EvmFee{Legacy: assets.NewWei(gasPrice)}, uint64(0), nil) + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, assets.NewWei(maxGasPrice), (*common.Address)(nil), (*common.Address)(nil)).Return(gas.EvmFee{Legacy: assets.NewWei(gasPrice)}, uint64(0), nil) lm.On("GasPrice", mock.Anything).Return(assets.NewWei(daPrice), nil) for v, cr := range crs { diff --git a/core/services/ocr2/plugins/ccip/prices/exec_price_estimator.go b/core/services/ocr2/plugins/ccip/prices/exec_price_estimator.go index 56e1ddb583..84a6014bef 100644 --- a/core/services/ocr2/plugins/ccip/prices/exec_price_estimator.go +++ b/core/services/ocr2/plugins/ccip/prices/exec_price_estimator.go @@ -26,7 +26,7 @@ func NewExecGasPriceEstimator(estimator gas.EvmFeeEstimator, maxGasPrice *big.In } func (g ExecGasPriceEstimator) GetGasPrice(ctx context.Context) (*big.Int, error) { - gasPriceWei, _, err := g.estimator.GetFee(ctx, nil, 0, assets.NewWei(g.maxGasPrice)) + gasPriceWei, _, err := g.estimator.GetFee(ctx, nil, 0, assets.NewWei(g.maxGasPrice), nil, nil) if err != nil { return nil, err } diff --git a/core/services/ocr2/plugins/ccip/prices/exec_price_estimator_test.go b/core/services/ocr2/plugins/ccip/prices/exec_price_estimator_test.go index e1c2fa0398..f9ba1523e5 100644 --- a/core/services/ocr2/plugins/ccip/prices/exec_price_estimator_test.go +++ b/core/services/ocr2/plugins/ccip/prices/exec_price_estimator_test.go @@ -5,6 +5,7 @@ import ( "math/big" "testing" + "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -85,7 +86,7 @@ func TestExecPriceEstimator_GetGasPrice(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { sourceFeeEstimator := mocks.NewEvmFeeEstimator(t) - sourceFeeEstimator.On("GetFee", ctx, []byte(nil), uint64(0), assets.NewWei(tc.maxGasPrice)).Return( + sourceFeeEstimator.On("GetFee", ctx, []byte(nil), uint64(0), assets.NewWei(tc.maxGasPrice), (*common.Address)(nil), (*common.Address)(nil)).Return( tc.sourceFeeEstimatorRespFee, uint64(0), tc.sourceFeeEstimatorRespErr) g := ExecGasPriceEstimator{ diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice.go index f84a48c1ff..36460683d4 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice.go @@ -45,7 +45,7 @@ func CheckGasPrice(ctx context.Context, upkeepId *big.Int, offchainConfigBytes [ } lggr.Debugf("successfully decode offchain config for %s, max gas price is %s", upkeepId.String(), offchainConfig.MaxGasPrice.String()) - fee, _, err := ge.GetFee(ctx, []byte{}, feeLimit, assets.NewWei(big.NewInt(maxFeePrice))) + fee, _, err := ge.GetFee(ctx, []byte{}, feeLimit, assets.NewWei(big.NewInt(maxFeePrice)), nil, nil) if err != nil { lggr.Errorw("failed to get fee, gas price check is disabled", "upkeepId", upkeepId.String(), "err", err) return encoding.UpkeepFailureReasonNone diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice_test.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice_test.go index 9b5640051d..7b5ef999f3 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice_test.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice_test.go @@ -86,13 +86,13 @@ func TestGasPrice_Check(t *testing.T) { ctx := testutils.Context(t) ge := gasMocks.NewEvmFeeEstimator(t) if test.FailedToGetFee { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( gas.EvmFee{}, feeLimit, errors.New("failed to retrieve gas price"), ) } else if test.CurrentLegacyGasPrice != nil { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( gas.EvmFee{ Legacy: assets.NewWei(test.CurrentLegacyGasPrice), }, @@ -100,7 +100,7 @@ func TestGasPrice_Check(t *testing.T) { nil, ) } else if test.CurrentDynamicGasPrice != nil { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( gas.EvmFee{ DynamicFeeCap: assets.NewWei(test.CurrentDynamicGasPrice), DynamicTipCap: assets.NewWei(big.NewInt(1_000_000_000)), diff --git a/core/services/relay/evm/chain_writer.go b/core/services/relay/evm/chain_writer.go index f188ffeced..466811d115 100644 --- a/core/services/relay/evm/chain_writer.go +++ b/core/services/relay/evm/chain_writer.go @@ -186,7 +186,7 @@ func (w *chainWriter) GetFeeComponents(ctx context.Context) (*commontypes.ChainF return nil, fmt.Errorf("gas estimator not available") } - fee, _, err := w.ge.GetFee(ctx, nil, 0, w.maxGasPrice) + fee, _, err := w.ge.GetFee(ctx, nil, 0, w.maxGasPrice, nil, nil) if err != nil { return nil, err } diff --git a/core/services/relay/evm/chain_writer_test.go b/core/services/relay/evm/chain_writer_test.go index 66c85bfc2c..e3fc8f8e22 100644 --- a/core/services/relay/evm/chain_writer_test.go +++ b/core/services/relay/evm/chain_writer_test.go @@ -86,7 +86,7 @@ func TestChainWriter(t *testing.T) { }) t.Run("GetFeeComponents", func(t *testing.T) { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: assets.NewWei(big.NewInt(1000000001)), DynamicFeeCap: assets.NewWei(big.NewInt(1000000002)), DynamicTipCap: assets.NewWei(big.NewInt(1000000003)), @@ -112,7 +112,7 @@ func TestChainWriter(t *testing.T) { }) t.Run("Returns Legacy Fee in absence of Dynamic Fee", func(t *testing.T) { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: assets.NewWei(big.NewInt(1000000001)), DynamicFeeCap: nil, DynamicTipCap: assets.NewWei(big.NewInt(1000000003)), @@ -124,7 +124,7 @@ func TestChainWriter(t *testing.T) { }) t.Run("Fails when neither legacy or dynamic fee is available", func(t *testing.T) { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: nil, DynamicFeeCap: nil, DynamicTipCap: nil, @@ -136,7 +136,7 @@ func TestChainWriter(t *testing.T) { t.Run("Fails when GetFee returns an error", func(t *testing.T) { expectedErr := fmt.Errorf("GetFee error") - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: nil, DynamicFeeCap: nil, DynamicTipCap: nil, @@ -146,7 +146,7 @@ func TestChainWriter(t *testing.T) { }) t.Run("Fails when L1Oracle returns error", func(t *testing.T) { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: assets.NewWei(big.NewInt(1000000001)), DynamicFeeCap: assets.NewWei(big.NewInt(1000000002)), DynamicTipCap: assets.NewWei(big.NewInt(1000000003)), diff --git a/core/web/evm_transfer_controller.go b/core/web/evm_transfer_controller.go index 88d3dead4c..75f6c07b6d 100644 --- a/core/web/evm_transfer_controller.go +++ b/core/web/evm_transfer_controller.go @@ -54,7 +54,7 @@ func (tc *EVMTransfersController) Create(c *gin.Context) { } if !tr.AllowHigherAmounts { - err = ValidateEthBalanceForTransfer(c, chain, tr.FromAddress, tr.Amount) + err = ValidateEthBalanceForTransfer(c, chain, tr.FromAddress, tr.Amount, tr.DestinationAddress) if err != nil { jsonAPIError(c, http.StatusUnprocessableEntity, errors.Errorf("transaction failed: %v", err)) return @@ -92,7 +92,7 @@ func (tc *EVMTransfersController) Create(c *gin.Context) { } // ValidateEthBalanceForTransfer validates that the current balance can cover the transaction amount -func ValidateEthBalanceForTransfer(c *gin.Context, chain legacyevm.Chain, fromAddr common.Address, amount assets.Eth) error { +func ValidateEthBalanceForTransfer(c *gin.Context, chain legacyevm.Chain, fromAddr common.Address, amount assets.Eth, toAddr common.Address) error { var err error var balance *big.Int @@ -116,7 +116,7 @@ func ValidateEthBalanceForTransfer(c *gin.Context, chain legacyevm.Chain, fromAd gasLimit := chain.Config().EVM().GasEstimator().LimitTransfer() estimator := chain.GasEstimator() - amountWithFees, err := estimator.GetMaxCost(c, amount, nil, gasLimit, chain.Config().EVM().GasEstimator().PriceMaxKey(fromAddr)) + amountWithFees, err := estimator.GetMaxCost(c, amount, nil, gasLimit, chain.Config().EVM().GasEstimator().PriceMaxKey(fromAddr), &fromAddr, &toAddr) if err != nil { return err } diff --git a/core/web/resolver/testdata/config-full.toml b/core/web/resolver/testdata/config-full.toml index c5547f8698..b546042756 100644 --- a/core/web/resolver/testdata/config-full.toml +++ b/core/web/resolver/testdata/config-full.toml @@ -316,6 +316,7 @@ LimitDefault = 12 LimitMax = 17 LimitMultiplier = '1.234' LimitTransfer = 100 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 10 BumpThreshold = 6 @@ -340,6 +341,9 @@ CheckInclusionPercentile = 19 EIP1559FeeCapBufferBlocks = 13 TransactionPercentile = 15 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '1s' + [EVM.HeadTracker] HistoryDepth = 15 MaxBufferSize = 17 diff --git a/core/web/resolver/testdata/config-multi-chain-effective.toml b/core/web/resolver/testdata/config-multi-chain-effective.toml index 8c063d0e9e..ca9edca06f 100644 --- a/core/web/resolver/testdata/config-multi-chain-effective.toml +++ b/core/web/resolver/testdata/config-multi-chain-effective.toml @@ -303,6 +303,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -318,6 +319,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -403,6 +407,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -418,6 +423,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -497,6 +505,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -512,6 +521,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 71ddf1c2c8..c315b574e7 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -1818,6 +1818,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -1833,6 +1834,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -1912,6 +1916,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -1927,6 +1932,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2006,6 +2014,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2021,6 +2030,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2100,6 +2112,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2115,6 +2128,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2195,6 +2211,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -2210,6 +2227,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -2289,6 +2309,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2304,6 +2325,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2383,6 +2407,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2398,6 +2423,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2478,6 +2506,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2493,6 +2522,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2572,6 +2604,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -2587,6 +2620,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2665,6 +2701,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2680,6 +2717,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2851,6 +2891,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2866,6 +2907,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2945,6 +2989,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -2960,6 +3005,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -3040,6 +3088,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3055,6 +3104,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -3134,6 +3186,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -3149,6 +3202,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -3228,6 +3284,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -3243,6 +3300,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -3322,6 +3382,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 mwei' BumpPercent = 40 BumpThreshold = 3 @@ -3337,6 +3398,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -3416,6 +3480,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 mwei' BumpPercent = 40 BumpThreshold = 3 @@ -3431,6 +3496,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -3510,6 +3578,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3525,6 +3594,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -3604,6 +3676,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -3619,6 +3692,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 400 MaxBufferSize = 3 @@ -3698,6 +3774,7 @@ LimitDefault = 100000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3713,6 +3790,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -3792,6 +3872,7 @@ LimitDefault = 2500000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3807,6 +3888,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 250 MaxBufferSize = 3 @@ -3886,6 +3970,7 @@ LimitDefault = 2500000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3901,6 +3986,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 1500 MaxBufferSize = 3 @@ -3981,6 +4069,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -3996,6 +4085,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -4075,6 +4167,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4090,6 +4183,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4356,6 +4452,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -4371,6 +4468,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4450,6 +4550,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4465,6 +4566,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4544,6 +4648,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 mwei' BumpPercent = 40 BumpThreshold = 3 @@ -4559,6 +4664,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -4638,6 +4746,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4653,6 +4762,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4732,6 +4844,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4747,6 +4860,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4825,6 +4941,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 0 @@ -4840,6 +4957,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 10 MaxBufferSize = 100 @@ -4919,6 +5039,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 mwei' BumpPercent = 40 BumpThreshold = 3 @@ -4934,6 +5055,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -5013,6 +5137,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -5028,6 +5153,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 400 MaxBufferSize = 3 @@ -5107,6 +5235,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 mwei' BumpPercent = 40 BumpThreshold = 3 @@ -5122,6 +5251,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -5201,6 +5333,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -5216,6 +5349,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5389,6 +5525,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -5404,6 +5541,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5483,6 +5623,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -5498,6 +5639,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -5577,6 +5721,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -5592,6 +5737,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5672,6 +5820,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -5687,6 +5836,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5767,6 +5919,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -5782,6 +5935,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5957,6 +6113,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -5972,6 +6129,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6051,6 +6211,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '2 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6066,6 +6227,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -6145,6 +6309,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6160,6 +6325,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6239,6 +6407,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6254,6 +6423,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6333,6 +6505,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '2 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6348,6 +6521,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -6426,6 +6602,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 40 BumpThreshold = 3 @@ -6441,6 +6618,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6519,6 +6699,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6534,6 +6715,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 1000 MaxBufferSize = 3 @@ -6612,6 +6796,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 40 BumpThreshold = 3 @@ -6627,6 +6812,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 350 MaxBufferSize = 3 @@ -6706,6 +6894,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6721,6 +6910,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6894,6 +7086,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -6909,6 +7102,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -6987,6 +7183,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -7002,6 +7199,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -7176,6 +7376,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -7191,6 +7392,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -7271,6 +7475,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -7286,6 +7491,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -7366,6 +7574,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -7381,6 +7590,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -7461,6 +7673,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -7476,6 +7689,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -7556,6 +7772,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -7571,6 +7788,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -7650,6 +7870,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '1 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -7665,6 +7886,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -7744,6 +7968,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '1 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -7759,6 +7984,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -7838,6 +8066,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -7853,6 +8082,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -7932,6 +8164,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -7947,6 +8180,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -8121,6 +8357,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -8136,6 +8373,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -8215,6 +8455,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -8230,6 +8471,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -8577,6 +8821,7 @@ LimitDefault = 8_000_000 # Default LimitMax = 8_000_000 # Default LimitMultiplier = '1.0' # Default LimitTransfer = 21_000 # Default +EstimateLimit = false # Default BumpMin = '5 gwei' # Default BumpPercent = 20 # Default BumpThreshold = 3 # Default @@ -8671,6 +8916,12 @@ LimitTransfer = 21_000 # Default ``` LimitTransfer is the gas limit used for an ordinary ETH transfer. +### EstimateLimit +```toml +EstimateLimit = false # Default +``` +EstimateLimit enables estimating gas limits for transactions. This feature respects the gas limit provided during transaction creation as an upper bound. + ### BumpMin ```toml BumpMin = '5 gwei' # Default @@ -8884,6 +9135,24 @@ Setting this number higher will cause the Chainlink node to select higher gas pr Setting it lower will tend to set lower gas prices. +## EVM.GasEstimator.FeeHistory +```toml +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' # Default +``` + + +### CacheTimeout +```toml +CacheTimeout = '10s' # Default +``` +CacheTimeout is the time to wait in order to refresh the cached values stored in the FeeHistory estimator. A small jitter is applied so the timeout won't be exactly the same each time. + +You want this value to be close to the block time. For slower chains, like Ethereum, you can set it to 12s, the same as the block time. For faster chains you can skip a block or two +and set it to two times the block time i.e. on Optimism you can set it to 4s. Ideally, you don't want to go lower than 1s since the RTT times of the RPC requests will be comparable to +the timeout. The estimator is already adding a buffer to account for a potential increase in prices within one or two blocks. On the other hand, slower frequency will fail to refresh +the prices and end up in stale values. + ## EVM.HeadTracker ```toml [EVM.HeadTracker] diff --git a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar index fe522ca1bb..a6612cc8f1 100644 --- a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar @@ -359,6 +359,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -374,6 +375,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 diff --git a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar index 5a5ce42d6b..79dcfa776d 100644 --- a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar @@ -359,6 +359,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -374,6 +375,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 diff --git a/testdata/scripts/node/validate/disk-based-logging.txtar b/testdata/scripts/node/validate/disk-based-logging.txtar index 12773dc99c..efa27eec11 100644 --- a/testdata/scripts/node/validate/disk-based-logging.txtar +++ b/testdata/scripts/node/validate/disk-based-logging.txtar @@ -359,6 +359,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -374,6 +375,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 diff --git a/testdata/scripts/node/validate/invalid.txtar b/testdata/scripts/node/validate/invalid.txtar index e709b24d5b..6932eb5038 100644 --- a/testdata/scripts/node/validate/invalid.txtar +++ b/testdata/scripts/node/validate/invalid.txtar @@ -349,6 +349,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -364,6 +365,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 diff --git a/testdata/scripts/node/validate/valid.txtar b/testdata/scripts/node/validate/valid.txtar index 9b22da4850..7074c82bf5 100644 --- a/testdata/scripts/node/validate/valid.txtar +++ b/testdata/scripts/node/validate/valid.txtar @@ -356,6 +356,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -371,6 +372,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3