mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-02-26 15:47:21 +00:00
core: invoke selfdestruct tracer hooks during finalisation (#32919)
The core part of this PR that we need to adopt is to move the code and nonce change hook invocations to occur at tx finalization, instead of when the selfdestruct opcode is called. Additionally: * remove `SelfDestruct6780` now that it is essentially the same as `SelfDestruct` just gated by `is new contract` * don't duplicate `BalanceIncreaseSelfdestruct` (transfer to recipient of selfdestruct) in the hooked statedb and in the opcode handler for the selfdestruct opcode. * balance is burned immediately when the beneficiary of the selfdestruct is the sender, and the contract was created in the same transaction. Previously we emit two balance increases to the recipient (see above point), and a balance decrease from the sender. --------- Co-authored-by: Sina Mahmoodi <itz.s1na@gmail.com> Co-authored-by: Gary Rong <garyrong0905@gmail.com> Co-authored-by: lightclient <lightclient@protonmail.com>
This commit is contained in:
parent
b6fb79cdf9
commit
715bf8e81e
15 changed files with 918 additions and 99 deletions
|
|
@ -509,21 +509,13 @@ func (s *StateDB) SetStorage(addr common.Address, storage map[common.Hash]common
|
|||
}
|
||||
|
||||
// SelfDestruct marks the given account as selfdestructed.
|
||||
// This clears the account balance.
|
||||
//
|
||||
// The account's state object is still available until the state is committed,
|
||||
// getStateObject will return a non-nil account after SelfDestruct.
|
||||
func (s *StateDB) SelfDestruct(addr common.Address) uint256.Int {
|
||||
func (s *StateDB) SelfDestruct(addr common.Address) {
|
||||
stateObject := s.getStateObject(addr)
|
||||
var prevBalance uint256.Int
|
||||
if stateObject == nil {
|
||||
return prevBalance
|
||||
}
|
||||
prevBalance = *(stateObject.Balance())
|
||||
// Regardless of whether it is already destructed or not, we do have to
|
||||
// journal the balance-change, if we set it to zero here.
|
||||
if !stateObject.Balance().IsZero() {
|
||||
stateObject.SetBalance(new(uint256.Int))
|
||||
return
|
||||
}
|
||||
// If it is already marked as self-destructed, we do not need to add it
|
||||
// for journalling a second time.
|
||||
|
|
@ -531,18 +523,6 @@ func (s *StateDB) SelfDestruct(addr common.Address) uint256.Int {
|
|||
s.journal.destruct(addr)
|
||||
stateObject.markSelfdestructed()
|
||||
}
|
||||
return prevBalance
|
||||
}
|
||||
|
||||
func (s *StateDB) SelfDestruct6780(addr common.Address) (uint256.Int, bool) {
|
||||
stateObject := s.getStateObject(addr)
|
||||
if stateObject == nil {
|
||||
return uint256.Int{}, false
|
||||
}
|
||||
if stateObject.newContract {
|
||||
return s.SelfDestruct(addr), true
|
||||
}
|
||||
return *(stateObject.Balance()), false
|
||||
}
|
||||
|
||||
// SetTransientState sets transient storage for a given account. It
|
||||
|
|
@ -670,6 +650,16 @@ func (s *StateDB) CreateContract(addr common.Address) {
|
|||
}
|
||||
}
|
||||
|
||||
// IsNewContract reports whether the contract at the given address was deployed
|
||||
// during the current transaction.
|
||||
func (s *StateDB) IsNewContract(addr common.Address) bool {
|
||||
obj := s.getStateObject(addr)
|
||||
if obj == nil {
|
||||
return false
|
||||
}
|
||||
return obj.newContract
|
||||
}
|
||||
|
||||
// Copy creates a deep, independent copy of the state.
|
||||
// Snapshots of the copied state cannot be applied to the copy.
|
||||
func (s *StateDB) Copy() *StateDB {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ func (s *hookedStateDB) CreateContract(addr common.Address) {
|
|||
s.inner.CreateContract(addr)
|
||||
}
|
||||
|
||||
func (s *hookedStateDB) IsNewContract(addr common.Address) bool {
|
||||
return s.inner.IsNewContract(addr)
|
||||
}
|
||||
|
||||
func (s *hookedStateDB) GetBalance(addr common.Address) *uint256.Int {
|
||||
return s.inner.GetBalance(addr)
|
||||
}
|
||||
|
|
@ -211,56 +215,8 @@ func (s *hookedStateDB) SetState(address common.Address, key common.Hash, value
|
|||
return prev
|
||||
}
|
||||
|
||||
func (s *hookedStateDB) SelfDestruct(address common.Address) uint256.Int {
|
||||
var prevCode []byte
|
||||
var prevCodeHash common.Hash
|
||||
|
||||
if s.hooks.OnCodeChange != nil || s.hooks.OnCodeChangeV2 != nil {
|
||||
prevCode = s.inner.GetCode(address)
|
||||
prevCodeHash = s.inner.GetCodeHash(address)
|
||||
}
|
||||
|
||||
prev := s.inner.SelfDestruct(address)
|
||||
|
||||
if s.hooks.OnBalanceChange != nil && !prev.IsZero() {
|
||||
s.hooks.OnBalanceChange(address, prev.ToBig(), new(big.Int), tracing.BalanceDecreaseSelfdestruct)
|
||||
}
|
||||
|
||||
if len(prevCode) > 0 {
|
||||
if s.hooks.OnCodeChangeV2 != nil {
|
||||
s.hooks.OnCodeChangeV2(address, prevCodeHash, prevCode, types.EmptyCodeHash, nil, tracing.CodeChangeSelfDestruct)
|
||||
} else if s.hooks.OnCodeChange != nil {
|
||||
s.hooks.OnCodeChange(address, prevCodeHash, prevCode, types.EmptyCodeHash, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return prev
|
||||
}
|
||||
|
||||
func (s *hookedStateDB) SelfDestruct6780(address common.Address) (uint256.Int, bool) {
|
||||
var prevCode []byte
|
||||
var prevCodeHash common.Hash
|
||||
|
||||
if s.hooks.OnCodeChange != nil || s.hooks.OnCodeChangeV2 != nil {
|
||||
prevCodeHash = s.inner.GetCodeHash(address)
|
||||
prevCode = s.inner.GetCode(address)
|
||||
}
|
||||
|
||||
prev, changed := s.inner.SelfDestruct6780(address)
|
||||
|
||||
if s.hooks.OnBalanceChange != nil && !prev.IsZero() {
|
||||
s.hooks.OnBalanceChange(address, prev.ToBig(), new(big.Int), tracing.BalanceDecreaseSelfdestruct)
|
||||
}
|
||||
|
||||
if changed && len(prevCode) > 0 {
|
||||
if s.hooks.OnCodeChangeV2 != nil {
|
||||
s.hooks.OnCodeChangeV2(address, prevCodeHash, prevCode, types.EmptyCodeHash, nil, tracing.CodeChangeSelfDestruct)
|
||||
} else if s.hooks.OnCodeChange != nil {
|
||||
s.hooks.OnCodeChange(address, prevCodeHash, prevCode, types.EmptyCodeHash, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return prev, changed
|
||||
func (s *hookedStateDB) SelfDestruct(address common.Address) {
|
||||
s.inner.SelfDestruct(address)
|
||||
}
|
||||
|
||||
func (s *hookedStateDB) AddLog(log *types.Log) {
|
||||
|
|
@ -272,17 +228,47 @@ func (s *hookedStateDB) AddLog(log *types.Log) {
|
|||
}
|
||||
|
||||
func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) {
|
||||
defer s.inner.Finalise(deleteEmptyObjects)
|
||||
if s.hooks.OnBalanceChange == nil {
|
||||
if s.hooks.OnBalanceChange == nil && s.hooks.OnNonceChangeV2 == nil && s.hooks.OnNonceChange == nil && s.hooks.OnCodeChangeV2 == nil && s.hooks.OnCodeChange == nil {
|
||||
// Short circuit if no relevant hooks are set.
|
||||
s.inner.Finalise(deleteEmptyObjects)
|
||||
return
|
||||
}
|
||||
|
||||
// Iterate all dirty addresses and record self-destructs.
|
||||
for addr := range s.inner.journal.dirties {
|
||||
obj := s.inner.stateObjects[addr]
|
||||
if obj != nil && obj.selfDestructed {
|
||||
// If ether was sent to account post-selfdestruct it is burnt.
|
||||
if obj == nil || !obj.selfDestructed {
|
||||
// Not self-destructed, keep searching.
|
||||
continue
|
||||
}
|
||||
// Bingo: state object was self-destructed, call relevant hooks.
|
||||
|
||||
// If ether was sent to account post-selfdestruct, record as burnt.
|
||||
if s.hooks.OnBalanceChange != nil {
|
||||
if bal := obj.Balance(); bal.Sign() != 0 {
|
||||
s.hooks.OnBalanceChange(addr, bal.ToBig(), new(big.Int), tracing.BalanceDecreaseSelfdestructBurn)
|
||||
}
|
||||
}
|
||||
|
||||
// Nonce is set to reset on self-destruct.
|
||||
if s.hooks.OnNonceChangeV2 != nil {
|
||||
s.hooks.OnNonceChangeV2(addr, obj.Nonce(), 0, tracing.NonceChangeSelfdestruct)
|
||||
} else if s.hooks.OnNonceChange != nil {
|
||||
s.hooks.OnNonceChange(addr, obj.Nonce(), 0)
|
||||
}
|
||||
|
||||
// If an initcode invokes selfdestruct, do not emit a code change.
|
||||
prevCodeHash := s.inner.GetCodeHash(addr)
|
||||
if prevCodeHash == types.EmptyCodeHash {
|
||||
continue
|
||||
}
|
||||
// Otherwise, trace the change.
|
||||
if s.hooks.OnCodeChangeV2 != nil {
|
||||
s.hooks.OnCodeChangeV2(addr, prevCodeHash, s.inner.GetCode(addr), types.EmptyCodeHash, nil, tracing.CodeChangeSelfDestruct)
|
||||
} else if s.hooks.OnCodeChange != nil {
|
||||
s.hooks.OnCodeChange(addr, prevCodeHash, s.inner.GetCode(addr), types.EmptyCodeHash, nil)
|
||||
}
|
||||
}
|
||||
|
||||
s.inner.Finalise(deleteEmptyObjects)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ func TestBurn(t *testing.T) {
|
|||
createAndDestroy := func(addr common.Address) {
|
||||
hooked.AddBalance(addr, uint256.NewInt(100), tracing.BalanceChangeUnspecified)
|
||||
hooked.CreateContract(addr)
|
||||
// Simulate what the opcode handler does: clear balance before selfdestruct
|
||||
hooked.SubBalance(addr, hooked.GetBalance(addr), tracing.BalanceDecreaseSelfdestruct)
|
||||
hooked.SelfDestruct(addr)
|
||||
// sanity-check that balance is now 0
|
||||
if have, want := hooked.GetBalance(addr), new(uint256.Int); !have.Eq(want) {
|
||||
|
|
@ -140,8 +142,8 @@ func TestHooks_OnCodeChangeV2(t *testing.T) {
|
|||
var result []string
|
||||
var wants = []string{
|
||||
"0xaa00000000000000000000000000000000000000.code: (0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470) ->0x1325 (0xa12ae05590de0c93a00bc7ac773c2fdb621e44f814985e72194f921c0050f728) ContractCreation",
|
||||
"0xaa00000000000000000000000000000000000000.code: 0x1325 (0xa12ae05590de0c93a00bc7ac773c2fdb621e44f814985e72194f921c0050f728) -> (0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470) SelfDestruct",
|
||||
"0xbb00000000000000000000000000000000000000.code: (0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470) ->0x1326 (0x3c54516221d604e623f358bc95996ca3242aaa109bddabcebda13db9b3f90dcb) ContractCreation",
|
||||
"0xaa00000000000000000000000000000000000000.code: 0x1325 (0xa12ae05590de0c93a00bc7ac773c2fdb621e44f814985e72194f921c0050f728) -> (0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470) SelfDestruct",
|
||||
"0xbb00000000000000000000000000000000000000.code: 0x1326 (0x3c54516221d604e623f358bc95996ca3242aaa109bddabcebda13db9b3f90dcb) -> (0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470) SelfDestruct",
|
||||
}
|
||||
emitF := func(format string, a ...any) {
|
||||
|
|
@ -157,7 +159,8 @@ func TestHooks_OnCodeChangeV2(t *testing.T) {
|
|||
|
||||
sdb.SetCode(common.Address{0xbb}, []byte{0x13, 38}, tracing.CodeChangeContractCreation)
|
||||
sdb.CreateContract(common.Address{0xbb})
|
||||
sdb.SelfDestruct6780(common.Address{0xbb})
|
||||
sdb.SelfDestruct(common.Address{0xbb})
|
||||
sdb.Finalise(true)
|
||||
|
||||
if len(result) != len(wants) {
|
||||
t.Fatalf("number of tracing events wrong, have %d want %d", len(result), len(wants))
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@ func _() {
|
|||
_ = x[NonceChangeNewContract-4]
|
||||
_ = x[NonceChangeAuthorization-5]
|
||||
_ = x[NonceChangeRevert-6]
|
||||
_ = x[NonceChangeSelfdestruct-7]
|
||||
}
|
||||
|
||||
const _NonceChangeReason_name = "UnspecifiedGenesisEoACallContractCreatorNewContractAuthorizationRevert"
|
||||
const _NonceChangeReason_name = "UnspecifiedGenesisEoACallContractCreatorNewContractAuthorizationRevertSelfdestruct"
|
||||
|
||||
var _NonceChangeReason_index = [...]uint8{0, 11, 18, 25, 40, 51, 64, 70}
|
||||
var _NonceChangeReason_index = [...]uint8{0, 11, 18, 25, 40, 51, 64, 70, 82}
|
||||
|
||||
func (i NonceChangeReason) String() string {
|
||||
if i >= NonceChangeReason(len(_NonceChangeReason_index)-1) {
|
||||
|
|
|
|||
|
|
@ -432,6 +432,9 @@ const (
|
|||
// NonceChangeRevert is emitted when the nonce is reverted back to a previous value due to call failure.
|
||||
// It is only emitted when the tracer has opted in to use the journaling wrapper (WrapWithJournal).
|
||||
NonceChangeRevert NonceChangeReason = 6
|
||||
|
||||
// NonceChangeSelfdestruct is emitted when the nonce is reset to zero due to a self-destruct
|
||||
NonceChangeSelfdestruct NonceChangeReason = 7
|
||||
)
|
||||
|
||||
// CodeChangeReason is used to indicate the reason for a code change.
|
||||
|
|
|
|||
|
|
@ -876,13 +876,25 @@ func opStop(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
}
|
||||
|
||||
func opSelfdestruct(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
||||
beneficiary := scope.Stack.pop()
|
||||
balance := evm.StateDB.GetBalance(scope.Contract.Address())
|
||||
evm.StateDB.AddBalance(beneficiary.Bytes20(), balance, tracing.BalanceIncreaseSelfdestruct)
|
||||
evm.StateDB.SelfDestruct(scope.Contract.Address())
|
||||
var (
|
||||
this = scope.Contract.Address()
|
||||
balance = evm.StateDB.GetBalance(this)
|
||||
top = scope.Stack.pop()
|
||||
beneficiary = common.Address(top.Bytes20())
|
||||
)
|
||||
|
||||
// The funds are burned immediately if the beneficiary is the caller itself,
|
||||
// in this case, the beneficiary's balance is not increased.
|
||||
if this != beneficiary {
|
||||
evm.StateDB.AddBalance(beneficiary, balance, tracing.BalanceIncreaseSelfdestruct)
|
||||
}
|
||||
// Clear any leftover funds for the account being destructed.
|
||||
evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct)
|
||||
evm.StateDB.SelfDestruct(this)
|
||||
|
||||
if tracer := evm.Config.Tracer; tracer != nil {
|
||||
if tracer.OnEnter != nil {
|
||||
tracer.OnEnter(evm.depth, byte(SELFDESTRUCT), scope.Contract.Address(), beneficiary.Bytes20(), []byte{}, 0, balance.ToBig())
|
||||
tracer.OnEnter(evm.depth, byte(SELFDESTRUCT), this, beneficiary, []byte{}, 0, balance.ToBig())
|
||||
}
|
||||
if tracer.OnExit != nil {
|
||||
tracer.OnExit(evm.depth, []byte{}, 0, nil, false)
|
||||
|
|
@ -892,14 +904,33 @@ func opSelfdestruct(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
|||
}
|
||||
|
||||
func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
|
||||
beneficiary := scope.Stack.pop()
|
||||
balance := evm.StateDB.GetBalance(scope.Contract.Address())
|
||||
evm.StateDB.SubBalance(scope.Contract.Address(), balance, tracing.BalanceDecreaseSelfdestruct)
|
||||
evm.StateDB.AddBalance(beneficiary.Bytes20(), balance, tracing.BalanceIncreaseSelfdestruct)
|
||||
evm.StateDB.SelfDestruct6780(scope.Contract.Address())
|
||||
var (
|
||||
this = scope.Contract.Address()
|
||||
balance = evm.StateDB.GetBalance(this)
|
||||
top = scope.Stack.pop()
|
||||
beneficiary = common.Address(top.Bytes20())
|
||||
|
||||
newContract = evm.StateDB.IsNewContract(this)
|
||||
)
|
||||
|
||||
// Contract is new and will actually be deleted.
|
||||
if newContract {
|
||||
if this != beneficiary { // Skip no-op transfer when self-destructing to self.
|
||||
evm.StateDB.AddBalance(beneficiary, balance, tracing.BalanceIncreaseSelfdestruct)
|
||||
}
|
||||
evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct)
|
||||
evm.StateDB.SelfDestruct(this)
|
||||
}
|
||||
|
||||
// Contract already exists, only do transfer if beneficiary is not self.
|
||||
if !newContract && this != beneficiary {
|
||||
evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct)
|
||||
evm.StateDB.AddBalance(beneficiary, balance, tracing.BalanceIncreaseSelfdestruct)
|
||||
}
|
||||
|
||||
if tracer := evm.Config.Tracer; tracer != nil {
|
||||
if tracer.OnEnter != nil {
|
||||
tracer.OnEnter(evm.depth, byte(SELFDESTRUCT), scope.Contract.Address(), beneficiary.Bytes20(), []byte{}, 0, balance.ToBig())
|
||||
tracer.OnEnter(evm.depth, byte(SELFDESTRUCT), this, beneficiary, []byte{}, 0, balance.ToBig())
|
||||
}
|
||||
if tracer.OnExit != nil {
|
||||
tracer.OnExit(evm.depth, []byte{}, 0, nil, false)
|
||||
|
|
|
|||
|
|
@ -57,19 +57,17 @@ type StateDB interface {
|
|||
GetTransientState(addr common.Address, key common.Hash) common.Hash
|
||||
SetTransientState(addr common.Address, key, value common.Hash)
|
||||
|
||||
SelfDestruct(common.Address) uint256.Int
|
||||
SelfDestruct(common.Address)
|
||||
HasSelfDestructed(common.Address) bool
|
||||
|
||||
// SelfDestruct6780 is post-EIP6780 selfdestruct, which means that it's a
|
||||
// send-all-to-beneficiary, unless the contract was created in this same
|
||||
// transaction, in which case it will be destructed.
|
||||
// This method returns the prior balance, along with a boolean which is
|
||||
// true iff the object was indeed destructed.
|
||||
SelfDestruct6780(common.Address) (uint256.Int, bool)
|
||||
|
||||
// Exist reports whether the given account exists in state.
|
||||
// Notably this also returns true for self-destructed accounts within the current transaction.
|
||||
Exist(common.Address) bool
|
||||
|
||||
// IsNewContract reports whether the contract at the given address was deployed
|
||||
// during the current transaction.
|
||||
IsNewContract(addr common.Address) bool
|
||||
|
||||
// Empty returns whether the given account is empty. Empty
|
||||
// is defined according to EIP161 (balance = nonce = code = 0).
|
||||
Empty(common.Address) bool
|
||||
|
|
|
|||
653
eth/tracers/internal/tracetest/selfdestruct_state_test.go
Normal file
653
eth/tracers/internal/tracetest/selfdestruct_state_test.go
Normal file
|
|
@ -0,0 +1,653 @@
|
|||
// Copyright 2025 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package tracetest
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/consensus"
|
||||
"github.com/ethereum/go-ethereum/consensus/beacon"
|
||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/tracing"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
// accountState represents the expected final state of an account
|
||||
type accountState struct {
|
||||
Balance *big.Int
|
||||
Nonce uint64
|
||||
Code []byte
|
||||
Exists bool
|
||||
}
|
||||
|
||||
// selfdestructStateTracer tracks state changes during selfdestruct operations
|
||||
type selfdestructStateTracer struct {
|
||||
env *tracing.VMContext
|
||||
accounts map[common.Address]*accountState
|
||||
}
|
||||
|
||||
func newSelfdestructStateTracer() *selfdestructStateTracer {
|
||||
return &selfdestructStateTracer{
|
||||
accounts: make(map[common.Address]*accountState),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *selfdestructStateTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) {
|
||||
t.env = env
|
||||
}
|
||||
|
||||
func (t *selfdestructStateTracer) OnTxEnd(receipt *types.Receipt, err error) {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
func (t *selfdestructStateTracer) getOrCreateAccount(addr common.Address) *accountState {
|
||||
if acc, ok := t.accounts[addr]; ok {
|
||||
return acc
|
||||
}
|
||||
|
||||
// Initialize with current state from statedb
|
||||
acc := &accountState{
|
||||
Balance: t.env.StateDB.GetBalance(addr).ToBig(),
|
||||
Nonce: t.env.StateDB.GetNonce(addr),
|
||||
Code: t.env.StateDB.GetCode(addr),
|
||||
Exists: t.env.StateDB.Exist(addr),
|
||||
}
|
||||
t.accounts[addr] = acc
|
||||
return acc
|
||||
}
|
||||
|
||||
func (t *selfdestructStateTracer) OnBalanceChange(addr common.Address, prev, new *big.Int, reason tracing.BalanceChangeReason) {
|
||||
acc := t.getOrCreateAccount(addr)
|
||||
acc.Balance = new
|
||||
}
|
||||
|
||||
func (t *selfdestructStateTracer) OnNonceChangeV2(addr common.Address, prev, new uint64, reason tracing.NonceChangeReason) {
|
||||
acc := t.getOrCreateAccount(addr)
|
||||
acc.Nonce = new
|
||||
|
||||
// If this is a selfdestruct nonce change, mark account as not existing
|
||||
if reason == tracing.NonceChangeSelfdestruct {
|
||||
acc.Exists = false
|
||||
}
|
||||
}
|
||||
|
||||
func (t *selfdestructStateTracer) OnCodeChangeV2(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte, reason tracing.CodeChangeReason) {
|
||||
acc := t.getOrCreateAccount(addr)
|
||||
acc.Code = code
|
||||
|
||||
// If this is a selfdestruct code change, mark account as not existing
|
||||
if reason == tracing.CodeChangeSelfDestruct {
|
||||
acc.Exists = false
|
||||
}
|
||||
}
|
||||
|
||||
func (t *selfdestructStateTracer) Hooks() *tracing.Hooks {
|
||||
return &tracing.Hooks{
|
||||
OnTxStart: t.OnTxStart,
|
||||
OnTxEnd: t.OnTxEnd,
|
||||
OnBalanceChange: t.OnBalanceChange,
|
||||
OnNonceChangeV2: t.OnNonceChangeV2,
|
||||
OnCodeChangeV2: t.OnCodeChangeV2,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *selfdestructStateTracer) Accounts() map[common.Address]*accountState {
|
||||
return t.accounts
|
||||
}
|
||||
|
||||
// verifyAccountState compares actual and expected account state and reports any mismatches
|
||||
func verifyAccountState(t *testing.T, addr common.Address, actual, expected *accountState) {
|
||||
if actual.Balance.Cmp(expected.Balance) != 0 {
|
||||
t.Errorf("address %s: balance mismatch: have %s, want %s",
|
||||
addr.Hex(), actual.Balance, expected.Balance)
|
||||
}
|
||||
if actual.Nonce != expected.Nonce {
|
||||
t.Errorf("address %s: nonce mismatch: have %d, want %d",
|
||||
addr.Hex(), actual.Nonce, expected.Nonce)
|
||||
}
|
||||
if len(actual.Code) != len(expected.Code) {
|
||||
t.Errorf("address %s: code length mismatch: have %d, want %d",
|
||||
addr.Hex(), len(actual.Code), len(expected.Code))
|
||||
}
|
||||
if actual.Exists != expected.Exists {
|
||||
t.Errorf("address %s: exists mismatch: have %v, want %v",
|
||||
addr.Hex(), actual.Exists, expected.Exists)
|
||||
}
|
||||
}
|
||||
|
||||
// setupTestBlockchain creates a blockchain with the given genesis and transaction,
|
||||
// returns the blockchain, the first block, and a statedb at genesis for testing
|
||||
func setupTestBlockchain(t *testing.T, genesis *core.Genesis, tx *types.Transaction, useBeacon bool) (*core.BlockChain, *types.Block, *state.StateDB) {
|
||||
var engine consensus.Engine
|
||||
if useBeacon {
|
||||
engine = beacon.New(ethash.NewFaker())
|
||||
} else {
|
||||
engine = ethash.NewFaker()
|
||||
}
|
||||
|
||||
_, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, 1, func(i int, b *core.BlockGen) {
|
||||
b.AddTx(tx)
|
||||
})
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
blockchain, err := core.NewBlockChain(db, genesis, engine, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create blockchain: %v", err)
|
||||
}
|
||||
if _, err := blockchain.InsertChain(blocks); err != nil {
|
||||
t.Fatalf("failed to insert chain: %v", err)
|
||||
}
|
||||
genesisBlock := blockchain.GetBlockByNumber(0)
|
||||
if genesisBlock == nil {
|
||||
t.Fatalf("failed to get genesis block")
|
||||
}
|
||||
statedb, err := blockchain.StateAt(genesisBlock.Root())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get state: %v", err)
|
||||
}
|
||||
|
||||
return blockchain, blocks[0], statedb
|
||||
}
|
||||
|
||||
func TestSelfdestructStateTracer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
// Gas limit high enough for all test scenarios (factory creation + multiple calls)
|
||||
testGasLimit = 500000
|
||||
|
||||
// Common balance amounts used across tests
|
||||
testBalanceInitial = 100 // Initial balance for contracts being tested
|
||||
testBalanceSent = 50 // Amount sent back in sendback tests
|
||||
testBalanceFactory = 200 // Factory needs extra balance for contract creation
|
||||
)
|
||||
|
||||
// Helper to create *big.Int for wei amounts
|
||||
wei := func(amount int64) *big.Int {
|
||||
return big.NewInt(amount)
|
||||
}
|
||||
|
||||
// Test account (transaction sender)
|
||||
var (
|
||||
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||
caller = crypto.PubkeyToAddress(key.PublicKey)
|
||||
)
|
||||
|
||||
// Simple selfdestruct test contracts
|
||||
var (
|
||||
contract = common.HexToAddress("0x00000000000000000000000000000000000000bb")
|
||||
recipient = common.HexToAddress("0x00000000000000000000000000000000000000cc")
|
||||
)
|
||||
// Build selfdestruct code: PUSH20 <recipient> SELFDESTRUCT
|
||||
selfdestructCode := []byte{byte(vm.PUSH20)}
|
||||
selfdestructCode = append(selfdestructCode, recipient.Bytes()...)
|
||||
selfdestructCode = append(selfdestructCode, byte(vm.SELFDESTRUCT))
|
||||
|
||||
// Factory test contracts (create-and-destroy pattern)
|
||||
var (
|
||||
factory = common.HexToAddress("0x00000000000000000000000000000000000000ff")
|
||||
)
|
||||
// Factory code: creates a contract with 100 wei and calls it to trigger selfdestruct back to factory
|
||||
// See selfdestruct_test_contracts/factory.yul for source
|
||||
// Runtime bytecode compiled with: solc --strict-assembly --evm-version paris factory.yul --bin
|
||||
// (Using paris to avoid PUSH0 opcode which is not available pre-Shanghai)
|
||||
var (
|
||||
factoryCode = common.Hex2Bytes("6a6133ff6000526002601ef360a81b600052600080808080600b816064f05af100")
|
||||
createdContractAddr = crypto.CreateAddress(factory, 0) // Address where factory creates the contract
|
||||
)
|
||||
|
||||
// Sendback test contracts (A→B→A pattern)
|
||||
// For the refund test: Coordinator calls A, then B
|
||||
// A selfdestructs to B, B sends funds back to A
|
||||
var (
|
||||
contractA = common.HexToAddress("0x00000000000000000000000000000000000000aa")
|
||||
contractB = common.HexToAddress("0x00000000000000000000000000000000000000bb")
|
||||
coordinator = common.HexToAddress("0x00000000000000000000000000000000000000cc")
|
||||
)
|
||||
// Contract A: if msg.value > 0, accept funds; else selfdestruct to B
|
||||
// See selfdestruct_test_contracts/contractA.yul for source
|
||||
// Runtime bytecode compiled with: solc --strict-assembly --evm-version paris contractA.yul --bin
|
||||
contractACode := common.Hex2Bytes("60003411600a5760bbff5b00")
|
||||
|
||||
// Contract B: sends 50 wei back to contract A
|
||||
// See selfdestruct_test_contracts/contractB.yul for source
|
||||
// Runtime bytecode compiled with: solc --strict-assembly --evm-version paris contractB.yul --bin
|
||||
contractBCode := common.Hex2Bytes("6000808080603260aa5af100")
|
||||
|
||||
// Coordinator: calls A (A selfdestructs to B), then calls B (B sends funds to A)
|
||||
// See selfdestruct_test_contracts/coordinator.yul for source
|
||||
// Runtime bytecode compiled with: solc --strict-assembly --evm-version paris coordinator.yul --bin
|
||||
coordinatorCode := common.Hex2Bytes("60008080808060aa818080808060bb955af1505af100")
|
||||
|
||||
// Factory for create-and-refund test: creates A with 100 wei, calls A, calls B
|
||||
// See selfdestruct_test_contracts/factoryRefund.yul for source
|
||||
// Runtime bytecode compiled with: solc --strict-assembly --evm-version paris factoryRefund.yul --bin
|
||||
var (
|
||||
factoryRefund = common.HexToAddress("0x00000000000000000000000000000000000000dd")
|
||||
factoryRefundCode = common.Hex2Bytes("60008080808060bb78600c600d600039600c6000f3fe60003411600a5760bbff5b0082528180808080601960076064f05af1505af100")
|
||||
createdContractAddrA = crypto.CreateAddress(factoryRefund, 0) // Address where factory creates contract A
|
||||
)
|
||||
|
||||
// Self-destruct-to-self test contracts
|
||||
var (
|
||||
contractSelfDestruct = common.HexToAddress("0x00000000000000000000000000000000000000aa")
|
||||
coordinatorSendAfter = common.HexToAddress("0x00000000000000000000000000000000000000ee")
|
||||
)
|
||||
// Contract that selfdestructs to self
|
||||
// See selfdestruct_test_contracts/contractSelfDestruct.yul
|
||||
contractSelfDestructCode := common.Hex2Bytes("30ff")
|
||||
|
||||
// Coordinator: calls contract (triggers selfdestruct to self), stores balance, sends 50 wei, stores balance again
|
||||
// See selfdestruct_test_contracts/coordinatorSendAfter.yul
|
||||
coordinatorSendAfterCode := common.Hex2Bytes("60aa600080808080855af150803160005560008080806032855af1503160015500")
|
||||
|
||||
// Factory with balance checking: creates contract, calls it, checks balances
|
||||
// See selfdestruct_test_contracts/factorySelfDestructBalanceCheck.yul
|
||||
var (
|
||||
factorySelfDestructBalanceCheck = common.HexToAddress("0x00000000000000000000000000000000000000fd")
|
||||
factorySelfDestructBalanceCheckCode = common.Hex2Bytes("6e6002600d60003960026000f3fe30ff600052600f60116064f0600080808080855af150803160005560008080806032855af1503160015500")
|
||||
createdContractAddrSelfBalanceCheck = crypto.CreateAddress(factorySelfDestructBalanceCheck, 0)
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
description string
|
||||
targetContract common.Address
|
||||
genesis *core.Genesis
|
||||
useBeacon bool
|
||||
expectedResults map[common.Address]accountState
|
||||
expectedStorage map[common.Address]map[uint64]*big.Int
|
||||
}{
|
||||
{
|
||||
name: "pre_6780_existing",
|
||||
description: "Pre-EIP-6780: Existing contract selfdestructs to recipient. Contract should be destroyed and balance transferred.",
|
||||
targetContract: contract,
|
||||
genesis: &core.Genesis{
|
||||
Config: params.AllEthashProtocolChanges,
|
||||
Alloc: types.GenesisAlloc{
|
||||
caller: {Balance: big.NewInt(params.Ether)},
|
||||
contract: {
|
||||
Balance: wei(testBalanceInitial),
|
||||
Code: selfdestructCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
useBeacon: false,
|
||||
expectedResults: map[common.Address]accountState{
|
||||
contract: {
|
||||
Balance: wei(0),
|
||||
Nonce: 0,
|
||||
Code: []byte{},
|
||||
Exists: false,
|
||||
},
|
||||
recipient: {
|
||||
Balance: wei(testBalanceInitial), // Received contract's balance
|
||||
Nonce: 0,
|
||||
Code: []byte{},
|
||||
Exists: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "post_6780_existing",
|
||||
description: "Post-EIP-6780: Existing contract selfdestructs to recipient. Balance transferred but contract NOT destroyed (code/storage remain).",
|
||||
targetContract: contract,
|
||||
genesis: &core.Genesis{
|
||||
Config: params.AllDevChainProtocolChanges,
|
||||
Alloc: types.GenesisAlloc{
|
||||
caller: {Balance: big.NewInt(params.Ether)},
|
||||
contract: {
|
||||
Balance: wei(testBalanceInitial),
|
||||
Code: selfdestructCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
useBeacon: true,
|
||||
expectedResults: map[common.Address]accountState{
|
||||
contract: {
|
||||
Balance: wei(0),
|
||||
Nonce: 0,
|
||||
Code: selfdestructCode,
|
||||
Exists: true,
|
||||
},
|
||||
recipient: {
|
||||
Balance: wei(testBalanceInitial),
|
||||
Nonce: 0,
|
||||
Code: []byte{},
|
||||
Exists: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pre_6780_create_destroy",
|
||||
description: "Pre-EIP-6780: Factory creates contract with 100 wei, contract selfdestructs back to factory. Contract destroyed, factory gets refund.",
|
||||
targetContract: factory,
|
||||
genesis: &core.Genesis{
|
||||
Config: params.AllEthashProtocolChanges,
|
||||
Alloc: types.GenesisAlloc{
|
||||
caller: {Balance: big.NewInt(params.Ether)},
|
||||
factory: {
|
||||
Balance: wei(testBalanceFactory),
|
||||
Code: factoryCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
useBeacon: false,
|
||||
expectedResults: map[common.Address]accountState{
|
||||
factory: {
|
||||
Balance: wei(testBalanceFactory),
|
||||
Nonce: 1,
|
||||
Code: factoryCode,
|
||||
Exists: true,
|
||||
},
|
||||
createdContractAddr: {
|
||||
Balance: wei(0),
|
||||
Nonce: 0,
|
||||
Code: []byte{},
|
||||
Exists: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "post_6780_create_destroy",
|
||||
description: "Post-EIP-6780: Factory creates contract with 100 wei, contract selfdestructs back to factory. Contract destroyed (EIP-6780 exception for same-tx creation).",
|
||||
targetContract: factory,
|
||||
genesis: &core.Genesis{
|
||||
Config: params.AllDevChainProtocolChanges,
|
||||
Alloc: types.GenesisAlloc{
|
||||
caller: {Balance: big.NewInt(params.Ether)},
|
||||
factory: {
|
||||
Balance: wei(testBalanceFactory),
|
||||
Code: factoryCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
useBeacon: true,
|
||||
expectedResults: map[common.Address]accountState{
|
||||
factory: {
|
||||
Balance: wei(testBalanceFactory),
|
||||
Nonce: 1,
|
||||
Code: factoryCode,
|
||||
Exists: true,
|
||||
},
|
||||
createdContractAddr: {
|
||||
Balance: wei(0),
|
||||
Nonce: 0,
|
||||
Code: []byte{},
|
||||
Exists: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pre_6780_sendback",
|
||||
description: "Pre-EIP-6780: Contract A selfdestructs sending funds to B, then B sends funds back to A's address. Funds sent to destroyed address are burnt.",
|
||||
targetContract: coordinator,
|
||||
genesis: &core.Genesis{
|
||||
Config: params.AllEthashProtocolChanges,
|
||||
Alloc: types.GenesisAlloc{
|
||||
caller: {Balance: big.NewInt(params.Ether)},
|
||||
contractA: {
|
||||
Balance: wei(testBalanceInitial),
|
||||
Code: contractACode,
|
||||
},
|
||||
contractB: {
|
||||
Balance: wei(0),
|
||||
Code: contractBCode,
|
||||
},
|
||||
coordinator: {
|
||||
Code: coordinatorCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
useBeacon: false,
|
||||
expectedResults: map[common.Address]accountState{
|
||||
contractA: {
|
||||
Balance: wei(0),
|
||||
Nonce: 0,
|
||||
Code: []byte{},
|
||||
Exists: false,
|
||||
},
|
||||
contractB: {
|
||||
// 100 received - 50 sent back
|
||||
Balance: wei(testBalanceSent),
|
||||
Nonce: 0,
|
||||
Code: contractBCode,
|
||||
Exists: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "post_6780_existing_sendback",
|
||||
description: "Post-EIP-6780: Existing contract A selfdestructs to B, then B sends funds back to A. Funds are NOT burnt (A still exists post-6780).",
|
||||
targetContract: coordinator,
|
||||
genesis: &core.Genesis{
|
||||
Config: params.AllDevChainProtocolChanges,
|
||||
Alloc: types.GenesisAlloc{
|
||||
caller: {Balance: big.NewInt(params.Ether)},
|
||||
contractA: {
|
||||
Balance: wei(testBalanceInitial),
|
||||
Code: contractACode,
|
||||
},
|
||||
contractB: {
|
||||
Balance: wei(0),
|
||||
Code: contractBCode,
|
||||
},
|
||||
coordinator: {
|
||||
Code: coordinatorCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
useBeacon: true,
|
||||
expectedResults: map[common.Address]accountState{
|
||||
contractA: {
|
||||
Balance: wei(testBalanceSent),
|
||||
Nonce: 0,
|
||||
Code: contractACode,
|
||||
Exists: true,
|
||||
},
|
||||
contractB: {
|
||||
Balance: wei(testBalanceSent),
|
||||
Nonce: 0,
|
||||
Code: contractBCode,
|
||||
Exists: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "post_6780_create_destroy_sendback",
|
||||
description: "Post-EIP-6780: Factory creates A, A selfdestructs to B, B sends funds back to A. Funds are burnt (A was destroyed via EIP-6780 exception).",
|
||||
targetContract: factoryRefund,
|
||||
genesis: &core.Genesis{
|
||||
Config: params.AllDevChainProtocolChanges,
|
||||
Alloc: types.GenesisAlloc{
|
||||
caller: {Balance: big.NewInt(params.Ether)},
|
||||
contractB: {
|
||||
Balance: wei(0),
|
||||
Code: contractBCode,
|
||||
},
|
||||
factoryRefund: {
|
||||
Balance: wei(testBalanceFactory),
|
||||
Code: factoryRefundCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
useBeacon: true,
|
||||
expectedResults: map[common.Address]accountState{
|
||||
createdContractAddrA: {
|
||||
// Funds sent back are burnt!
|
||||
Balance: wei(0),
|
||||
Nonce: 0,
|
||||
Code: []byte{},
|
||||
Exists: false,
|
||||
},
|
||||
contractB: {
|
||||
Balance: wei(testBalanceSent),
|
||||
Nonce: 0,
|
||||
Code: contractBCode,
|
||||
Exists: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "post_6780_existing_to_self",
|
||||
description: "Post-EIP-6780: Pre-existing contract selfdestructs to itself. Balance NOT burnt (selfdestruct-to-self is no-op for existing contracts).",
|
||||
targetContract: coordinatorSendAfter,
|
||||
genesis: &core.Genesis{
|
||||
Config: params.AllDevChainProtocolChanges,
|
||||
Alloc: types.GenesisAlloc{
|
||||
caller: {Balance: big.NewInt(params.Ether)},
|
||||
contractSelfDestruct: {
|
||||
Balance: wei(testBalanceInitial),
|
||||
Code: contractSelfDestructCode,
|
||||
},
|
||||
coordinatorSendAfter: {
|
||||
Balance: wei(testBalanceInitial),
|
||||
Code: coordinatorSendAfterCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
useBeacon: true,
|
||||
expectedResults: map[common.Address]accountState{
|
||||
contractSelfDestruct: {
|
||||
Balance: wei(150),
|
||||
Nonce: 0,
|
||||
Code: contractSelfDestructCode,
|
||||
Exists: true,
|
||||
},
|
||||
coordinatorSendAfter: {
|
||||
Balance: wei(testBalanceSent),
|
||||
Nonce: 0,
|
||||
Code: coordinatorSendAfterCode,
|
||||
Exists: true,
|
||||
},
|
||||
},
|
||||
expectedStorage: map[common.Address]map[uint64]*big.Int{
|
||||
coordinatorSendAfter: {
|
||||
0: wei(testBalanceInitial),
|
||||
1: wei(150),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "post_6780_create_destroy_to_self",
|
||||
description: "Post-EIP-6780: Factory creates contract, contract selfdestructs to itself. Balance IS burnt and contract destroyed (EIP-6780 exception for same-tx creation).",
|
||||
targetContract: factorySelfDestructBalanceCheck,
|
||||
genesis: &core.Genesis{
|
||||
Config: params.AllDevChainProtocolChanges,
|
||||
Alloc: types.GenesisAlloc{
|
||||
caller: {Balance: big.NewInt(params.Ether)},
|
||||
factorySelfDestructBalanceCheck: {
|
||||
Balance: wei(testBalanceFactory),
|
||||
Code: factorySelfDestructBalanceCheckCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
useBeacon: true,
|
||||
expectedResults: map[common.Address]accountState{
|
||||
createdContractAddrSelfBalanceCheck: {
|
||||
Balance: wei(0),
|
||||
Nonce: 0,
|
||||
Code: []byte{},
|
||||
Exists: false,
|
||||
},
|
||||
factorySelfDestructBalanceCheck: {
|
||||
Balance: wei(testBalanceSent),
|
||||
Nonce: 1,
|
||||
Code: factorySelfDestructBalanceCheckCode,
|
||||
Exists: true,
|
||||
},
|
||||
},
|
||||
expectedStorage: map[common.Address]map[uint64]*big.Int{
|
||||
factorySelfDestructBalanceCheck: {
|
||||
0: wei(0),
|
||||
1: wei(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
signer = types.HomesteadSigner{}
|
||||
tx *types.Transaction
|
||||
err error
|
||||
)
|
||||
|
||||
tx, err = types.SignTx(types.NewTx(&types.LegacyTx{
|
||||
Nonce: 0,
|
||||
To: &tt.targetContract,
|
||||
Value: big.NewInt(0),
|
||||
Gas: testGasLimit,
|
||||
GasPrice: big.NewInt(params.InitialBaseFee * 2),
|
||||
Data: nil,
|
||||
}), signer, key)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to sign transaction: %v", err)
|
||||
}
|
||||
|
||||
blockchain, block, statedb := setupTestBlockchain(t, tt.genesis, tx, tt.useBeacon)
|
||||
defer blockchain.Stop()
|
||||
|
||||
tracer := newSelfdestructStateTracer()
|
||||
hookedState := state.NewHookedState(statedb, tracer.Hooks())
|
||||
msg, err := core.TransactionToMessage(tx, signer, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to prepare transaction for tracing: %v", err)
|
||||
}
|
||||
context := core.NewEVMBlockContext(block.Header(), blockchain, nil)
|
||||
evm := vm.NewEVM(context, hookedState, tt.genesis.Config, vm.Config{Tracer: tracer.Hooks()})
|
||||
usedGas := uint64(0)
|
||||
_, err = core.ApplyTransactionWithEVM(msg, new(core.GasPool).AddGas(tx.Gas()), statedb, block.Number(), block.Hash(), block.Time(), tx, &usedGas, evm)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to execute transaction: %v", err)
|
||||
}
|
||||
|
||||
results := tracer.Accounts()
|
||||
|
||||
// Verify storage
|
||||
for addr, expectedSlots := range tt.expectedStorage {
|
||||
for slot, expectedValue := range expectedSlots {
|
||||
actualValue := statedb.GetState(addr, common.BigToHash(big.NewInt(int64(slot))))
|
||||
if actualValue.Big().Cmp(expectedValue) != 0 {
|
||||
t.Errorf("address %s slot %d: storage mismatch: have %s, want %s",
|
||||
addr.Hex(), slot, actualValue.Big(), expectedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify results
|
||||
for addr, expected := range tt.expectedResults {
|
||||
actual, ok := results[addr]
|
||||
if !ok {
|
||||
t.Errorf("address %s missing from results", addr.Hex())
|
||||
continue
|
||||
}
|
||||
verifyAccountState(t, addr, actual, &expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
object "ContractA" {
|
||||
code {
|
||||
datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
|
||||
return(0, datasize("Runtime"))
|
||||
}
|
||||
object "Runtime" {
|
||||
code {
|
||||
// If receiving funds (msg.value > 0), just accept them and return
|
||||
if gt(callvalue(), 0) {
|
||||
stop()
|
||||
}
|
||||
|
||||
// Otherwise, selfdestruct to B (transfers balance immediately, then stops execution)
|
||||
let contractB := 0x00000000000000000000000000000000000000bb
|
||||
selfdestruct(contractB)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
object "ContractB" {
|
||||
code {
|
||||
datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
|
||||
return(0, datasize("Runtime"))
|
||||
}
|
||||
object "Runtime" {
|
||||
code {
|
||||
// Send 50 wei back to contract A
|
||||
let contractA := 0x00000000000000000000000000000000000000aa
|
||||
let success := call(gas(), contractA, 50, 0, 0, 0, 0)
|
||||
stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
object "ContractSelfDestruct" {
|
||||
code {
|
||||
datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
|
||||
return(0, datasize("Runtime"))
|
||||
}
|
||||
object "Runtime" {
|
||||
code {
|
||||
// Simply selfdestruct to self
|
||||
selfdestruct(address())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
object "Coordinator" {
|
||||
code {
|
||||
datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
|
||||
return(0, datasize("Runtime"))
|
||||
}
|
||||
object "Runtime" {
|
||||
code {
|
||||
let contractA := 0x00000000000000000000000000000000000000aa
|
||||
let contractB := 0x00000000000000000000000000000000000000bb
|
||||
|
||||
// First, call A (A will selfdestruct to B)
|
||||
pop(call(gas(), contractA, 0, 0, 0, 0, 0))
|
||||
|
||||
// Then, call B (B will send funds back to A)
|
||||
pop(call(gas(), contractB, 0, 0, 0, 0, 0))
|
||||
|
||||
stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
object "CoordinatorSendAfter" {
|
||||
code {
|
||||
datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
|
||||
return(0, datasize("Runtime"))
|
||||
}
|
||||
object "Runtime" {
|
||||
code {
|
||||
let contractAddr := 0x00000000000000000000000000000000000000aa
|
||||
|
||||
// Call contract (triggers selfdestruct to self, burning its balance)
|
||||
pop(call(gas(), contractAddr, 0, 0, 0, 0, 0))
|
||||
|
||||
// Check contract's balance immediately after selfdestruct
|
||||
// Store in slot 0 to verify it's 0 (proving immediate burn)
|
||||
sstore(0, balance(contractAddr))
|
||||
|
||||
// Send 50 wei to the contract (after it selfdestructed)
|
||||
pop(call(gas(), contractAddr, 50, 0, 0, 0, 0))
|
||||
|
||||
// Check balance again after sending funds
|
||||
// Store in slot 1 to verify it's 50 (new funds not burnt)
|
||||
sstore(1, balance(contractAddr))
|
||||
|
||||
stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
object "FactoryRefund" {
|
||||
code {
|
||||
datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
|
||||
return(0, datasize("Runtime"))
|
||||
}
|
||||
object "Runtime" {
|
||||
code {
|
||||
let contractB := 0x00000000000000000000000000000000000000bb
|
||||
|
||||
// Store the deploy bytecode for contract A in memory
|
||||
// Full deploy bytecode from: solc --strict-assembly --evm-version paris contractA.yul --bin
|
||||
// Including the 0xfe separator: 600c600d600039600c6000f3fe60003411600a5760bbff5b00
|
||||
// That's 25 bytes, padded to 32 bytes with 7 zero bytes at the front
|
||||
mstore(0, 0x0000000000000000000000000000600c600d600039600c6000f3fe60003411600a5760bbff5b00)
|
||||
|
||||
// CREATE contract A with 100 wei, using 25 bytes starting at position 7
|
||||
let contractA := create(100, 7, 25)
|
||||
|
||||
// Call contract A (triggers selfdestruct to B)
|
||||
pop(call(gas(), contractA, 0, 0, 0, 0, 0))
|
||||
|
||||
// Call contract B (B sends 50 wei back to A)
|
||||
pop(call(gas(), contractB, 0, 0, 0, 0, 0))
|
||||
|
||||
stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
object "FactorySelfDestructBalanceCheck" {
|
||||
code {
|
||||
datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
|
||||
return(0, datasize("Runtime"))
|
||||
}
|
||||
object "Runtime" {
|
||||
code {
|
||||
// Get the full deploy bytecode for ContractSelfDestruct
|
||||
// Compiled with: solc --strict-assembly --evm-version paris contractSelfDestruct.yul --bin
|
||||
// Full bytecode: 6002600d60003960026000f3fe30ff
|
||||
// That's 15 bytes total, padded to 32 bytes with 17 zero bytes at front
|
||||
mstore(0, 0x0000000000000000000000000000000000000000006002600d60003960026000f3fe30ff)
|
||||
|
||||
// CREATE contract with 100 wei, using deploy bytecode
|
||||
// The bytecode is 15 bytes, starts at position 17 in the 32-byte word
|
||||
let contractAddr := create(100, 17, 15)
|
||||
|
||||
// Call the created contract (triggers selfdestruct to self)
|
||||
pop(call(gas(), contractAddr, 0, 0, 0, 0, 0))
|
||||
|
||||
// Check contract's balance immediately after selfdestruct
|
||||
// Store in slot 0 to verify it's 0 (proving immediate burn)
|
||||
sstore(0, balance(contractAddr))
|
||||
|
||||
// Send 50 wei to the contract (after it selfdestructed)
|
||||
pop(call(gas(), contractAddr, 50, 0, 0, 0, 0))
|
||||
|
||||
// Check balance again after sending funds
|
||||
// Store in slot 1 to verify it's 0 (funds sent to destroyed contract are burnt)
|
||||
sstore(1, balance(contractAddr))
|
||||
|
||||
stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue