diff --git a/core/state/state_object.go b/core/state/state_object.go index ce456e7668..4bf26ae293 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -122,6 +122,28 @@ func (s *stateObject) markSelfdestructed() { s.selfDestructed = true } +// clearPreservingBalance resets the account's nonce, code, and storage while +// keeping its balance, mirroring the spec's clear_account_preserving_balance +// (EIP-8246: SELFDESTRUCT no longer burns the balance). It also revokes the +// self-destruct and new-contract flags so the cleared account is finalised as +// an ordinary balance-only account rather than being deleted. Only valid for +// EIP-6780-eligible (same-transaction) accounts, whose pre-transaction nonce, +// code, and storage are empty, so discarding the in-transaction writes leaves +// the persistent trie untouched. +func (s *stateObject) clearPreservingBalance() { + s.data.Nonce = 0 + s.data.CodeHash = types.EmptyCodeHash.Bytes() + s.data.Root = types.EmptyRootHash + s.code = nil + s.dirtyCode = false + s.originStorage = make(Storage) + s.dirtyStorage = make(Storage) + s.pendingStorage = make(Storage) + s.uncommittedStorage = make(Storage) + s.selfDestructed = false + s.newContract = false +} + func (s *stateObject) touch() { s.db.journal.touchChange(s.address) } diff --git a/core/state/statedb.go b/core/state/statedb.go index 4d4e41bd67..8e7962e147 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -829,6 +829,14 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccess // finalise or delete, so ignore it here. continue } + // EIP-8246 (Amsterdam, gated by BAL being enabled): a self-destructed + // same-transaction account that still holds balance is not deleted. + // Instead its nonce, code, and storage are cleared while the balance is + // preserved, so it survives as a balance-only account. The cleared + // object then falls through to the ordinary update path below. + if obj.selfDestructed && s.stateAccessList != nil && !obj.Balance().IsZero() { + obj.clearPreservingBalance() + } if obj.selfDestructed || (deleteEmptyObjects && obj.empty()) { delete(s.stateObjects, obj.address) s.markDelete(addr) diff --git a/core/vm/evm.go b/core/vm/evm.go index 8fd02a09ac..af0ad9b5b7 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -121,6 +121,12 @@ type EVM struct { // applied in opCall*. callGasTemp uint64 + // callNewAccountChargedTemp records whether the current CALL charged the + // EIP-8037 NEW_ACCOUNT state gas (value transfer to an empty account). It is + // set in gasCallIntrinsic and read in opCall to refund that charge in LIFO + // order when the sub-call fails (spec generic_call credit_state_gas_refund). + callNewAccountChargedTemp bool + // precompiles holds the precompiled contracts for the current epoch precompiles map[common.Address]PrecompiledContract @@ -587,8 +593,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value contract.SetCallCode(common.Hash{}, code) contract.IsDeployment = true - var depositHalt bool - ret, depositHalt, err = evm.initNewContract(contract, address) + ret, _, err = evm.initNewContract(contract, address) // Special case: ErrCodeStoreOutOfGas pre-Homestead does NOT roll back // state and gas is preserved (i.e., treated as success). @@ -596,14 +601,10 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value evm.StateDB.RevertToSnapshot(snapshot) // EIP-8037: a code-deposit halt (initcode body succeeded, deposit step - // failed) keeps the state gas the body consumed for discard at the tx - // level, rather than refunding the reservoir like a mid-execution halt. - var exit GasBudget - if depositHalt && evm.chainRules.IsAmsterdam { - exit = contract.Gas.ExitCodeDepositHalt() - } else { - exit = contract.Gas.Exit(err) - } + // failed) is metered as an ordinary exceptional halt — the spec's + // process_create_message exception handler runs refill_frame_state_gas + // then burns the remaining regular gas, exactly like ExitHalt. + exit := contract.Gas.Exit(err) if err != ErrExecutionReverted { if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution) diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 061fd2d365..2de744cfa0 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -514,6 +514,7 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m // reservoir, spilling into regular gas only when the reservoir is // exhausted, mirroring the spec's inline charge_state_gas in // system.call. + evm.callNewAccountChargedTemp = false if transfersValue && evm.StateDB.Empty(address) { stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte regularAfterCall := contract.Gas.RegularGas - gas @@ -523,6 +524,7 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m if !contract.chargeState(stateGas, evm.Config.Tracer, tracing.GasChangeAccountCreation) { return 0, ErrOutOfGas } + evm.callNewAccountChargedTemp = true } return gas, nil } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index fd33329a67..e18da5775a 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -61,10 +61,10 @@ func (g GasCosts) String() string { // - At absorption: the caller's Absorb method merges the child's leftover // budget into its own running budget. type GasBudget struct { - RegularGas uint64 // remaining regular-gas balance (or leftover for caller to absorb) - StateGas uint64 // remaining state-gas reservoir (or leftover for caller to absorb) - UsedRegularGas uint64 // gross regular gas consumed in this frame - UsedStateGas int64 // signed net state-gas consumed in this frame + RegularGas uint64 // remaining regular-gas balance (or leftover for caller to absorb) + StateGas uint64 // remaining state-gas reservoir (or leftover for caller to absorb) + UsedRegularGas uint64 // gross regular gas consumed in this frame + UsedStateGas int64 // signed net state-gas consumed in this frame SpilledStateGas uint64 // state gas that spilled from the reservoir into regular gas in this frame } @@ -264,26 +264,6 @@ func (g GasBudget) ExitHalt() GasBudget { } } -// ExitCodeDepositHalt produces the leftover for a CREATE/CREATE2 frame whose -// initcode body ran to completion but then failed during the code-deposit step -// (oversized code, 0xEF prefix, or insufficient gas for the hash/deposit -// charge). Per the spec's process_create_message exception handler, this path -// differs from a mid-execution ExceptionalHalt: only the remaining regular gas -// is burned, while the state gas the (successful) body consumed is KEPT in -// UsedStateGas rather than refunded to the reservoir. The state changes are -// reverted by the caller, but the state-gas accounting is propagated upward so -// the top-level tx settlement can discard it as the state dimension (rather -// than folding it back into the combined balance, which would wrongly deflate -// the regular dimension). -func (g GasBudget) ExitCodeDepositHalt() GasBudget { - return GasBudget{ - RegularGas: 0, - StateGas: g.StateGas, - UsedRegularGas: g.UsedRegularGas + g.RegularGas, - UsedStateGas: g.UsedStateGas, - } -} - // Exit dispatches on err to the appropriate exit-form constructor // for the post-evm.Run path: // diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 2c2e78a8bd..6b86dfb6c6 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -741,6 +741,8 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // We can use this as a temporary value temp := stack.pop() gas := evm.callGasTemp + // Capture before the sub-call: nested CALLs would overwrite the flag. + newAccountCharged := evm.callNewAccountChargedTemp // Pop other call parameters. addr, value, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop() toAddr := common.Address(addr.Bytes20()) @@ -772,6 +774,13 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + // EIP-8037: when a value-bearing CALL to an empty account fails, no account + // is created, so refund the NEW_ACCOUNT state gas in LIFO order (spec + // generic_call credit_state_gas_refund on child error). + if evm.chainRules.IsAmsterdam && err != nil && newAccountCharged { + scope.Contract.Gas.CreditStateRefund(params.AccountCreationSize * evm.Context.CostPerStateByte) + } + evm.returnData = ret return ret, nil } @@ -941,12 +950,18 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro beneficiary = common.Address(top.Bytes20()) newContract = evm.StateDB.IsNewContract(this) ) - // Contract is new and will actually be deleted. + // Contract is new and is eligible for deletion (EIP-6780). if newContract { - if this != beneficiary { // Skip no-op transfer when self-destructing to self. + if this != beneficiary { + // Transfer the balance to a distinct beneficiary. evm.StateDB.AddBalance(beneficiary, balance, tracing.BalanceIncreaseSelfdestruct) + evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) + } else if !evm.chainRules.IsAmsterdam { + // Pre-EIP-8246: self-destructing to self burns the balance. + // EIP-8246 (Amsterdam) preserves it instead; the account is cleared + // preserving balance at the transaction boundary (StateDB.Finalise). + evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) } - evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) evm.StateDB.SelfDestruct(this) }