From d6c444df28e822794395a374f872d611ec15c18e Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Tue, 25 Nov 2025 12:13:58 -0800 Subject: [PATCH 1/3] core/vm: for selfdestruct/sstore whose gas funcs are dependent on reading state, move readOnly call context check into gas func to avoid unecessary state reads in the gas handler in case where these are called in a static context. --- core/vm/instructions.go | 9 --------- core/vm/operations_acl.go | 8 ++++++++ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 44d3e81a9cfd..83eb7325d3ad 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -518,9 +518,6 @@ func opSload(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opSstore(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - if evm.readOnly { - return nil, ErrWriteProtection - } loc := scope.Stack.pop() val := scope.Stack.pop() evm.StateDB.SetState(scope.Contract.Address(), loc.Bytes32(), val.Bytes32()) @@ -882,9 +879,6 @@ func opStop(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opSelfdestruct(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - if evm.readOnly { - return nil, ErrWriteProtection - } beneficiary := scope.Stack.pop() balance := evm.StateDB.GetBalance(scope.Contract.Address()) evm.StateDB.AddBalance(beneficiary.Bytes20(), balance, tracing.BalanceIncreaseSelfdestruct) @@ -901,9 +895,6 @@ func opSelfdestruct(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - if evm.readOnly { - return nil, ErrWriteProtection - } beneficiary := scope.Stack.pop() balance := evm.StateDB.GetBalance(scope.Contract.Address()) evm.StateDB.SubBalance(scope.Contract.Address(), balance, tracing.BalanceDecreaseSelfdestruct) diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 085b018e4c42..db53ede5897f 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -28,6 +28,9 @@ import ( func makeGasSStoreFunc(clearingRefund uint64) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + if evm.readOnly { + return 0, ErrWriteProtection + } // If we fail the minimum gas availability invariant, fail (0) if contract.Gas <= params.SstoreSentryGasEIP2200 { return 0, errors.New("not enough gas for reentrancy sentry") @@ -226,6 +229,11 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { gas uint64 address = common.Address(stack.peek().Bytes20()) ) + // TODO: EEST test that performs selfdestruct within a static context. no reads should be reported + // This is probably covered in the converted blockchain tests but need to double-check + if evm.readOnly { + return 0, ErrWriteProtection + } if !evm.StateDB.AddressInAccessList(address) { // If the caller cannot afford the cost, this change will be rolled back evm.StateDB.AddAddressToAccessList(address) From d2ace19309507ea99aad04a4961154049181d3d1 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 26 Nov 2025 06:19:06 -0800 Subject: [PATCH 2/3] followup on last PR: include all sstore gas funcs (some were missing from last commit). expand to call opcodes --- core/vm/gas_table.go | 14 ++++++++++++++ core/vm/instructions.go | 3 --- core/vm/operations_acl.go | 9 +++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index c7c1274bf2f3..23a2cbbf4dd5 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -97,6 +97,9 @@ var ( ) func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + if evm.readOnly { + return 0, ErrWriteProtection + } var ( y, x = stack.Back(1), stack.Back(0) current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), x.Bytes32()) @@ -181,6 +184,9 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi // (2.2.2.1.) If original value is 0, add SSTORE_SET_GAS - SLOAD_GAS to refund counter. // (2.2.2.2.) Otherwise, add SSTORE_RESET_GAS - SLOAD_GAS gas to refund counter. func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + if evm.readOnly { + return 0, ErrWriteProtection + } // If we fail the minimum gas availability invariant, fail (0) if contract.Gas <= params.SstoreSentryGasEIP2200 { return 0, errors.New("not enough gas for reentrancy sentry") @@ -374,6 +380,10 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize transfersValue = !stack.Back(2).IsZero() address = common.Address(stack.Back(1).Bytes20()) ) + if evm.readOnly && transfersValue { + return 0, ErrWriteProtection + } + if evm.chainRules.IsEIP158 { if transfersValue && evm.StateDB.Empty(address) { gas += params.CallNewAccountGas @@ -462,6 +472,10 @@ func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo } func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + if evm.readOnly { + return 0, ErrWriteProtection + } + var gas uint64 // EIP150 homestead gas reprice fork: if evm.chainRules.IsEIP150 { diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 83eb7325d3ad..dd4d1fc24ee4 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -740,9 +740,6 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get the arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) - if evm.readOnly && !value.IsZero() { - return nil, ErrWriteProtection - } if !value.IsZero() { gas += params.CallStipend } diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index db53ede5897f..662fe8321c48 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -261,10 +261,15 @@ var ( func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { var ( - total uint64 // total dynamic gas used - addr = common.Address(stack.Back(1).Bytes20()) + total uint64 // total dynamic gas used + addr = common.Address(stack.Back(1).Bytes20()) + transfersValue = !stack.Back(2).IsZero() ) + if evm.readOnly && transfersValue { + return 0, ErrWriteProtection + } + // Check slot presence in the access list if !evm.StateDB.AddressInAccessList(addr) { evm.StateDB.AddAddressToAccessList(addr) From fe52714e22f5c40ad10b3d5a94da68f41d58866e Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 26 Nov 2025 10:05:29 -0800 Subject: [PATCH 3/3] fix call --- core/vm/operations_acl.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 662fe8321c48..a161cbb5141a 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -252,24 +252,27 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { } var ( - gasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCall) + innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCall) gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCall) gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCall) gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCode) ) +func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + transfersValue := !stack.Back(2).IsZero() + if evm.readOnly && transfersValue { + return 0, ErrWriteProtection + } + return innerGasCallEIP7702(evm, contract, stack, mem, memorySize) +} + func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { var ( - total uint64 // total dynamic gas used - addr = common.Address(stack.Back(1).Bytes20()) - transfersValue = !stack.Back(2).IsZero() + total uint64 // total dynamic gas used + addr = common.Address(stack.Back(1).Bytes20()) ) - if evm.readOnly && transfersValue { - return 0, ErrWriteProtection - } - // Check slot presence in the access list if !evm.StateDB.AddressInAccessList(addr) { evm.StateDB.AddAddressToAccessList(addr)