core/vm: use up gas _after_ the call

This commit is contained in:
MariusVanDerWijden 2026-05-21 16:14:49 +02:00
parent bf69a15de6
commit 6e4f5710f9
No known key found for this signature in database
2 changed files with 31 additions and 24 deletions

View file

@ -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
}

View file

@ -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 {