From fa45d1a38fd64847e104b2f360eb49ef1b766afc Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Wed, 22 Apr 2026 17:31:53 +0200 Subject: [PATCH] core: apply fixes for 8037 --- core/state/statedb.go | 30 ++++++++++++++++++++++++++++ core/state/statedb_hooked.go | 8 ++++++++ core/state_transition.go | 38 +++++++++++++++++++++++++++++++++++- core/vm/contract.go | 12 ++++++++++++ core/vm/evm.go | 4 +++- core/vm/gas_table.go | 25 +++++++++++++++++------- core/vm/instructions.go | 7 ++++++- core/vm/interface.go | 8 ++++++++ 8 files changed, 122 insertions(+), 10 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index d17f947a12..ae5e343c85 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -687,6 +687,36 @@ func (s *StateDB) IsNewContract(addr common.Address) bool { return obj.newContract } +// SameTxSelfDestructs returns the addresses that were both created and +// self-destructed in the current transaction (EIP-6780). Used for the +// EIP-8037 same-tx selfdestruct state-gas refund. +func (s *StateDB) SameTxSelfDestructs() []common.Address { + var out []common.Address + for addr, obj := range s.stateObjects { + if obj.newContract && obj.selfDestructed { + out = append(out, addr) + } + } + return out +} + +// NewStorageSlotCount returns the number of storage slots that were written +// to a non-zero value in the current transaction on the given account. Used +// for the EIP-8037 same-tx selfdestruct state-gas refund. +func (s *StateDB) NewStorageSlotCount(addr common.Address) int { + obj, ok := s.stateObjects[addr] + if !ok { + return 0 + } + var count int + for _, v := range obj.dirtyStorage { + if v != (common.Hash{}) { + count++ + } + } + return count +} + // Copy creates a deep, independent copy of the state. // Snapshots of the copied state cannot be applied to the copy. func (s *StateDB) Copy() *StateDB { diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index ae6d795cd3..06bdee78f9 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -80,6 +80,14 @@ func (s *hookedStateDB) GetCodeSize(addr common.Address) int { return s.inner.GetCodeSize(addr) } +func (s *hookedStateDB) SameTxSelfDestructs() []common.Address { + return s.inner.SameTxSelfDestructs() +} + +func (s *hookedStateDB) NewStorageSlotCount(addr common.Address) int { + return s.inner.NewStorageSlotCount(addr) +} + func (s *hookedStateDB) AddRefund(u uint64) { s.inner.AddRefund(u) } diff --git a/core/state_transition.go b/core/state_transition.go index cb494256b8..927c9a055e 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -89,7 +89,7 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set gas.RegularGas += uint64(len(authList)) * params.TxAuthTupleRegularGas gas.StateGas += uint64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * costPerStateByte } else { - gas.RegularGas += uint64(len(authList)) * params.TxAuthTupleGas + gas.RegularGas += uint64(len(authList)) * params.CallNewAccountGas } } dataLen := uint64(len(data)) @@ -607,6 +607,42 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ret, st.gasRemaining, execGasUsed, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value) } + // On outer level tx failure, no state is written. + if rules.IsAmsterdam && vmerr != nil { + st.gasRemaining.StateGas += execGasUsed.StateGas + execGasUsed.StateGas = 0 + } + + // Refund costs for selfdestructed accounts and slots. + if rules.IsAmsterdam && vmerr == nil { + cpsb := st.evm.Context.CostPerGasByte + stateGasUsed := execGasUsed.StateGas + cost.StateGas + var sdRefund uint64 + for _, addr := range st.state.SameTxSelfDestructs() { + r := params.AccountCreationSize * cpsb + r += uint64(st.state.NewStorageSlotCount(addr)) * params.StorageCreationSize * cpsb + r += uint64(st.state.GetCodeSize(addr)) * cpsb + sdRefund += r + } + if sdRefund > stateGasUsed { + sdRefund = stateGasUsed + } + if sdRefund > 0 { + st.gasRemaining.StateGas += sdRefund + if execGasUsed.StateGas >= sdRefund { + execGasUsed.StateGas -= sdRefund + } else { + extra := sdRefund - execGasUsed.StateGas + execGasUsed.StateGas = 0 + if cost.StateGas >= extra { + cost.StateGas -= extra + } else { + cost.StateGas = 0 + } + } + } + } + // Record the gas used excluding gas refunds. This value represents the actual // gas allowance required to complete execution. peakGasUsed := st.gasUsed() diff --git a/core/vm/contract.go b/core/vm/contract.go index 207d21e4ad..0edef083c4 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -160,6 +160,18 @@ func (c *Contract) RefundGas(err error, initialRegularGasUsed uint64, gas GasBud c.GasUsed.RegularGas = initialRegularGasUsed + gasUsed.RegularGas } +// Refunds the account creation state costs if a CREATE/CREATE2 call fails. +func (c *Contract) RefundCreateStateGas(refund uint64) { + if refund > 0 { + c.Gas.StateGas += refund + if c.GasUsed.StateGas >= refund { + c.GasUsed.StateGas -= refund + } else { + c.GasUsed.StateGas = 0 + } + } +} + // Address returns the contracts address func (c *Contract) Address() common.Address { return c.address diff --git a/core/vm/evm.go b/core/vm/evm.go index de397a3e8e..e733e79793 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -544,8 +544,10 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) } + // Record all burned gas + burned := gas.RegularGas gas.Exhaust() - return nil, common.Address{}, gas, GasCosts{}, ErrContractAddressCollision + return nil, common.Address{}, gas, GasUsed{RegularGas: burned}, ErrContractAddressCollision } // Create a new account on the state only if the object was not present. // It might be possible the contract code is deployed to a pre-existent diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 05425f4db6..b6693e3efe 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -671,12 +671,14 @@ func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo } if original == current { if original == (common.Hash{}) { // create slot (2.1.1) - // EIP-8037: Return both regular and state gas. The interpreter - // charges regular gas before state gas, preventing reservoir - // inflation when the regular charge OOGs. + // EIP-8037: Return both regular and state gas. System calls do not charge state gas. + var stateGas uint64 + if !contract.IsSystemCall { + stateGas = params.StorageCreationSize * evm.Context.CostPerGasByte + } return GasCosts{ RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929, - StateGas: params.StorageCreationSize * evm.Context.CostPerGasByte, + StateGas: stateGas, }, nil } if value == (common.Hash{}) { // delete slot (2.1.2b) @@ -695,9 +697,18 @@ func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo } if original == value { if original == (common.Hash{}) { // reset to original inexistent slot (2.2.2.1) - // EIP 2200 Original clause: - //evm.StateDB.AddRefund(params.SstoreSetGasEIP2200 - params.SloadGasEIP2200) - evm.StateDB.AddRefund(params.StorageCreationSize*evm.Context.CostPerGasByte + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929) + // EIP-8037 point (2): refund state gas directly to the reservoir + // at the SSTORE restoration point (0→x→0 in same tx); not to the + // refund counter, which is capped at gas_used/5. + stateRefund := params.StorageCreationSize * evm.Context.CostPerGasByte + contract.Gas.StateGas += stateRefund + if contract.GasUsed.StateGas >= stateRefund { + contract.GasUsed.StateGas -= stateRefund + } else { + contract.GasUsed.StateGas = 0 + } + // Regular portion of the refund still goes through the refund counter. + evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929) } else { // reset to original existing slot (2.2.2.2) // EIP 2200 Original clause: // evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.SloadGasEIP2200) diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 5b244bf1b1..234fcbab33 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -682,7 +682,9 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Stack.push(&stackvalue) scope.Contract.RefundGas(suberr, regularGasUsed, returnGas, childGasUsed, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) - + if evm.chainRules.IsAmsterdam && suberr != nil { + scope.Contract.RefundCreateStateGas(params.AccountCreationSize * evm.Context.CostPerGasByte) + } if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer return res, nil @@ -719,6 +721,9 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Stack.push(&stackvalue) scope.Contract.RefundGas(suberr, regularGasUsed, returnGas, childGasUsed, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + if evm.chainRules.IsAmsterdam && suberr != nil { + scope.Contract.RefundCreateStateGas(params.AccountCreationSize * evm.Context.CostPerGasByte) + } if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer diff --git a/core/vm/interface.go b/core/vm/interface.go index 4d75dd46d0..e8d5702c57 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -71,6 +71,14 @@ type StateDB interface { // during the current transaction. IsNewContract(addr common.Address) bool + // SameTxSelfDestructs returns addresses that were created and then + // self-destructed in the current transaction. + SameTxSelfDestructs() []common.Address + + // NewStorageSlotCount returns the number of storage slots written to a + // non-zero value in the current transaction on the given address. + NewStorageSlotCount(addr common.Address) int + // Empty returns whether the given account is empty. Empty // is defined according to EIP161 (balance = nonce = code = 0). Empty(common.Address) bool