Skip to content

fix: guard against empty baseFeePerGas in FeeHistoryEstimator.RefreshDynamicPrice#498

Open
snowkide wants to merge 1 commit into
smartcontractkit:developfrom
snowkide:fix/feehistory-empty-basefee-panic
Open

fix: guard against empty baseFeePerGas in FeeHistoryEstimator.RefreshDynamicPrice#498
snowkide wants to merge 1 commit into
smartcontractkit:developfrom
snowkide:fix/feehistory-empty-basefee-panic

Conversation

@snowkide

Copy link
Copy Markdown

Summary

FeeHistoryEstimator.RefreshDynamicPrice indexes into feeHistory.BaseFee with
feeHistory.BaseFee[len(feeHistory.BaseFee)-1] without checking the slice is
non-empty. If an eth_feeHistory RPC call returns err == nil with an empty
baseFeePerGas array — which we've observed real providers do instead of
returning an error, e.g. on a deep/archive block range they can't serve, or on
chains without full EIP-1559 support — this indexes with -1 and panics:

panic: runtime error: index out of range [-1]

goroutine ... [running]:
github.com/smartcontractkit/chainlink-evm/pkg/gas.(*FeeHistoryEstimator).RefreshDynamicPrice(...)
	pkg/gas/fee_history_estimator.go:243 +0x8e5
github.com/smartcontractkit/chainlink-evm/pkg/gas.(*FeeHistoryEstimator).run(...)
	pkg/gas/fee_history_estimator.go:130 +0x105

Because RefreshDynamicPrice runs in an unrecovered background goroutine
(FeeHistoryEstimator.run), this panic is fatal to the entire node process —
not just the affected chain's gas estimator. Every chain and job the node
services goes down until the process is restarted.

Note the Reward slice a few lines below this is already defended
(if len(reward) < 2 { continue }); BaseFee was not.

Impact observed

We hit this in production across three separate CCIP node deployments running
different chainlink-evm builds (pseudo-versions from Feb, Mar, and Jun 2026),
on different physical hosts, recurring in bursts over several weeks before we
root-caused it. Log history shows it's been reachable since at least the
February 2026 build.

Fix

Added a bounds check right after the existing RPC-error check, returning a
descriptive error instead of indexing into the empty slice — the same
treatment already given to a malformed Reward response a few lines down.

Testing

Added TestRefreshDynamicPrice_EmptyBaseFee, which reproduces the panic
against the real package using its own mocks (feeding an empty BaseFee with
err == nil) and confirms the fix returns a graceful error instead. Full
existing pkg/gas test suite passes unmodified; go vet is clean.

@snowkide snowkide requested a review from a team as a code owner July 10, 2026 08:57
…DynamicPrice

RefreshDynamicPrice indexed into feeHistory.BaseFee with
feeHistory.BaseFee[len(feeHistory.BaseFee)-1] without checking the slice was
non-empty. If an eth_feeHistory RPC call returns err == nil with an empty
baseFeePerGas array (some providers do this instead of returning an error,
e.g. on a deep/archive range they can't serve, or on chains without full
EIP-1559 support), this indexes with -1 and panics with
"runtime error: index out of range [-1]".

Because this runs in an unrecovered background goroutine, the panic crashes
the entire node process, not just the affected chain's estimator.

We hit this in production across three separate CCIP node deployments running
different chainlink-evm builds (Feb, Mar, and Jun 2026 pseudo-versions),
recurring over several weeks. Reproduced deterministically with a unit test
using the package's own mocks, feeding an empty BaseFee.

This adds a bounds check that returns a normal error instead, matching the
existing handling for the Reward slice a few lines below (which already
guards against short/empty per-block reward arrays).
@snowkide snowkide force-pushed the fix/feehistory-empty-basefee-panic branch from cdd66e8 to 15d2257 Compare July 10, 2026 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant