From 6e4f5710f926d37e16c61555a5bd21e4f95b49de Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Thu, 21 May 2026 16:14:49 +0200 Subject: [PATCH] core/vm: use up gas _after_ the call --- core/vm/gascosts.go | 26 ++++++++++++++------------ core/vm/instructions.go | 29 +++++++++++++++++------------ 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 63269a071b..0ae36ba126 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -173,8 +173,7 @@ func (g *GasBudget) RefundRegular(s uint64) { // Forward drains `regular` regular gas and the entire state reservoir from // the parent's running budget and returns the initial GasBudget for a child -// frame. The parent's UsedRegularGas is bumped by the forwarded amount so -// that the absorb-on-return path correctly reclaims the unused portion. +// frame. // // Used by frame boundaries where the regular forward has NOT been pre- // deducted: tx-level dispatch (state_transition) and CREATE / CREATE2. The @@ -185,7 +184,6 @@ func (g *GasBudget) RefundRegular(s uint64) { // apply any EIP-150 1/64 retention before calling Forward. func (g *GasBudget) Forward(regular uint64) GasBudget { g.RegularGas -= regular - g.UsedRegularGas += regular child := GasBudget{ RegularGas: regular, @@ -293,18 +291,22 @@ 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. // -// 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. +// - RegularGas: the child's leftover regular gas flows back to the caller. +// - UsedRegularGas: the child's own gross regular-gas consumption is added +// to the caller's accumulator. Combined with the caller having NOT +// pre-bumped UsedRegularGas by the forwarded amount, this matches the +// spec's escrow_subcall_regular_gas + incorporate_child_* pattern: only +// opcode charges count towards regular_gas_used, never state-gas +// spillover or escrowed forwards. +// - StateGas: the reservoir is overwritten by the child's leftover (spec's +// incorporate_child_on_success / on_error formula for state_gas_left). +// - UsedStateGas: the child's signed net state-gas usage is added to the +// caller's accumulator (spec's incorporate child gas). func (g *GasBudget) Absorb(child GasBudget) { - g.UsedRegularGas -= child.RegularGas g.RegularGas += child.RegularGas + g.UsedRegularGas += child.UsedRegularGas g.StateGas = child.StateGas g.UsedStateGas += child.UsedStateGas } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index d85fea5ee6..417c7efab9 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -743,6 +743,12 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if !value.IsZero() { gas += params.CallStipend } + // Escrow pattern (spec: escrow_subcall_regular_gas): undo the full + // forwarded gas — including the value-transfer stipend — from + // UsedRegularGas. The stipend is a free loan to the child and must + // not count toward the caller's regular gas usage; the child's own + // opcode charges are re-added via Absorb on return. + scope.Contract.Gas.UsedRegularGas -= gas // Regular gas for the forward was already pre-deducted by the dynamic // gas table (see makeCallVariantGasCallEIP*); only the state reservoir @@ -762,18 +768,10 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } scope.Contract.refundGas(result, 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. - // - // The state-gas should only be refunded if the state creation doesn't - // happens, such as ErrDepth, ErrInsufficientBalance. - // - // TODO(rjl) it's so ugly, please rework it. - if evm.chainRules.IsAmsterdam && err != nil { - if (err == ErrDepth || err == ErrInsufficientBalance) && !value.IsZero() && evm.StateDB.Empty(toAddr) { - scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeStateGasRefund) - } - } + // EIP-8037: CALL does not refund the new-account state_gas on + // ErrDepth/ErrInsufficientBalance — the spec only reclaims the forwarded + // reservoir; the state_gas charged for new account creation stays in + // state_gas_used (see Amsterdam spec call() / generic_call()). evm.returnData = ret return ret, nil @@ -794,6 +792,9 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if !value.IsZero() { gas += params.CallStipend } + // Escrow pattern: undo the full forwarded gas (including stipend) from + // UsedRegularGas; the child's own opcode charges come back via Absorb. + scope.Contract.Gas.UsedRegularGas -= gas childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) ret, result, err := evm.CallCode(scope.Contract.Address(), toAddr, args, childBudget, &value) @@ -825,6 +826,8 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) + // Undo the forwarded gas from UsedRegularGas to prevent double-charging. + scope.Contract.Gas.UsedRegularGas -= gas childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) ret, result, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, childBudget, scope.Contract.value) if err != nil { @@ -854,6 +857,8 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) + // Undo the forwarded gas from UsedRegularGas to prevent double-charging. + scope.Contract.Gas.UsedRegularGas -= gas childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) ret, result, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, childBudget) if err != nil {