Skip to content

test_selfdestruct_self_or_precompile_beneficiary()

Documentation for tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py::test_selfdestruct_self_or_precompile_beneficiary@2867859a.

Generate fixtures for these test cases for Amsterdam with:

fill -v tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py::test_selfdestruct_self_or_precompile_beneficiary --fork Amsterdam

SELFDESTRUCT to self or a precompile is warm and charges no ACCOUNT_WRITE.

The executing account is in the accessed set on entry (self), and precompiles are pre-warmed from the start, so neither pays a cold surcharge: execution = OPCODE_SELFDESTRUCT_BASE (warm base, no WARM_ACCESS) with no state gas.

The destructor balance is chosen so no account creation occurs: self is alive (sending to itself never creates), and the precompile case sends zero value (precompiles hold no state entry, so a value transfer would otherwise create one and charge GAS_NEW_ACCOUNT on the state axis).

Source code in tests/amsterdam/eip8038_state_access_gas_cost_increase/test_selfdestruct_gas.py
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
@EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement()
@pytest.mark.parametrize(
    "beneficiary_kind",
    [
        pytest.param("self", id="self_beneficiary"),
        pytest.param("precompile", id="precompile_beneficiary"),
    ],
)
def test_selfdestruct_self_or_precompile_beneficiary(
    state_test: StateTestFiller,
    pre: Alloc,
    fork: Fork,
    beneficiary_kind: str,
) -> None:
    """
    SELFDESTRUCT to self or a precompile is warm and charges no
    ACCOUNT_WRITE.

    The executing account is in the accessed set on entry (self), and
    precompiles are pre-warmed from the start, so neither pays a cold
    surcharge: execution = ``OPCODE_SELFDESTRUCT_BASE`` (warm base, no
    ``WARM_ACCESS``) with no state gas.

    The destructor balance is chosen so no account creation occurs: self
    is alive (sending to itself never creates), and the precompile case
    sends zero value (precompiles hold no state entry, so a value
    transfer would otherwise create one and charge ``GAS_NEW_ACCOUNT`` on
    the state axis).
    """
    if beneficiary_kind == "self":
        # Self is warm on entry; the PUSH is `ADDRESS` (BASE=2). A
        # non-zero balance is transferred to self (no creation).
        destructor_code = Op.SELFDESTRUCT.with_metadata(address_warm=True)(
            Op.ADDRESS
        )
        balance = 1
    else:
        # Identity precompile (address 4) is pre-warmed. Zero balance so
        # no value transfer and thus no account creation.
        destructor_code = Op.SELFDESTRUCT.with_metadata(address_warm=True)(
            Address(4)
        )
        balance = 0
    destructor = pre.deploy_contract(code=destructor_code, balance=balance)

    caller_code = Op.POP(Op.CALL(gas=Op.GAS, address=destructor)) + Op.STOP
    caller = pre.deploy_contract(code=caller_code)

    intrinsic = fork.transaction_intrinsic_cost_calculator()()

    expected_gas_used = (
        intrinsic
        + caller_code.gas_cost(fork)
        + destructor_code.execution_cost(fork)
    )

    tx = Transaction(
        to=caller,
        sender=pre.fund_eoa(),
        state_gas_reservoir=0,
        expected_receipt=TransactionReceipt(
            cumulative_gas_used=expected_gas_used
        ),
    )

    # EIP-6780: the pre-deployed destructor is not deleted. The self case
    # keeps its balance (transferred to itself); the precompile case sent
    # nothing.
    post = {destructor: Account(balance=balance, code=destructor_code)}

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

Parametrized Test Cases

This test generates 2 parametrized test cases across 1 fork.