Skip to content

test_staticcall_reentrant_call_to_precompile()

Documentation for tests/byzantium/eip214_staticcall/test_staticcall.py::test_staticcall_reentrant_call_to_precompile@ffe48572.

Generate fixtures for these test cases for Bogota with:

fill -v tests/byzantium/eip214_staticcall/test_staticcall.py::test_staticcall_reentrant_call_to_precompile --fork Bogota

Test CALL to precompile inside STATICCALL with zero and non-zero value.

Regression test for tests#683. Source: https://github.com/ethereum/execution-specs/pull/1960#discussion_r2656834142

A single contract STATICCALLs itself. On reentry (detected via CALLVALUE=0, since STATICCALL doesn't forward value), it attempts CALL to a precompile.

  • call_value=0: CALL succeeds in static context → STATICCALL returns 1
  • call_value>0: CALL violates static context, reverts frame → STATICCALL returns 0
Source code in tests/byzantium/eip214_staticcall/test_staticcall.py
@pytest.mark.with_all_precompiles
@pytest.mark.parametrize(
    "call_value", [0, 2], ids=["zero_value", "nonzero_value"]
)
@pytest.mark.ported_from(
    "https://github.com/ethereum/tests/blob/v13.3/src/GeneralStateTestsFiller/"
    "stStaticFlagEnabled/StaticcallForPrecompilesIssue683Filler.yml"
)
@pytest.mark.valid_from("Byzantium")
@pytest.mark.eels_base_coverage
def test_staticcall_reentrant_call_to_precompile(
    pre: Alloc,
    state_test: StateTestFiller,
    precompile: Address,
    call_value: int,
    fork: Fork,
) -> None:
    """
    Test CALL to precompile inside STATICCALL with zero and non-zero value.

    Regression test for ethereum/tests#683.
    Source: https://github.com/ethereum/execution-specs/pull/1960#discussion_r2656834142

    A single contract STATICCALLs itself. On reentry (detected via CALLVALUE=0,
    since STATICCALL doesn't forward value), it attempts CALL to a precompile.

    - call_value=0: CALL succeeds in static context → STATICCALL returns 1
    - call_value>0: CALL violates static context, reverts frame → STATICCALL
      returns 0
    """
    alice = pre.fund_eoa()

    # Contract that STATICCALLs itself on reentry (CALLVALUE=0),
    # attempts CALL to precompile
    target_code = Conditional(
        # CALLVALUE=0 indicates we're inside the STATICCALL (reentry)
        condition=Op.ISZERO(Op.CALLVALUE),
        # try CALL with parametrized value (fails if value > 0)
        if_true=Op.CALL(address=precompile, value=call_value),
        # STATICCALL to self, store result (0=fail, 1=success)
        if_false=Op.SSTORE(0, Op.STATICCALL(address=Op.ADDRESS)),
    )

    target_balance = 1000
    target = pre.deploy_contract(code=target_code, balance=target_balance)

    tx_value = 100
    tx = Transaction(
        sender=alice,
        to=target,
        value=tx_value,
        protected=True,
    )

    bal_expectation = None
    if fork.is_eip_enabled(7928):
        # Target contract always receives tx value
        target_balance_changes = [
            BalBalanceChange(
                block_access_index=1, post_balance=target_balance + tx_value
            )
        ]

        # call_value > 0: SSTORE(0, 0) is a read; call_value == 0: real change
        account_expectations: dict[Address, BalAccountExpectation | None] = {
            target: (
                BalAccountExpectation(
                    storage_changes=[
                        BalStorageSlot(
                            slot=0,
                            slot_changes=[
                                BalStorageChange(
                                    block_access_index=1, post_value=1
                                )
                            ],
                        ),
                    ],
                    balance_changes=target_balance_changes,
                )
                if call_value == 0
                else BalAccountExpectation(
                    storage_reads=[0],
                    balance_changes=target_balance_changes,
                )
            ),
        }

        if call_value == 0:
            account_expectations[precompile] = BalAccountExpectation.empty()
        else:
            account_expectations[precompile] = None  # reverted before accessed

        bal_expectation = BlockAccessListExpectation(
            account_expectations=account_expectations
        )

    state_test(
        pre=pre,
        tx=tx,
        expected_block_access_list=bal_expectation,
        post={
            target: Account(
                balance=target_balance + tx_value,
                storage={0: 1 if call_value == 0 else 0},
            ),
        },
    )

Parametrized Test Cases

This test generates 36 parametrized test cases across 13 forks.