@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_selfdestruct_pre_existing(
state_test: StateTestFiller,
eip_enabled: bool,
pre: Alloc,
sender: EOA,
fork: Fork,
selfdestruct_code: Bytecode,
selfdestruct_contract_initial_balance: int,
sendall_recipient_addresses: List[Address],
call_times: int,
) -> None:
"""
Test calling a previously created account that contains a selfdestruct, and
verify its balance is sent to the destination address.
After EIP-6780, the balance should be sent to the send-all recipient
address, similar to the behavior before the EIP, but the account is not
deleted.
Test using:
- Different send-all recipient addresses: single, multiple,
including self
- Different initial balances for the self-destructing contract
"""
selfdestruct_contract_address = pre.deploy_contract(
selfdestruct_code, balance=selfdestruct_contract_initial_balance
)
entry_code_storage = Storage()
for i, addr in enumerate(sendall_recipient_addresses):
if addr == SELF_ADDRESS:
sendall_recipient_addresses[i] = selfdestruct_contract_address
# 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 in this case will simply call the pre-existing self-
# destructing contract, as many times as required
entry_code = Bytecode()
# Pre-compute the entry_code_address to use for Transfer log 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
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 nonzero value emits Transfer(entry_code -> contract).
if i > 0:
expected_logs_after_tx_value.append(
transfer_log(
entry_code_address, selfdestruct_contract_address, i
)
)
# SELFDESTRUCT emits Transfer to a different recipient; for a
# pre-existing contract sending to itself, no log is emitted (balance
# stays). Pre-Cancun, SD also burns on self, but EIP-7708 is
# Amsterdam+, long after EIP-6780 is enabled, so the self-keep path
# applies here.
if (
sendall_recipient != selfdestruct_contract_address
and selfdestruct_contract_current_balance > 0
):
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
)
# Balance is only kept by the self-destructing contract if we are
# sending to self and the EIP is activated, otherwise the balance is
# destroyed
if (
sendall_recipient != selfdestruct_contract_address
or not eip_enabled
):
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
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(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():
if address != selfdestruct_contract_address:
post[address] = Account(balance=balance, storage={0: 1})
if eip_enabled:
balance = selfdestruct_contract_current_balance
post[selfdestruct_contract_address] = Account(
balance=balance,
storage={0: call_times},
)
else:
post[selfdestruct_contract_address] = Account.NONEXISTENT # type: ignore
if fork.is_eip_enabled(7708):
expected_logs = []
if entry_code_balance > 0:
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)