diff --git a/core/state_transition.go b/core/state_transition.go index 927c9a055e..0443e8b655 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -609,14 +609,16 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // On outer level tx failure, no state is written. if rules.IsAmsterdam && vmerr != nil { - st.gasRemaining.StateGas += execGasUsed.StateGas + if execGasUsed.StateGas > 0 { + st.gasRemaining.StateGas += uint64(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 + stateGasUsed := int64(cost.StateGas) + execGasUsed.StateGas var sdRefund uint64 for _, addr := range st.state.SameTxSelfDestructs() { r := params.AccountCreationSize * cpsb @@ -624,15 +626,22 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { r += uint64(st.state.GetCodeSize(addr)) * cpsb sdRefund += r } - if sdRefund > stateGasUsed { - sdRefund = stateGasUsed + if stateGasUsed < 0 { + sdRefund = 0 + } else if sdRefund > uint64(stateGasUsed) { + sdRefund = uint64(stateGasUsed) } if sdRefund > 0 { st.gasRemaining.StateGas += sdRefund - if execGasUsed.StateGas >= sdRefund { - execGasUsed.StateGas -= sdRefund + if execGasUsed.StateGas >= int64(sdRefund) { + execGasUsed.StateGas -= int64(sdRefund) } else { - extra := sdRefund - execGasUsed.StateGas + var extra uint64 + if execGasUsed.StateGas > 0 { + extra = sdRefund - uint64(execGasUsed.StateGas) + } else { + extra = sdRefund + } execGasUsed.StateGas = 0 if cost.StateGas >= extra { cost.StateGas -= extra @@ -673,7 +682,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // EIP-8037: 2D gas accounting for Amsterdam. // tx_regular = intrinsic_regular + exec_regular_gas_used // tx_state = intrinsic_state (adjusted) + exec_state_gas_used - txState := cost.StateGas + execGasUsed.StateGas + // execGasUsed.StateGas may be negative when an SSTORE 0→x→0 refund + // exceeded the intrinsic-charged state gas + txState := cost.StateGas + if execGasUsed.StateGas > 0 { + txState += uint64(execGasUsed.StateGas) + } txRegular := cost.RegularGas + execGasUsed.RegularGas txRegular = max(txRegular, floorDataGas) if err := st.gp.ReturnGasAmsterdam(txRegular, txState, st.gasUsed()); err != nil { diff --git a/core/vm/contract.go b/core/vm/contract.go index 0edef083c4..d2d9bb3087 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -139,13 +139,21 @@ func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.G return true } -// RefundGas refunds gas to the contract. gasUsed carries the child frame's -// accumulated gas usage metrics (EIP-8037), incorporated on both success and error. +// RefundGas refunds gas to the contract. func (c *Contract) RefundGas(err error, initialRegularGasUsed uint64, gas GasBudget, gasUsed GasUsed, logger *tracing.Hooks, reason tracing.GasChangeReason) { - // If the preceding call errored, return the state gas - // to the parent call + // If the preceding call errored, return the child's state-gas reservoir + // and any net state-gas consumption to the parent. A negative + // gasUsed.StateGas (an unabsorbed SSTORE 0→x→0 refund) is an inflation + // to undo: the matching state mutation was reverted with the child, so + // the refund must not leak back to the parent. if err != nil { - gas.StateGas += gasUsed.StateGas + if gasUsed.StateGas >= 0 { + gas.StateGas += uint64(gasUsed.StateGas) + } else if undo := uint64(-gasUsed.StateGas); gas.StateGas >= undo { + gas.StateGas -= undo + } else { + gas.StateGas = 0 + } gasUsed.StateGas = 0 } if gas.RegularGas == 0 && gas.StateGas == 0 && gasUsed.StateGas == 0 && gasUsed.RegularGas == 0 { @@ -164,11 +172,7 @@ func (c *Contract) RefundGas(err error, initialRegularGasUsed uint64, gas GasBud 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 - } + c.GasUsed.StateGas -= int64(refund) } } diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index b6693e3efe..ed2e4d4494 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -702,11 +702,7 @@ func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo // 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 - } + contract.GasUsed.StateGas -= int64(stateRefund) // 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) diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index ce1fadc443..b35e161680 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -18,11 +18,16 @@ package vm import "fmt" -type GasUsed = GasCosts +// GasUsed is the per-frame accumulator for gas consumption. +// StateGas is signed because of 0 -> X -> 0 SSTORE refunds. +type GasUsed struct { + RegularGas uint64 + StateGas int64 +} func (g *GasUsed) Add(costs GasCosts) { g.RegularGas += costs.RegularGas - g.StateGas += costs.StateGas + g.StateGas += int64(costs.StateGas) } // GasCosts denotes a vector of gas costs in the @@ -66,11 +71,9 @@ func (g GasBudget) Used(initial GasBudget) uint64 { return (initial.RegularGas + initial.StateGas) - (g.RegularGas + g.StateGas) } -// Exhaust sets all remaining gas to zero, preserving the initial amount -// for usage tracking. +// Exhaust burns the remaining regular gas on exceptional halt. func (g *GasBudget) Exhaust() { g.RegularGas = 0 - g.StateGas = 0 } func (g *GasBudget) Copy() GasBudget {