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.
This commit is contained in:
rjl493456442 2026-07-07 20:38:08 +08:00 committed by GitHub
parent e5c5e1897d
commit cf35a1b6f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 27 additions and 5 deletions

View file

@ -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)
}
}

View file

@ -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