Skip to content

test_extcodehash_subcall_selfdestruct()

Documentation for tests/constantinople/eip1052_extcodehash/test_extcodehash.py::test_extcodehash_subcall_selfdestruct@ffe48572.

Generate fixtures for these test cases for Bogota with:

fill -v tests/constantinople/eip1052_extcodehash/test_extcodehash.py::test_extcodehash_subcall_selfdestruct --fork Bogota

Test EXTCODEHASH after subcall with CALLCODE/DELEGATECALL to SELFDESTRUCT.

B calls A, which uses CALLCODE or DELEGATECALL to invoke a contract C containing SELFDESTRUCT, executing it in A's context. B checks EXTCODEHASH, EXTCODESIZE, and EXTCODECOPY of A before and after. Within the transaction, A's code properties are unchanged. Pre-Cancun, A is deleted at end of transaction. In Cancun, A survives only if pre-existing; a dynamically created A is still deleted (EIP-6780).

Source code in tests/constantinople/eip1052_extcodehash/test_extcodehash.py
@pytest.mark.ported_from(
    [
        "https://github.com/ethereum/tests/blob/v13.3/src/GeneralStateTestsFiller/stExtCodeHash/extCodeHashSubcallSuicideFiller.yml",  # noqa: E501
        "https://github.com/ethereum/tests/blob/v13.3/src/GeneralStateTestsFiller/stExtCodeHash/extCodeHashSubcallSuicideCancunFiller.yml",  # noqa: E501
    ],
    pr=["https://github.com/ethereum/execution-specs/pull/2418"],
)
@pytest.mark.parametrize(
    "call_opcode",
    [
        pytest.param(Op.CALLCODE, id="callcode"),
        pytest.param(Op.DELEGATECALL, id="delegatecall"),
    ],
)
@pytest.mark.parametrize(
    "dynamic_a",
    [
        pytest.param(False, id="pre_existing"),
        pytest.param(True, id="dynamic"),
    ],
)
def test_extcodehash_subcall_selfdestruct(
    state_test: StateTestFiller,
    pre: Alloc,
    fork: Fork,
    call_opcode: Opcodes,
    dynamic_a: bool,
) -> None:
    """
    Test EXTCODEHASH after subcall with CALLCODE/DELEGATECALL to SELFDESTRUCT.

    B calls A, which uses CALLCODE or DELEGATECALL to invoke a contract C
    containing SELFDESTRUCT, executing it in A's context. B checks
    EXTCODEHASH, EXTCODESIZE, and EXTCODECOPY of A before and after.
    Within the transaction, A's code properties are unchanged. Pre-Cancun,
    A is deleted at end of transaction. In Cancun, A survives only if
    pre-existing; a dynamically created A is still deleted (EIP-6780).
    """
    storage = Storage()
    beneficiary = pre.nonexistent_account()
    selfdestruct_code = Op.SELFDESTRUCT(beneficiary)
    target_c = pre.deploy_contract(selfdestruct_code)

    # A: executes C's code in A's context via CALLCODE/DELEGATECALL
    a_code = call_opcode(
        address=target_c,
        ret_size=32,
    )

    if not dynamic_a:
        a = pre.deploy_contract(a_code, balance=1)

    a_hash = a_code.keccak256()
    a_size = len(a_code)
    a_code_word0 = bytes(a_code)[:32].ljust(32, b"\0")

    def extcode_checks(target: Address | Bytecode) -> Bytecode:
        """Check EXTCODEHASH, EXTCODESIZE, and EXTCODECOPY of A."""
        return (
            Op.SSTORE(storage.store_next(a_hash), Op.EXTCODEHASH(target))
            + Op.SSTORE(storage.store_next(a_size), Op.EXTCODESIZE(target))
            + Op.EXTCODECOPY(target, 0, 0, 32)
            + Op.SSTORE(
                storage.store_next(a_code_word0),
                Op.MLOAD(0),
            )
        )

    code = Bytecode()

    if dynamic_a:
        initcode = Initcode(deploy_code=a_code)
        code += Om.MSTORE(initcode)
        created_slot = storage.store_next(0)
        code += Op.SSTORE(
            created_slot,
            Op.CREATE(value=0, offset=0, size=len(initcode)),
        )
        a_target: Address | Bytecode = Op.SLOAD(created_slot)
    else:
        a_target = a

    code += extcode_checks(a_target)
    code += Op.SSTORE(
        storage.store_next(1),
        Op.CALL(address=a_target),
    )
    code += extcode_checks(a_target)
    code += Op.SSTORE(
        storage.store_next(1),
        Op.CALL(address=a_target),
    )

    code_address = pre.deploy_contract(code, storage=storage.canary())

    if dynamic_a:
        a = compute_create_address(address=code_address, nonce=1)
        storage[created_slot] = a

    tx = Transaction(sender=pre.fund_eoa(), to=code_address)

    # Pre-Cancun, CALLCODE/DELEGATECALL executes SELFDESTRUCT in A's
    # context, deleting A at end of transaction.
    # In Cancun, pre-existing A survives (EIP-6780); dynamic A is deleted.
    post: dict[Address, Account | None] = {
        code_address: Account(storage=storage),
    }
    if fork >= Cancun and not dynamic_a:
        post[a] = Account(code=a_code, balance=0)
    else:
        post[a] = Account.NONEXISTENT

    state_test(pre=pre, post=post, tx=tx)

Parametrized Test Cases

This test generates 4 parametrized test cases across 11 forks.