diff --git a/core/state_transition.go b/core/state_transition.go index a6742b98ca..5f52570bc7 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -737,8 +737,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ) if contractCreation { var result vm.GasBudget + // Capture the forwarded regular-gas amount BEFORE ForwardAll consumes + // it, so Absorb can back out state-gas spillover from + // UsedRegularGas per EIP-8037 (EELS semantics). + forwardedR := st.gasRemaining.RegularGas ret, _, result, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value) - st.gasRemaining.Absorb(result) + st.gasRemaining.Absorb(result, forwardedR) } else { // Increment the nonce for the next transaction. st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) @@ -756,8 +760,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // Execute the transaction's call. var result vm.GasBudget + forwardedR := st.gasRemaining.RegularGas ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) - st.gasRemaining.Absorb(result) + st.gasRemaining.Absorb(result, forwardedR) } // If this was a failed contract creation, refund the account creation costs. if rules.IsAmsterdam { diff --git a/core/vm/contract.go b/core/vm/contract.go index 11172af24c..7ee125470c 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -162,11 +162,10 @@ func (c *Contract) refundState(s uint64, logger *tracing.Hooks, reason tracing.G } } -// refundGas absorbs a sub-call's leftover GasBudget into this contract's gas -// state. Thin wrapper around GasBudget.Absorb with tracer integration. -func (c *Contract) refundGas(child GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) { +// refundGas absorbs a sub-call's leftover GasBudget into this contract's gas state. +func (c *Contract) refundGas(child GasBudget, forwarded uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) { prior := c.Gas - c.Gas.Absorb(child) + c.Gas.Absorb(child, forwarded) if logger.HasGasHook() && reason != tracing.GasChangeIgnored { logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 63269a071b..0e8b7fc98b 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -293,18 +293,21 @@ func (g GasBudget) Exit(err error) GasBudget { } // Absorb merges a sub-call's leftover GasBudget into this (caller's) running -// budget. The caller's UsedRegularGas is reclaimed by the unused forwarded -// regular gas (which was pre-charged in full at call entry); the state -// reservoir is overwritten with the child's leftover; and the child's signed -// net state-gas usage is added to the caller's accumulator. +// budget. Additionally, it does an EIP-8037 spillover correction: +// state-gas that spilled into the regular pool inside the child frame is +// excluded from the UsedRegularGas. // -// Invariant maintained by all callers: at the moment of this call, the -// caller's UsedRegularGas already accounts for the FULL forwarded regular -// gas (as if the child had consumed all of it). On halt, child.RegularGas -// is 0 so the reclaim is a no-op. -func (g *GasBudget) Absorb(child GasBudget) { +// spillover = forwarded - child.RegularGas - child.UsedRegularGas +// +// forwarded is the regular-gas amount that was passed to the child at call +// entry (i.e., the regular initial of the child's GasBudget). +func (g *GasBudget) Absorb(child GasBudget, forwarded uint64) { + spillover := forwarded - child.RegularGas - child.UsedRegularGas + g.UsedRegularGas -= child.RegularGas g.RegularGas += child.RegularGas g.StateGas = child.StateGas g.UsedStateGas += child.UsedStateGas + + g.UsedRegularGas -= spillover } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index d85fea5ee6..0c696c88e0 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -675,7 +675,7 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } scope.Stack.push(&stackvalue) - scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) if evm.chainRules.IsAmsterdam && suberr != nil { scope.Contract.Gas.RefundState(params.AccountCreationSize * evm.Context.CostPerStateByte) } @@ -710,7 +710,7 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { stackvalue.SetBytes(addr.Bytes()) } scope.Stack.push(&stackvalue) - scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) // If the creation frame reverts or halts exceptionally, the charged state-gas // is refilled back to the state reservoir in Amsterdam. @@ -760,7 +760,7 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if err == nil || err == ErrExecutionReverted { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) // If the call frame reverts or halts exceptionally, the charged state-gas // is refilled back to the state reservoir in Amsterdam. @@ -807,7 +807,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -836,7 +836,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if err == nil || err == ErrExecutionReverted { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -866,7 +866,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil