@pytest.mark.parametrize("call_opcode", [Op.DELEGATECALL, Op.CALLCODE])
@pytest.mark.parametrize(
"beneficiary",
[pytest.param("self", id="self"), pytest.param("other", id="other")],
)
@pytest.mark.parametrize(
"initial_balance",
[pytest.param(0, id="zero_balance"), pytest.param(3, id="funded")],
)
@EIPChecklist.Opcode.Test.ExecutionContext.Delegatecall(eip=[8246])
@EIPChecklist.Opcode.Test.ExecutionContext.Delegatecall.Balance(eip=[8246])
@EIPChecklist.Opcode.Test.ExecutionContext.Delegatecall.Code(eip=[8246])
@EIPChecklist.Opcode.Test.ExecutionContext.Delegatecall.Storage(eip=[8246])
@EIPChecklist.Opcode.Test.ExecutionContext.Callcode(eip=[8246])
def test_selfdestruct_in_delegate_context(
state_test: StateTestFiller,
pre: Alloc,
fork: Fork,
call_opcode: Op,
beneficiary: str,
initial_balance: int,
) -> None:
"""
The account destroyed is the one whose context runs SELFDESTRUCT, not
the library holding the code.
"""
sender = pre.fund_eoa()
other = pre.fund_eoa(amount=OTHER_BALANCE)
if beneficiary == "self":
target: Address | Bytecode = Op.ADDRESS
elif beneficiary == "other":
target = other
else:
raise ValueError(f"unhandled beneficiary {beneficiary}")
library_code = Op.SSTORE(0, 1) + Op.SELFDESTRUCT(target)
library = pre.deploy_contract(code=library_code, balance=7)
victim_code = Op.POP(call_opcode(gas=Op.GAS, address=library)) + Op.STOP
initcode = Initcode(deploy_code=victim_code)
entry = pre.deploy_contract(
code=Om.MSTORE(initcode, 0)
+ Op.SSTORE(
0, Op.CREATE(value=initial_balance, offset=0, size=len(initcode))
)
+ Op.SSTORE(1, Op.CALL(gas=Op.GAS, address=Op.SLOAD(0)))
+ Op.STOP
)
victim = compute_create_address(address=entry, nonce=1)
tx = Transaction(sender=sender, to=entry, value=initial_balance)
if beneficiary == "self":
victim_balance = initial_balance
other_balance = OTHER_BALANCE
elif beneficiary == "other":
victim_balance = 0
other_balance = OTHER_BALANCE + initial_balance
else:
raise ValueError(f"unhandled beneficiary {beneficiary}")
logs: list[TransactionLog] = []
if initial_balance > 0:
logs.append(transfer_log(sender, entry, initial_balance))
logs.append(transfer_log(entry, victim, initial_balance))
if beneficiary == "other":
logs.append(transfer_log(victim, other, initial_balance))
if fork.is_eip_enabled(7708):
tx.expected_receipt = TransactionReceipt(logs=logs)
expected_bal = None
if fork.is_eip_enabled(7928):
# The library's SSTORE runs in the victim's context, so the BAL
# attributes the (then cleared) slot to the victim, not the library.
expected_bal = BlockAccessListExpectation(
account_expectations={
victim: finalized_bal(fork, victim_balance, [0]),
library: BalAccountExpectation.empty(),
}
)
state_test(
pre=pre,
post={
entry: Account(storage={0: victim, 1: 1}),
victim: finalized(fork, victim_balance),
library: Account(balance=7, code=library_code, storage={}),
other: Account(balance=other_balance),
},
tx=tx,
expected_block_access_list=expected_bal,
)