@pytest.mark.parametrize("create_opcode", [Op.CREATE, Op.CREATE2])
@pytest.mark.parametrize(
"call_times,sendall_recipient_addresses",
[
pytest.param(
1,
[PRE_DEPLOY_CONTRACT_1],
id="single_call",
),
pytest.param(
1,
[SELF_ADDRESS],
id="single_call_self",
),
pytest.param(
2,
[PRE_DEPLOY_CONTRACT_1],
id="multiple_calls_single_sendall_recipient",
),
pytest.param(
2,
[SELF_ADDRESS],
id="multiple_calls_single_self_recipient",
),
pytest.param(
3,
[
PRE_DEPLOY_CONTRACT_1,
PRE_DEPLOY_CONTRACT_2,
PRE_DEPLOY_CONTRACT_3,
],
id="multiple_calls_multiple_sendall_recipients",
),
pytest.param(
3,
[SELF_ADDRESS, PRE_DEPLOY_CONTRACT_2, PRE_DEPLOY_CONTRACT_3],
id="multiple_calls_multiple_sendall_recipients_including_self",
),
pytest.param(
3,
[PRE_DEPLOY_CONTRACT_1, PRE_DEPLOY_CONTRACT_2, SELF_ADDRESS],
id="multiple_calls_multiple_sendall_recipients_including_self_last",
),
pytest.param(
6,
[SELF_ADDRESS, PRE_DEPLOY_CONTRACT_2, PRE_DEPLOY_CONTRACT_3],
id="multiple_calls_multiple_repeating_sendall_recipients_including_self",
),
pytest.param(
6,
[PRE_DEPLOY_CONTRACT_1, PRE_DEPLOY_CONTRACT_2, SELF_ADDRESS],
id="multiple_calls_multiple_repeating_sendall_recipients_including_self_last",
),
],
indirect=["sendall_recipient_addresses"],
)
@pytest.mark.parametrize(
"selfdestruct_contract_initial_balance",
[0, 100_000],
)
@pytest.mark.valid_from("Shanghai")
def test_create_selfdestruct_same_tx(
state_test: StateTestFiller,
pre: Alloc,
sender: EOA,
fork: Fork,
selfdestruct_code: Bytecode,
sendall_recipient_addresses: List[Address],
create_opcode: Op,
call_times: int,
selfdestruct_contract_initial_balance: int,
) -> None:
"""
Use CREATE or CREATE2 to create a self-destructing contract, and call it in
the same transaction.
Behavior should be the same before and after EIP-6780.
Test using:
- Different send-all recipient addresses: single, multiple,
including self
- Different initial balances for the self-destructing contract
- Different opcodes: CREATE, CREATE2
"""
selfdestruct_contract_initcode = Initcode(deploy_code=selfdestruct_code)
initcode_copy_from_address = pre.deploy_contract(
selfdestruct_contract_initcode
)
# Our entry point is an initcode that in turn creates a self-destructing
# contract
entry_code_storage = Storage()
# Bytecode used to create the contract, can be CREATE or CREATE2
create_bytecode = create_opcode(size=len(selfdestruct_contract_initcode))
selfdestruct_contract_address = compute_create_address(
address=compute_create_address(address=sender, nonce=0),
nonce=1,
initcode=selfdestruct_contract_initcode,
opcode=create_opcode,
)
for i, addr in enumerate(sendall_recipient_addresses):
if addr == SELF_ADDRESS:
sendall_recipient_addresses[i] = selfdestruct_contract_address
if selfdestruct_contract_initial_balance > 0:
pre.fund_address(
selfdestruct_contract_address,
selfdestruct_contract_initial_balance,
)
# Create a dict to record the expected final balances
sendall_final_balances = dict(
zip(
sendall_recipient_addresses,
[0] * len(sendall_recipient_addresses),
strict=False,
)
)
selfdestruct_contract_current_balance = (
selfdestruct_contract_initial_balance
)
# 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_contract_initcode),
)
# 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(len(selfdestruct_code)),
Op.EXTCODESIZE(selfdestruct_contract_address),
)
entry_code += Op.SSTORE(
entry_code_storage.store_next(selfdestruct_code.keccak256()),
Op.EXTCODEHASH(selfdestruct_contract_address),
)
# Precompute entry_code_address (for Transfer log sender attribution)
entry_code_address = compute_create_address(address=sender, nonce=0)
# Call the self-destructing contract multiple times as required, increasing
# the wei sent each time. Also track the sequence of EIP-7708 logs so they
# can be asserted as expected receipt logs.
expected_logs_after_tx_value: list = []
entry_code_balance = 0
for i, sendall_recipient in zip(
range(call_times), cycle(sendall_recipient_addresses)
):
entry_code += Op.MSTORE(0, sendall_recipient)
entry_code += Op.SSTORE(
entry_code_storage.store_next(1),
Op.CALL(
Op.GASLIMIT, # Gas
selfdestruct_contract_address, # Address
i, # Value
0,
32,
0,
0,
),
)
entry_code_balance += i
selfdestruct_contract_current_balance += i
# CALL with value > 0 emits a Transfer log (entry_code -> contract)
if i > 0:
expected_logs_after_tx_value.append(
transfer_log(
entry_code_address, selfdestruct_contract_address, i
)
)
# SELFDESTRUCT emits a Transfer log to a different address, or a Burn
# log when sending to self (contract was created in this tx).
if selfdestruct_contract_current_balance > 0:
if sendall_recipient != selfdestruct_contract_address:
expected_logs_after_tx_value.append(
transfer_log(
selfdestruct_contract_address,
sendall_recipient,
selfdestruct_contract_current_balance,
)
)
# Balance is always sent to other contracts
if sendall_recipient != selfdestruct_contract_address:
sendall_final_balances[sendall_recipient] += (
selfdestruct_contract_current_balance
)
selfdestruct_contract_current_balance = 0
elif not fork.is_eip_enabled(8246):
# per EIP-8246
selfdestruct_contract_current_balance = 0
entry_code += Op.SSTORE(
entry_code_storage.store_next(
selfdestruct_contract_current_balance
),
Op.BALANCE(selfdestruct_contract_address),
)
# Check the EXTCODE* properties of the self-destructing contract again
entry_code += Op.SSTORE(
entry_code_storage.store_next(len(selfdestruct_code)),
Op.EXTCODESIZE(selfdestruct_contract_address),
)
entry_code += Op.SSTORE(
entry_code_storage.store_next(selfdestruct_code.keccak256()),
Op.EXTCODEHASH(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_contract_initcode), 32), 1)
tx = Transaction(
value=entry_code_balance,
data=entry_code,
sender=sender,
to=None,
)
assert tx.created_contract == entry_code_address
post: Dict[Address, Account] = {
entry_code_address: Account(
storage=entry_code_storage,
),
}
# Check the balances of the sendall recipients
for address, balance in sendall_final_balances.items():
post[address] = Account(balance=balance, storage={0: 1})
if fork.is_eip_enabled(8246) and selfdestruct_contract_current_balance > 0:
# per EIP-8246
post[selfdestruct_contract_address] = Account(
balance=selfdestruct_contract_current_balance,
nonce=0,
code=b"",
storage={},
)
else:
post[selfdestruct_contract_address] = Account.NONEXISTENT # type: ignore
if fork.is_eip_enabled(7708):
expected_logs = []
if entry_code_balance > 0:
# tx value transfer: sender -> entry_code_address
expected_logs.append(
transfer_log(sender, entry_code_address, entry_code_balance)
)
expected_logs.extend(expected_logs_after_tx_value)
tx.expected_receipt = TransactionReceipt(logs=expected_logs)
state_test(pre=pre, post=post, tx=tx)