diff --git a/core/state_transition.go b/core/state_transition.go index 662257f951..c764119dde 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -737,7 +737,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // If the contract creation failed, or the destination was pre-existing, // refund the account-creation state gas pre-charged in IntrinsicGas. if rules.IsAmsterdam && !creation { - st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + st.gasRemaining.RefundStateToReservoir(params.AccountCreationSize * st.evm.Context.CostPerStateByte) } } else { // Increment the nonce for the next transaction. @@ -753,6 +753,13 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // performing the resolution and warming. if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok { st.state.AddAddressToAccessList(addr) + // The spec resolves the delegated code in process_message_call, + // before the top-frame recipient charges: the delegated account is + // read (and recorded in the block access list) even if the cold + // access charge below runs out of gas. + if rules.IsAmsterdam { + st.state.GetCode(addr) + } } // EIP-2780: charge the transaction's top-level recipient costs. If the // budget cannot cover the charge, the top frame halts out of gas. @@ -966,7 +973,7 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se authority, err := st.validateAuthorization(auth) if err != nil { if rules.IsAmsterdam { - st.gasRemaining.RefundState((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte) + st.gasRemaining.RefundStateToReservoir((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte) st.state.AddRefund(params.AccountWriteAmsterdam) } return err @@ -979,7 +986,7 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se } } else { if st.state.Exist(authority) { - st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + st.gasRemaining.RefundStateToReservoir(params.AccountCreationSize * st.evm.Context.CostPerStateByte) st.state.AddRefund(params.AccountWriteAmsterdam) } authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte @@ -991,17 +998,17 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se } if auth.Address == (common.Address{}) { // Clearing writes no indicator, refill this auth's state charge. - st.gasRemaining.RefundState(authBase) + st.gasRemaining.RefundStateToReservoir(authBase) // The indicator was created by an earlier auth within the same // transaction, refill the state charge as it's no longer justified. if curDelegated && !preDelegated { - st.gasRemaining.RefundState(authBase) + st.gasRemaining.RefundStateToReservoir(authBase) } } else if curDelegated || preDelegated { // The 23-byte slot is already occupied, overwriting it writes no // new bytes, refill the state charge. - st.gasRemaining.RefundState(authBase) + st.gasRemaining.RefundStateToReservoir(authBase) } } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 10b8eeb033..76a2c7af60 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -196,6 +196,22 @@ func (g *GasBudget) RefundState(s uint64) { g.UsedStateGas -= int64(s) } +// RefundStateToReservoir credits a state-gas refund directly to the +// reservoir, without repaying spilled regular gas first. +// +// Per the spec's set_delegation, authorization refunds (and the post-create +// new-account refund) are added to message.state_gas_reservoir directly, in +// contrast to the LIFO inline refunds handled by RefundState. The usage +// counter is decremented by the full amount, matching the spec's +// tx_state_gas = intrinsic_state + state_gas_used - state_refund and +// preserving the per-frame invariant: +// +// StateGas + UsedStateGas == initialStateGas + Spilled +func (g *GasBudget) RefundStateToReservoir(s uint64) { + g.StateGas += s + g.UsedStateGas -= int64(s) +} + // 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 diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 111cc6a00c..103df64f5f 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -415,6 +415,14 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc, coldCost uint if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } + // The spec checks the delegation cost together with the memory + // expansion and transfer costs before reading the delegated account. + // Since intrinsicCost is only checked (not charged) above, re-check it + // against the remaining gas so the delegated address is not recorded + // in the block access list when the combined costs are unaffordable. + if contract.Gas.RegularGas < intrinsicCost { + return GasCosts{}, ErrOutOfGas + } // The delegated address has passed its gas check; record it in the // block access list now, before the call's sender-balance and // call-stack-depth checks.