@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/CallWithZeroValueToPrecompileFromTransactionFiller.yml",
"https://github.com/ethereum/tests/blob/v13.3/src/GeneralStateTestsFiller/stStaticFlagEnabled/CallWithNOTZeroValueToPrecompileFromTransactionFiller.yml",
],
)
@pytest.mark.valid_from("Byzantium")
def test_staticcall_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.
Contract A STATICCALLs contract B. Contract B attempts to CALL precompile.
With value = 0, this succeeds. With value > 0, this fails (static context).
"""
alice = pre.fund_eoa()
initial_contract_balance = 1000
marker = 0xFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEEDFEED
# Contract B: attempts CALL to precompile with the parametrized value
contract_b = pre.deploy_contract(
code=Op.CALL(gas=100_000, address=precompile, value=call_value),
balance=initial_contract_balance,
)
# Contract A: STATICCALLs contract B and stores the result
contract_a = pre.deploy_contract(
code=(
Op.SSTORE(0, marker)
+ Op.SSTORE(1, Op.STATICCALL(gas=200_000, address=contract_b))
+ 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):
contract_a_balance_changes = [
BalBalanceChange(
block_access_index=1,
post_balance=initial_contract_balance + tx_value,
)
]
# 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,
balance_change=tx_value,
initial_balance=initial_contract_balance,
)
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],
balance_changes=contract_a_balance_changes,
)
),
contract_b: 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_a,
value=tx_value,
protected=True,
),
expected_block_access_list=bal_expectation,
post={
contract_a: Account(
balance=initial_contract_balance + tx_value,
storage={
0: marker,
1: staticcall_result,
2: marker,
},
),
contract_b: Account(balance=initial_contract_balance),
},
)