@pytest.mark.parametrize("destroy_parent", [True, False])
@pytest.mark.parametrize("selfdestruct_contract_initial_balance", [0, 100_000])
@pytest.mark.valid_from("Shanghai")
def test_parent_creates_child_selfdestruct_one(
state_test: StateTestFiller,
pre: Alloc,
sender: EOA,
fork: Fork,
destroy_parent: bool,
selfdestruct_contract_initial_balance: int,
) -> None:
"""
Test a parent contract that creates a child contract, then only one
of them self-destructs (either parent or child, based on parameter).
Both contracts are created in the same transaction:
- If destroy_parent=True: Parent self-destructs, child survives
- If destroy_parent=False: Child self-destructs, parent survives
Since both are created in the same tx, whichever self-destructs should be
deleted.
"""
entry_code_storage = Storage()
sendall_recipient = pre.deploy_contract(
code=Op.SSTORE(0, 0),
storage={0: 1},
)
# Child contract: just has code, self-destructs when called
child_code = Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1)) + Op.SELFDESTRUCT(
sendall_recipient
)
child_initcode = Initcode(deploy_code=child_code)
# Parent contract: creates child, then either self-destructs or calls child
# to self-destruct based on calldata[0]: 1 = destroy parent, 0 = destroy
# child
# For simplicity, use pre-deployed child initcode
child_initcode_address = pre.deploy_contract(child_initcode)
parent_code = (
Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1))
+ Op.EXTCODECOPY(child_initcode_address, 0, 0, len(child_initcode))
+ Op.SSTORE(1, Op.CREATE(value=0, offset=0, size=len(child_initcode)))
+ Conditional(
condition=Op.EQ(Op.CALLDATALOAD(0), 1),
if_true=Op.SELFDESTRUCT(sendall_recipient),
if_false=Op.CALL(Op.GASLIMIT, Op.SLOAD(1), 0, 0, 0, 0, 0),
)
+ Op.STOP
)
parent_initcode = Initcode(deploy_code=parent_code)
parent_initcode_address = pre.deploy_contract(parent_initcode)
entry_code_address = compute_create_address(address=sender, nonce=0)
parent_address = compute_create_address(
address=entry_code_address,
nonce=1,
initcode=parent_initcode,
opcode=Op.CREATE,
)
child_address = compute_create_address(address=parent_address, nonce=1)
if selfdestruct_contract_initial_balance > 0:
pre.fund_address(parent_address, selfdestruct_contract_initial_balance)
pre.fund_address(child_address, selfdestruct_contract_initial_balance)
# Entry code: create parent and call it with appropriate flag
entry_code = Op.EXTCODECOPY(
parent_initcode_address,
0,
0,
len(parent_initcode),
)
entry_code += Op.SSTORE(
entry_code_storage.store_next(parent_address),
Op.CREATE(value=0, offset=0, size=len(parent_initcode)),
)
flag = 1 if destroy_parent else 0
entry_code += Op.MSTORE(0, flag)
entry_code += Op.SSTORE(
entry_code_storage.store_next(1),
Op.CALL(Op.GASLIMIT, parent_address, 0, 0, 32, 0, 0),
)
entry_code += Op.RETURN(32, 1)
tx = Transaction(
data=entry_code,
sender=sender,
to=None,
)
post: Dict[Address, Account] = {
entry_code_address: Account(storage=entry_code_storage),
}
if destroy_parent:
post[parent_address] = Account.NONEXISTENT # type: ignore
post[child_address] = Account(
storage={0: 0},
balance=selfdestruct_contract_initial_balance,
)
post[sendall_recipient] = Account(
balance=selfdestruct_contract_initial_balance,
storage={0: 1},
)
else:
post[child_address] = Account.NONEXISTENT # type: ignore
post[parent_address] = Account(
storage={0: 1, 1: child_address},
balance=selfdestruct_contract_initial_balance,
)
post[sendall_recipient] = Account(
balance=selfdestruct_contract_initial_balance,
storage={0: 1},
)
if fork.is_eip_enabled(7708):
# Only the SELFDESTRUCT that actually runs emits a log. Both parent
# and child are pre-funded via pre.fund_address, so whichever
# SELFDESTRUCTs to the shared recipient transfers its initial_balance.
expected_logs = []
if selfdestruct_contract_initial_balance > 0:
source = parent_address if destroy_parent else child_address
expected_logs.append(
transfer_log(
source,
sendall_recipient,
selfdestruct_contract_initial_balance,
)
)
tx.expected_receipt = TransactionReceipt(logs=expected_logs)
state_test(pre=pre, post=post, tx=tx)