Skip to content

test_bal_nested_delegatecall_storage_writes_net_zero()

Documentation for tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py::test_bal_nested_delegatecall_storage_writes_net_zero@622620d8.

Generate fixtures for these test cases for Amsterdam with:

fill -v tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py::test_bal_nested_delegatecall_storage_writes_net_zero --fork Amsterdam

Test BAL correctly handles nested DELEGATECALL frames where intermediate frames write different values but the deepest frame reverts to original.

Each nesting level writes a different intermediate value, and the deepest frame writes back the original value, resulting in net-zero change.

Example for depth=2 (intermediate_values=[2, 3]): - Pre-state: slot 0 = 1 - Root frame writes: slot 0 = 2 - Child frame writes: slot 0 = 3 - Grandchild frame writes: slot 0 = 1 (back to original) - Expected: No storage_changes (net-zero overall)

Source code in tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
@pytest.mark.parametrize(
    "intermediate_values",
    [
        pytest.param([2], id="depth_1"),
        pytest.param([2, 3], id="depth_2"),
        pytest.param([2, 3, 4], id="depth_3"),
    ],
)
@pytest.mark.eels_base_coverage
def test_bal_nested_delegatecall_storage_writes_net_zero(
    pre: Alloc,
    blockchain_test: BlockchainTestFiller,
    intermediate_values: list,
) -> None:
    """
    Test BAL correctly handles nested DELEGATECALL frames where intermediate
    frames write different values but the deepest frame reverts to original.

    Each nesting level writes a different intermediate value, and the deepest
    frame writes back the original value, resulting in net-zero change.

    Example for depth=2 (intermediate_values=[2, 3]):
    - Pre-state: slot 0 = 1
    - Root frame writes: slot 0 = 2
    - Child frame writes: slot 0 = 3
    - Grandchild frame writes: slot 0 = 1 (back to original)
    - Expected: No storage_changes (net-zero overall)
    """
    alice = pre.fund_eoa()
    starting_value = 1

    # deepest contract writes back to starting_value
    deepest_code = Op.SSTORE(0, starting_value) + Op.STOP
    next_contract = pre.deploy_contract(code=deepest_code)
    delegate_contracts = [next_contract]

    # Build intermediate contracts (in reverse order) that write then
    # DELEGATECALL. Skip the first value since that's for the root contract
    for value in reversed(intermediate_values[1:]):
        code = (
            Op.SSTORE(0, value)
            + Op.DELEGATECALL(100_000, next_contract, 0, 0, 0, 0)
            + Op.STOP
        )
        next_contract = pre.deploy_contract(code=code)
        delegate_contracts.append(next_contract)

    # root_contract writes first intermediate value, then DELEGATECALLs
    root_contract = pre.deploy_contract(
        code=(
            Op.SSTORE(0, intermediate_values[0])
            + Op.DELEGATECALL(100_000, next_contract, 0, 0, 0, 0)
            + Op.STOP
        ),
        storage={0: starting_value},
    )

    tx = Transaction(
        sender=alice,
        to=root_contract,
        gas_limit=500_000,
    )

    account_expectations = {
        alice: BalAccountExpectation(
            nonce_changes=[BalNonceChange(block_access_index=1, post_nonce=1)],
        ),
        root_contract: BalAccountExpectation(
            storage_reads=[0],
            storage_changes=[],  # validate no changes
        ),
    }
    # All delegate contracts accessed but no changes
    for contract in delegate_contracts:
        account_expectations[contract] = BalAccountExpectation.empty()

    blockchain_test(
        pre=pre,
        blocks=[
            Block(
                txs=[tx],
                expected_block_access_list=BlockAccessListExpectation(
                    account_expectations=account_expectations
                ),
            )
        ],
        post={
            alice: Account(nonce=1),
            root_contract: Account(storage={0: starting_value}),
        },
    )

Parametrized Test Cases

This test generates 3 parametrized test cases across 1 fork.