From c5d36e749672a7aa999fe9a4b4020c15a5f47e45 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Mon, 6 Jul 2026 11:03:20 +0800 Subject: [PATCH] core/vm: implement access cost check in SSTORE --- core/vm/eip8038_test.go | 46 +++++++++++++++++++++++++++++++++++++++++ core/vm/gas_table.go | 24 ++++++++++++++++----- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/core/vm/eip8038_test.go b/core/vm/eip8038_test.go index c08c54fb2c..3d0383942b 100644 --- a/core/vm/eip8038_test.go +++ b/core/vm/eip8038_test.go @@ -235,3 +235,49 @@ func TestEIP8038SelfdestructAccountWrite(t *testing.T) { t.Fatalf("state gas = %d, want %d", res.UsedStateGas, want) } } + +// EIP-7928: SSTORE must cover the slot's access cost before its implicit read +// of the current value, so that a failing SSTORE does not leak the slot into +// the block access list. The stipend sentry alone cannot guard the read, as +// COLD_STORAGE_ACCESS (3000) exceeds GAS_CALL_STIPEND (2300). +func TestSStoreBALAccessCostCheck(t *testing.T) { + self := common.BytesToAddress([]byte("self")) + slot := common.BytesToHash([]byte{0}) + + // run executes a noop write (1 -> 1) on a cold slot with block access list + // recording enabled and returns the recorded storage reads of the contract. + run := func(regular uint64) (map[common.Hash]struct{}, error) { + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + statedb.CreateAccount(self) + statedb.SetCode(self, sstore(0, 1), tracing.CodeChangeUnspecified) + statedb.SetState(self, slot, common.BytesToHash([]byte{1})) + statedb.Finalise(true) + + evm := amsterdam8037EVM(statedb) + statedb.Prepare(evm.chainRules, common.Address{}, common.Address{}, &self, nil, nil) + _, _, err := evm.Call(common.Address{}, self, nil, NewGasBudget(regular, 0), new(uint256.Int)) + acct := statedb.Finalise(true).Accounts[self] + if acct == nil { + return nil, err + } + return acct.StorageReads, err + } + + // gas_left at the SSTORE clears the stipend sentry but falls one short of + // the cold access cost: the op must fail without touching the slot. + reads, err := run(6 + params.ColdStorageAccessAmsterdam - 1) + if err == nil { + t.Fatal("expected OOG below the cold access cost") + } + if _, ok := reads[slot]; ok { + t.Fatal("failing SSTORE leaked the slot into the block access list") + } + // Exactly the cold access cost succeeds and records the read. + reads, err = run(6 + params.ColdStorageAccessAmsterdam) + if err != nil { + t.Fatalf("unexpected failure at the cold access cost: %v", err) + } + if _, ok := reads[slot]; !ok { + t.Fatal("successful SSTORE missing from the block access list") + } +} diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 8cc63cb236..fbef287270 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -696,17 +696,31 @@ 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()) ) // Check slot presence in the access list access := params.WarmStorageReadCostEIP2929 - if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot) + if !slotPresent { access = params.ColdStorageAccessAmsterdam + } + // SSTORE performs an implicit read of the current storage value for the gas + // calculation below. Since the cold access cost exceeds GAS_CALL_STIPEND + // after the Amsterdam repricing, the reentrancy sentry alone no longer + // guards this read: the frame must also cover the slot's access cost + // beforehand, otherwise the slot must not be recorded in the block access + // list (EIP-7928). + if contract.Gas.RegularGas < access { + return GasCosts{}, ErrOutOfGas + } + if !slotPresent { evm.StateDB.AddSlotToAccessList(contract.Address(), slot) } + var ( + current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) + value = common.Hash(y.Bytes32()) + ) stateSet := params.StorageCreationSize * evm.Context.CostPerStateByte if current == value { // noop (1)