Skip to content

test_staticcall_nested_call_to_precompile()

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

Generate fixtures for these test cases for Bogota with:

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

Test STATICCALL behavior with an extra call depth layer.

Contract B (target) receives tx and CALLs contract A. Contract A STATICCALLs contract C. Contract C attempts to CALL the precompile. With value = 0, this succeeds. With value > 0, this fails (static context).

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/CallWithZeroValueToPrecompileFromCalledContractFiller.yml",
        "https://github.com/ethereum/tests/blob/v13.3/src/GeneralStateTestsFiller/stStaticFlagEnabled/CallWithNOTZeroValueToPrecompileFromCalledContractFiller.yml",
    ],
)
@pytest.mark.valid_from("Byzantium")
def test_staticcall_nested_call_to_precompile(
    pre: Alloc,
    state_test: StateTestFiller,
    precompile: Address,
    call_value: int,
    fork: Fork,
) -> None:
    """
    Test STATICCALL behavior with an extra call depth layer.

    Contract B (target) receives tx and CALLs contract A.
    Contract A STATICCALLs contract C.
    Contract C attempts to CALL the precompile.
    With value = 0, this succeeds. With value > 0, this fails (static context).
    """
    alice = pre.fund_eoa()

    initial_contract_balance = 1000
    marker = 0xFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEED

    # Contract C: attempts CALL to precompile with the parametrized value
    contract_c = pre.deploy_contract(
        code=Op.CALL(gas=100_000, address=precompile, value=call_value),
        balance=initial_contract_balance,
    )

    # Contract A: STATICCALLs contract C, stores markers and result
    contract_a = pre.deploy_contract(
        code=(
            Op.SSTORE(0, marker)
            + Op.SSTORE(1, Op.STATICCALL(gas=200_000, address=contract_c))
            + Op.SSTORE(2, marker)
        ),
        balance=initial_contract_balance,
    )

    # Contract B (target): CALLs contract A, stores markers and result
    contract_b = pre.deploy_contract(
        code=(
            Op.SSTORE(0, marker)
            + Op.SSTORE(1, Op.CALL(gas=300_000, address=contract_a))
            + Op.SSTORE(2, marker)
        ),
        balance=initial_contract_balance,
    )

    tx_value = 100
    staticcall_result = 1 if call_value == 0 else 0

    bal_expectation = None
    if fork.is_eip_enabled(7928):
        # slot 1 read when call_value > 0
        account_expectations: dict[Address, BalAccountExpectation | None] = {
            contract_a: (
                bal_expectation_for_contract_with_markers(
                    marker=marker,
                    staticcall_result=staticcall_result,
                )
                if call_value == 0
                else BalAccountExpectation(
                    storage_changes=[
                        BalStorageSlot(
                            slot=0,
                            slot_changes=[
                                BalStorageChange(
                                    block_access_index=1, post_value=marker
                                )
                            ],
                        ),
                        BalStorageSlot(
                            slot=2,
                            slot_changes=[
                                BalStorageChange(
                                    block_access_index=1, post_value=marker
                                )
                            ],
                        ),
                    ],
                    storage_reads=[1],
                )
            ),
            contract_b: bal_expectation_for_contract_with_markers(
                marker=marker,
                staticcall_result=1,  # CALL to A always succeeds
                balance_change=tx_value,
                initial_balance=initial_contract_balance,
            ),
            contract_c: BalAccountExpectation.empty(),  # STATICCALLed
        }

        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=Transaction(
            sender=alice,
            to=contract_b,
            value=tx_value,
            protected=True,
        ),
        expected_block_access_list=bal_expectation,
        post={
            contract_a: Account(
                balance=initial_contract_balance,
                storage={
                    0: marker,
                    # only succeeds if call_value == 0
                    1: staticcall_result,
                    2: marker,
                },
            ),
            contract_b: Account(
                balance=initial_contract_balance + tx_value,
                storage={
                    0: marker,
                    1: 1,  # CALL to A always succeeds
                    2: marker,
                },
            ),
            contract_c: Account(balance=initial_contract_balance),
        },
    )

Parametrized Test Cases

This test generates 36 parametrized test cases across 13 forks.