Claude fixes for Glam-6

- Refund state gas on failing call
- Keep balance on selfdestruct to self
This commit is contained in:
Marius van der Wijden 2026-06-24 17:03:55 +02:00
parent 3523721bd4
commit dd073df1cd
6 changed files with 65 additions and 37 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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:
//

View file

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