From cf35a1b6f6f7aed8fdc12a782645cfa96711afe8 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Tue, 7 Jul 2026 20:38:08 +0800 Subject: [PATCH] core/vm: add access cost check (#35261) This is an edge case found by @weiihann. Under 8038, the cold storage access cost is increased to 3,000 gas, which exceeds the sentry check threshold. Therefore, the sentry check no longer guarantees that the remaining gas is sufficient to cover a cold slot access. Therefore, an additional access affordability check is added to eliminate the potential DoS vector. --- core/vm/eip8038_test.go | 17 +++++++++++++++++ core/vm/gas_table.go | 15 ++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/core/vm/eip8038_test.go b/core/vm/eip8038_test.go index c08c54fb2c..f20a9d8a0e 100644 --- a/core/vm/eip8038_test.go +++ b/core/vm/eip8038_test.go @@ -21,6 +21,7 @@ package vm import ( + "strings" "testing" "github.com/ethereum/go-ethereum/common" @@ -235,3 +236,19 @@ func TestEIP8038SelfdestructAccountWrite(t *testing.T) { t.Fatalf("state gas = %d, want %d", res.UsedStateGas, want) } } + +// TestEIP8038SStoreAccessGuard covers the affordability check that bails out +// before the slot is read once the gas left cannot cover the slot's access cost. +// The two PUSH1s cost 6, so a 2506 budget leaves 2500 at the SSTORE: above the +// reentrancy sentry (2300) yet below COLD_STORAGE_ACCESS (3000). The guard must +// fire, distinguishable from the sentry/charge OOG by its "slot access" message. +func TestEIP8038SStoreAccessGuard(t *testing.T) { + budget := NewGasBudget(6+params.SstoreSentryGasEIP2200+200, 0) + _, _, err := run8038(t, sstore(0, 1), budget, new(uint256.Int), nil) + if err == nil { + t.Fatal("expected failure: gas left cannot cover cold-slot access") + } + if !strings.Contains(err.Error(), "not enough gas for slot access") { + t.Fatalf("got %q, want the slot-access guard error", err) + } +} diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 8cc63cb236..d8fbf2b461 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -696,10 +696,10 @@ func gasSStore8037And8038(evm *EVM, contract *Contract, stack *Stack, mem *Memor return GasCosts{}, errors.New("not enough gas for reentrancy sentry") } var ( - y, x = stack.back(1), stack.peek() - slot = common.Hash(x.Bytes32()) - current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) - value = common.Hash(y.Bytes32()) + y, x = stack.back(1), stack.peek() + slot = common.Hash(x.Bytes32()) + value = common.Hash(y.Bytes32()) + stateSet = params.StorageCreationSize * evm.Context.CostPerStateByte ) // Check slot presence in the access list access := params.WarmStorageReadCostEIP2929 @@ -707,7 +707,12 @@ func gasSStore8037And8038(evm *EVM, contract *Contract, stack *Stack, mem *Memor access = params.ColdStorageAccessAmsterdam evm.StateDB.AddSlotToAccessList(contract.Address(), slot) } - stateSet := params.StorageCreationSize * evm.Context.CostPerStateByte + // Check access cost affordability before reading slot + if contract.Gas.RegularGas < access { + return GasCosts{}, errors.New("not enough gas for slot access") + } + // Read the slot value for gas cost measurement + current, original := evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) if current == value { // noop (1) return GasCosts{RegularGas: access}, nil