diff --git a/core/state_transition.go b/core/state_transition.go index c01718a5e0..d3c3f0f886 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -603,12 +603,22 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { var receiptGasUsed uint64 if rules.IsAmsterdam { receiptGasUsed = msg.GasLimit - st.gasRemaining.RegularGas - st.gasRemaining.StateGas - st.stateGasRefund + // On successful execution, refund regular gas that was consumed for + // state operations in child calls that subsequently reverted. This gas + // paid for state growth that didn't persist, so the user shouldn't pay. + // On failed execution (exceptional halt), all gas is consumed — no refund. + if vmerr == nil { + receiptGasUsed -= st.gasRemaining.RevertedStateGasSpill + } } // Return gas to the user if rules.IsAmsterdam { // In Amsterdam, return regular gas + unspent state gas reservoir + state gas refund. gasReturn := st.gasRemaining.RegularGas + st.gasRemaining.StateGas + st.stateGasRefund + if vmerr == nil { + gasReturn += st.gasRemaining.RevertedStateGasSpill + } remaining := uint256.NewInt(gasReturn) remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice)) st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn) @@ -785,7 +795,7 @@ func (st *stateTransition) blockGasUsed(intrinsicGas, execGasStart vm.GasCosts) // This gas was consumed from the regular pool but was for state operations // that didn't persist, so it shouldn't count in either dimension for block // accounting (invisible to the block). This matches nethermind's approach. - execRegularUsed := totalExecUsed - execStateUsed - st.gasRemaining.RevertedStateGasSpill + execRegularUsed := totalExecUsed - execStateUsed - st.gasRemaining.RevertedStateGasSpill - st.gasRemaining.CollisionConsumedGas txRegular := intrinsicGas.RegularGas + execRegularUsed txState := intrinsicGas.StateGas + execStateUsed - st.stateGasRefund diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index bb7a1b2b32..d0e044af72 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -17,7 +17,14 @@ type GasCosts struct { // call fails, its state changes are undone, but the regular gas was already // consumed. Block gas accounting must exclude this amount from the regular // gas dimension since it was for state operations that didn't persist. + // This gas is refunded to the user (invisible to both block and receipt). RevertedStateGasSpill uint64 + + // CollisionConsumedGas tracks regular gas consumed on CREATE/CREATE2 address + // collision. On collision, the child's regular gas is consumed (user pays) + // but must be excluded from block regular gas accounting to preserve 2D + // block gas semantics. Unlike RevertedStateGasSpill, this is NOT refunded. + CollisionConsumedGas uint64 } func (g GasCosts) Max() uint64 { @@ -64,6 +71,7 @@ func (g *GasCosts) Add(b GasCosts) { g.StateGas += b.StateGas g.TotalStateGasCharged += b.TotalStateGasCharged g.RevertedStateGasSpill += b.RevertedStateGasSpill + g.CollisionConsumedGas += b.CollisionConsumedGas } // RevertStateGas handles state gas accounting when a call reverts (EIP-8037). diff --git a/core/vm/instructions.go b/core/vm/instructions.go index da9d6ff518..95f851c646 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -704,9 +704,10 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { returnGas.StateGas = stateGas } // On address collision, child's regular gas is consumed (not returned). - // Track as RevertedStateGasSpill so block 2D accounting is unaffected. + // Track as CollisionConsumedGas so block 2D accounting is unaffected + // while the user still pays for the consumed gas (not refunded). if evm.chainRules.IsAmsterdam && errors.Is(suberr, ErrContractAddressCollision) { - returnGas.RevertedStateGasSpill += returnGas.RegularGas + returnGas.CollisionConsumedGas += returnGas.RegularGas returnGas.RegularGas = 0 } scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) @@ -770,9 +771,10 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { returnGas.StateGas = stateGas } // On address collision, child's regular gas is consumed (not returned). - // Track as RevertedStateGasSpill so block 2D accounting is unaffected. + // Track as CollisionConsumedGas so block 2D accounting is unaffected + // while the user still pays for the consumed gas (not refunded). if evm.chainRules.IsAmsterdam && errors.Is(suberr, ErrContractAddressCollision) { - returnGas.RevertedStateGasSpill += returnGas.RegularGas + returnGas.CollisionConsumedGas += returnGas.RegularGas returnGas.RegularGas = 0 } scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)