A code override can return the expected result and still charge the wrong gas. In a proposed fix for Reth’s precompile overrides, an empty-code replacement worked, but a nested call to it cost an extra 2,500 gas.

The original report concerned replacement code being ignored. The gas error came from my first version of the fix, which changed native dispatch and accidentally changed address warming with it.

Getting the replacement code to run

State overrides let an RPC request simulate a call against temporary account state. They do not change the chain's state or the network's precompile rules.

The report in Reth #27806 concerned a code override at 0x04, the identity precompile. Identity normally returns its input unchanged. My reproduction used replacement bytecode that returns the byte 0x2a:

Call Expected output Observed output before the fix
Ordinary address, replacement code 0x2a 0x2a
Identity, no code override, input 0x1234 0x1234 0x1234
Identity, replacement code, input 0x1234 0x2a 0x1234

The database contained the replacement code, but the EVM dispatched to the native identity implementation first.

My patch wraps the installed precompile provider—the component that identifies and executes native precompiles—and masks addresses with an explicit code override. That includes empty bytecode. The proposed behavior leaves balance-, nonce- and storage-only overrides unchanged; their cross-client semantics are a separate discussion.

Wrapping the installed provider avoids rebuilding it and losing custom precompiles or cached results. Calls to masked addresses fall through to account code.

The wrapper also changed gas

The wrapper filtered the provider's address enumeration as well as its execution methods. I had left warm_addresses using the trait default, which derived the warm set from that filtered list. A masked address therefore stopped being prewarmed.

MysticRyuujin pointed this out in review, with cross-client results showing that code overrides preserve the fork's precompile warm set.

EIP-2929 initializes the transaction's accessed-address set with the sender, destination and precompiles. A warm account access costs 100 gas; a cold one costs 2,600. Suppressing native dispatch should not remove the address from that initial set.

The correction in my wrapper was to forward warming to the original provider:

fn warm_addresses(&self, warm: &mut evm2::evm::PrewarmSet) { self.inner.warm_addresses(warm); } 

Membership checks, native execution and address/ID enumeration remain filtered. Warming follows the underlying provider's contract.

Testing the gas separately

Calling 0x04 directly would hide this mistake: the transaction destination already starts warm. The regression test instead calls an ordinary probe contract, which makes a nested STATICCALL to 0x04.

The probe uses empty input and output buffers under Prague rules. It records GAS before and after the call; the measured difference includes 25 gas of probe overhead.

Case Measured gas
Empty-code override with my initial filter 2625
Empty-code override after forwarding warming 125
STOP replacement after forwarding warming 125
Native identity, no override 140

Empty code and STOP require no execution gas here, so the corrected result is 100 for warm access plus 25 for the probe. Native identity adds its 15-gas charge for empty input. My initial filter charged 2,600 for cold access instead: the extra 2,500 gas came from the wrapper, not the original report.

I also added a custom-provider test whose warm set includes an address absent from its precompile list. This checks that the wrapper preserves the provider's actual warming behavior rather than reconstructing it from another address list.

The gas regression and correction commit are available for inspection. These numbers measure gas accounting in a test, not throughput or a reduction in fees paid on the live network.

Related work

The ownership hook used by this wrapper was merged in evm2 #1067. As of 15 November 2026, the Reth patch uses that upstream hook but is still under review and has not been merged. The tests discussed here exercise execution and RPC helpers, rather than a full-node setup.

Thanks to MysticRyuujin for identifying the warming problem and providing cross-client checks. Their separate evm2 #1068 addresses warming when precompiles are relocated.