@pytest.mark.parametrize("create_opcode", [Op.CREATE, Op.CREATE2])
@pytest.mark.parametrize("call_times", [0, 1, 2])
@pytest.mark.parametrize(
"selfdestruct_contract_initial_balance",
[0, 100_000],
)
@pytest.mark.valid_from("Shanghai")
def test_self_destructing_initcode(
state_test: StateTestFiller,
pre: Alloc,
sender: EOA,
fork: Fork,
selfdestruct_code: Bytecode,
sendall_recipient_addresses: List[Address],
create_opcode: Op,
call_times: int, # Number of times to call the self-destructing contract
# in the same tx
selfdestruct_contract_initial_balance: int,
) -> None:
"""
Test that a contract can self-destruct in its initcode.
Behavior is the same before and after EIP-6780.
Test using:
- Different initial balances for the self-destructing contract
- Different opcodes: CREATE, CREATE2
- Different number of calls to the self-destructing contract in
the same tx
"""
initcode_copy_from_address = pre.deploy_contract(selfdestruct_code)
# Our entry point is an initcode that in turn creates a self-destructing
# contract
entry_code_storage = Storage()
sendall_amount = 0
# Bytecode used to create the contract, can be CREATE or CREATE2
create_bytecode = create_opcode(size=len(selfdestruct_code))
selfdestruct_contract_address = compute_create_address(
address=compute_create_address(address=sender, nonce=0),
nonce=1,
initcode=selfdestruct_code,
opcode=create_opcode,
)
# Entry code that will be executed, creates the contract and then calls it
# in the same tx
entry_code = (
# Initcode is already deployed at `initcode_copy_from_address`, so just
# copy it
Op.EXTCODECOPY(
initcode_copy_from_address,
0,
0,
len(selfdestruct_code),
)
# And we store the created address for verification purposes
+ Op.SSTORE(
entry_code_storage.store_next(selfdestruct_contract_address),
create_bytecode,
)
)
# Store the EXTCODE* properties of the created address
entry_code += Op.SSTORE(
entry_code_storage.store_next(0),
Op.EXTCODESIZE(selfdestruct_contract_address),
)
entry_code += Op.SSTORE(
entry_code_storage.store_next(Bytecode().keccak256()),
Op.EXTCODEHASH(selfdestruct_contract_address),
)
# Call the self-destructing contract multiple times as required, increasing
# the wei sent each time
entry_code_balance = 0
selfdestruct_contract_address_remaining_balance = 0
for i in range(call_times):
entry_code += Op.SSTORE(
entry_code_storage.store_next(1),
Op.CALL(
Op.GASLIMIT, # Gas
selfdestruct_contract_address, # Address
i, # Value
0,
0,
0,
0,
),
)
entry_code_balance += i
if fork.is_eip_enabled(8246):
# After the first call, this call value will become stuck since
# EIP-8246 disables self-destruct burns.
selfdestruct_contract_address_remaining_balance += i
entry_code += Op.SSTORE(
entry_code_storage.store_next(entry_code_balance),
Op.BALANCE(selfdestruct_contract_address),
)
# Lastly return zero so the entry point contract is created and we can
# retain the stored values for verification.
entry_code += Op.RETURN(max(len(selfdestruct_code), 32), 1)
if selfdestruct_contract_initial_balance > 0:
# Address where the contract is created already had some balance,
# which must be included in the send-all operation
sendall_amount += selfdestruct_contract_initial_balance
pre.fund_address(
selfdestruct_contract_address,
selfdestruct_contract_initial_balance,
)
tx = Transaction(
value=entry_code_balance,
data=entry_code,
sender=sender,
to=None,
)
entry_code_address = tx.created_contract
post: Dict[Address, Account] = {
entry_code_address: Account(
storage=entry_code_storage,
),
selfdestruct_contract_address: Account.NONEXISTENT
if selfdestruct_contract_address_remaining_balance == 0
else Account(balance=selfdestruct_contract_address_remaining_balance), # type: ignore
sendall_recipient_addresses[0]: Account(
balance=sendall_amount, storage={0: 1}
),
}
expected_logs = []
if fork.is_eip_enabled(7708):
# tx value transfer: sender -> entry_code_address (created contract)
if entry_code_balance > 0:
expected_logs.append(
transfer_log(sender, entry_code_address, entry_code_balance)
)
# Initcode SELFDESTRUCT sends pre-existing balance to the recipient.
if selfdestruct_contract_initial_balance > 0:
expected_logs.append(
transfer_log(
selfdestruct_contract_address,
sendall_recipient_addresses[0],
selfdestruct_contract_initial_balance,
)
)
# CALLs to the destroyed contract transfer ETH to it.
for i in range(call_times):
if i > 0:
expected_logs.append(
transfer_log(
entry_code_address, selfdestruct_contract_address, i
)
)
tx.expected_receipt = TransactionReceipt(logs=expected_logs)
state_test(pre=pre, post=post, tx=tx)