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.

