diff --git a/.github/configs/eels_resolutions.json b/.github/configs/eels_resolutions.json index 9fe14efc946..571b7adcf67 100644 --- a/.github/configs/eels_resolutions.json +++ b/.github/configs/eels_resolutions.json @@ -52,6 +52,6 @@ "Amsterdam": { "git_url": "https://github.com/fselmo/execution-specs.git", "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "6abe0ecb792265211d93bc3fea2f932e5d2cfa90" + "commit": "5d98d409da10f763754dea8d60e855395f944498" } } diff --git a/src/pytest_plugins/eels_resolutions.json b/src/pytest_plugins/eels_resolutions.json index f7b1dd99081..2b0305271d5 100644 --- a/src/pytest_plugins/eels_resolutions.json +++ b/src/pytest_plugins/eels_resolutions.json @@ -55,6 +55,6 @@ "Amsterdam": { "git_url": "https://github.com/fselmo/execution-specs.git", "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "6abe0ecb792265211d93bc3fea2f932e5d2cfa90" + "commit": "5d98d409da10f763754dea8d60e855395f944498" } } diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py new file mode 100644 index 00000000000..a1e3a403446 --- /dev/null +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py @@ -0,0 +1,585 @@ +"""Tests for the effects of EIP-7702 transactions on EIP-7928.""" + +import pytest + +from ethereum_test_tools import ( + Account, + Alloc, + AuthorizationTuple, + Block, + BlockchainTestFiller, + Transaction, +) +from ethereum_test_types.block_access_list import ( + BalAccountExpectation, + BalBalanceChange, + BalCodeChange, + BalNonceChange, + BalStorageChange, + BalStorageSlot, + BlockAccessListExpectation, +) +from ethereum_test_vm import Opcodes as Op +from tests.prague.eip7702_set_code_tx.spec import Spec as Spec7702 + +from .spec import ref_spec_7928 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path +REFERENCE_SPEC_VERSION = ref_spec_7928.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +@pytest.mark.parametrize( + "self_funded", + [ + pytest.param(False, id="sponsored"), + pytest.param(True, id="self_funded"), + ], +) +def test_bal_7702_delegation_create( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + self_funded: bool, +): + """Ensure BAL captures creation of EOA delegation.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + if not self_funded: + relayer = pre.fund_eoa() + sender = relayer + else: + sender = alice + + oracle = pre.deploy_contract(code=Op.STOP) + + tx = Transaction( + sender=sender, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle, + nonce=1 if self_funded else 0, + signer=alice, + ) + ], + ) + + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2 if self_funded else 1)], + code_changes=[ + BalCodeChange(tx_index=1, new_code=Spec7702.delegation_designation(oracle)) + ], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)] + ), + # Oracle must not be present in BAL - the account is never accessed + oracle: None, + } + + # For sponsored variant, relayer must also be included in BAL + if not self_funded: + account_expectations[relayer] = BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + + post = { + alice: Account( + nonce=2 if self_funded else 1, code=Spec7702.delegation_designation(oracle) + ), + # Bob receives 10 wei + bob: Account(balance=10), + } + + # For sponsored variant, include relayer in post state + if not self_funded: + post.update({relayer: Account(nonce=1)}) + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +@pytest.mark.parametrize( + "self_funded", + [ + pytest.param(False, id="sponsored"), + pytest.param(True, id="self_funded"), + ], +) +def test_bal_7702_delegation_update( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + self_funded: bool, +): + """Ensure BAL captures update of existing EOA delegation.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + if not self_funded: + relayer = pre.fund_eoa() + sender = relayer + else: + sender = alice + + oracle1 = pre.deploy_contract(code=Op.STOP) + oracle2 = pre.deploy_contract(code=Op.STOP) + + ## Perhaps create pre existing delegation, + ## see `test_bal_7702_delegated_storage_access` since + ## `test_bal_7702_delegation_create` already tests creation + tx_create = Transaction( + sender=sender, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle1, + nonce=1 if self_funded else 0, + signer=alice, + ) + ], + ) + + tx_update = Transaction( + nonce=2 if self_funded else 1, + sender=sender, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle2, + nonce=3 if self_funded else 1, + signer=alice, + ) + ], + ) + + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=2 if self_funded else 1), + BalNonceChange(tx_index=2, post_nonce=4 if self_funded else 2), + ], + code_changes=[ + BalCodeChange(tx_index=1, new_code=Spec7702.delegation_designation(oracle1)), + BalCodeChange(tx_index=2, new_code=Spec7702.delegation_designation(oracle2)), + ], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=10), + BalBalanceChange(tx_index=2, post_balance=20), + ] + ), + # Both delegation targets must not be present in BAL + # the account is never accessed + oracle1: None, + oracle2: None, + } + + # For sponsored variant, relayer must also be included in BAL + if not self_funded: + account_expectations[relayer] = BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1), + BalNonceChange(tx_index=2, post_nonce=2), + ], + ) + + block = Block( + txs=[tx_create, tx_update], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + + post = { + # Finally Alice's account should be delegated to oracle2 + alice: Account( + nonce=4 if self_funded else 2, code=Spec7702.delegation_designation(oracle2) + ), + # Bob receives 20 wei in total + bob: Account(balance=20), + } + + # For sponsored variant, include relayer in post state + if not self_funded: + post.update({relayer: Account(nonce=2)}) + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +@pytest.mark.parametrize( + "self_funded", + [ + pytest.param(False, id="sponsored"), + pytest.param(True, id="self_funded"), + ], +) +def test_bal_7702_delegation_clear( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + self_funded: bool, +): + """Ensure BAL captures clearing of EOA delegation.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + if not self_funded: + relayer = pre.fund_eoa() + sender = relayer + else: + sender = alice + + oracle = pre.deploy_contract(code=Op.STOP) + abyss = Spec7702.RESET_DELEGATION_ADDRESS + + ## Perhaps create pre existing delegation, + ## see `test_bal_7702_delegated_storage_access` since + ## `test_bal_7702_delegation_create` already tests creation + tx_create = Transaction( + sender=sender, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle, + nonce=1 if self_funded else 0, + signer=alice, + ) + ], + ) + + tx_clear = Transaction( + nonce=2 if self_funded else 1, + sender=sender, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=abyss, + nonce=3 if self_funded else 1, + signer=alice, + ) + ], + ) + + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=2 if self_funded else 1), + BalNonceChange(tx_index=2, post_nonce=4 if self_funded else 2), + ], + code_changes=[ + BalCodeChange(tx_index=1, new_code=Spec7702.delegation_designation(oracle)), + BalCodeChange(tx_index=2, new_code=""), + ], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=10), + BalBalanceChange(tx_index=2, post_balance=20), + ] + ), + # Both delegation targets must not be present in BAL + # the account is never accessed + oracle: None, + abyss: None, + } + + # For sponsored variant, relayer must also be included in BAL + if not self_funded: + account_expectations[relayer] = BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1), + BalNonceChange(tx_index=2, post_nonce=2), + ], + ) + + block = Block( + txs=[tx_create, tx_clear], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + + post = { + # Finally Alice's account should NOT have any code + alice: Account(nonce=4 if self_funded else 2, code=""), + # Bob receives 20 wei in total + bob: Account(balance=20), + } + + # For sponsored variant, include relayer in post state + if not self_funded: + post.update({relayer: Account(nonce=2)}) + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +def test_bal_7702_delegated_storage_access( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """ + Ensure BAL captures storage operations when calling a delegated + EIP-7702 account. + """ + # Oracle contract that reads from slot 0x01 and writes to slot 0x02 + oracle = pre.deploy_contract(code=Op.SLOAD(0x01) + Op.PUSH1(0x42) + Op.PUSH1(0x02) + Op.SSTORE) + bob = pre.fund_eoa() + + alice = pre.deploy_contract(nonce=0x1, code=Spec7702.delegation_designation(oracle), balance=0) + + tx = Transaction( + sender=bob, + to=alice, # Bob calls Alice (delegated account) + value=10, + gas_limit=1_000_000, + gas_price=0xA, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)], + storage_changes=[ + BalStorageSlot( + slot=0x02, + slot_changes=[BalStorageChange(tx_index=1, post_value=0x42)], + ) + ], + storage_reads=[0x01], + ), + bob: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + # Oracle appears in BAL due to account access + # (delegation target) + oracle: BalAccountExpectation.empty(), + } + ), + ) + + post = { + alice: Account( + balance=10, + storage={0x02: 0x42}, + ), + bob: Account(nonce=1), + } + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +def test_bal_7702_invalid_nonce_authorization( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL handles failed authorization due to wrong nonce.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + relayer = pre.fund_eoa() + oracle = pre.deploy_contract(code=Op.STOP) + + tx = Transaction( + sender=relayer, # Sponsored transaction + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle, + nonce=5, # Wrong nonce - Alice's actual nonce is 0 + signer=alice, + ) + ], + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + # Ensuring silent fail + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)] + ), + relayer: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + # Alice's account was marked warm but no changes were made + alice: BalAccountExpectation.empty(), + # Oracle must NOT be present - authorization failed so + # account is never accessed + oracle: None, + } + ), + ) + + post = { + relayer: Account(nonce=1), + bob: Account(balance=10), + } + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +def test_bal_7702_invalid_chain_id_authorization( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL handles failed authorization due to wrong chain id.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + relayer = pre.fund_eoa() + oracle = pre.deploy_contract(code=Op.STOP) + + tx = Transaction( + sender=relayer, # Sponsored transaction + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + chain_id=999, # Wrong chain id + address=oracle, + nonce=0, + signer=alice, + ) + ], + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + # Alice's account must not be read because + # authorization fails before loading her account + alice: None, + # Ensuring silent fail + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)] + ), + relayer: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + # Oracle must NOT be present - authorization failed so + # account never accessed + oracle: None, + } + ), + ) + + post = { + relayer: Account(nonce=1), + bob: Account(balance=10), + } + + blockchain_test( + # Set chain id here + # so this test holds if the default is + # ever changed + chain_id=1, + pre=pre, + blocks=[block], + post=post, + ) + + +@pytest.mark.with_all_call_opcodes +def test_bal_7702_delegated_via_call_opcode( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + call_opcode: Op, +): + """ + Ensure BAL captures delegation target when a contract uses *CALL + opcodes to call a delegated account. + """ + # `oracle` contract that just returns successfully + oracle = pre.deploy_contract(code=Op.STOP) + + # `alice` is a delegated account pointing to oracle + alice = pre.deploy_contract( + nonce=1, + code=Spec7702.delegation_designation(oracle), + balance=0, + ) + + # caller contract that uses `call_opcode` to call `alice` + caller = pre.deploy_contract(code=(call_opcode(address=alice) + Op.STOP)) + + bob = pre.fund_eoa() + tx = Transaction( + sender=bob, + to=caller, # `bob` calls caller contract + gas_limit=10_000_000, + gas_price=0xA, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + bob: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + caller: BalAccountExpectation.empty(), + # `alice` is accessed due to being the call target + alice: BalAccountExpectation.empty(), + # `oracle` appears in BAL due to delegation target access + oracle: BalAccountExpectation.empty(), + } + ), + ) + + post = {bob: Account(nonce=1)} + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md index 033e37d5cab..714de365a64 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -18,7 +18,6 @@ | `test_bal_2930_slot_listed_but_untouched` | Ensure BAL excludes listed but untouched storage slots | Alice sends tx with EIP-2930 access list including `(PureCalculator, slot=0x01)`; PureCalculator executes pure arithmetic (adding two numbers) without touching slot `0x01` | BAL MUST NOT include any entry for PureCalculator's slot `0x01` because it doesn't access state | ✅ Completed | | `test_bal_2930_slot_listed_and_unlisted_writes` | Ensure BAL includes storage writes regardless of access list presence | Alice sends tx with EIP-2930 access list including `(StorageWriter, slot=0x01)`; StorageWriter executes `SSTORE` to slots `0x01` and `0x02` | BAL MUST include `storage_changes` for StorageWriter's slots `0x01` and `0x02` | ✅ Completed | | `test_bal_2930_slot_listed_and_unlisted_reads` | Ensure BAL includes storage reads regardless of access list presence | Alice sends tx with EIP-2930 access list including `(StorageReader, slot=0x01)`; StorageReader executes `SLOAD` from slots `0x01` and `0x02` | BAL MUST include `storage_reads` for StorageReader's slots `0x01` and `0x02` | ✅ Completed | -| `test_bal_7702_delegated_create` | BAL tracks EIP-7702 delegation indicator write and contract creation | Alice sends a type-4 (7702) tx authorizing herself to delegate to `Deployer` code which executes `CREATE` | BAL MUST include for **Alice**: `code_changes` (delegation indicator), `nonce_changes` (increment from 7702 processing), and `balance_changes` (post-gas). For **Child**: `code_changes` (runtime bytecode) and `nonce_changes = 1`. | 🟡 Planned | | `test_bal_self_transfer` | BAL handles self-transfers correctly | Alice sends `100 wei` to Alice | BAL **MUST** include one entry for Alice with `balance_changes` reflecting gas cost only (value cancels out) and nonce change. | ✅ Completed | | `test_bal_zero_value_transfer` | BAL handles zero-value transfers correctly | Alice sends `0 wei` to Bob | BAL **MUST** include Alice with `balance_changes` (gas cost only) and nonce change, and Bob in `account_changes` with empty `balance_changes`. | ✅ Completed | | `test_bal_system_contracts_2935_4788` | BAL includes pre-exec system writes for parent hash & beacon root | Build a block with `N` normal txs; 2935 & 4788 active | BAL MUST include `HISTORY_STORAGE_ADDRESS` (EIP-2935) and `BEACON_ROOTS_ADDRESS` (EIP-4788) with `storage_changes` to ring-buffer slots; each write uses `tx_index = N` (system op). | 🟡 Planned | @@ -32,5 +31,13 @@ | `test_bal_precompile_funded_then_called` | BAL records precompile with balance change (fund) and access (call) | **Tx0**: Alice sends `1 ETH` to `ecrecover` (0x01). **Tx1**: Alice (or Bob) calls `ecrecover` with valid input and `0 ETH`. | BAL **MUST** include address `0x01` with `balance_changes` (from Tx0). No `storage_changes` or `code_changes`. | 🟡 Planned | | `test_bal_precompile_call_only` | BAL records precompile when called with no balance change | Alice calls `ecrecover` (0x01) with a valid input, sending **0 ETH**. | BAL **MUST** include address `0x01` in access list, with **no** `balance_changes`, `storage_changes`, or `code_changes`. | 🟡 Planned | | `test_bal_fully_unmutated_account` | Ensure BAL captures account that has zero net mutations | Alice sends 0 wei to `Oracle` which writes same pre-existing value to storage | BAL MUST include Alice with `nonce_changes` and balance changes (gas), `Oracle` with `storage_reads` for accessed slot but empty `storage_changes`. | ✅ Completed | +| `test_bal_7702_delegated_create` | BAL tracks EIP-7702 delegation indicator write and contract creation | Alice sends a type-4 (7702) tx authorizing herself to delegate to `Deployer` code which executes `CREATE` | BAL MUST include for **Alice**: `code_changes` (delegation indicator), `nonce_changes` (increment from 7702 processing), and `balance_changes` (post-gas). For **Child**: `code_changes` (runtime bytecode) and `nonce_changes = 1`. | 🟡 Planned | +| `test_bal_7702_delegation_create` | Ensure BAL captures creation of EOA delegation | Alice authorizes delegation to contract `Oracle`. Transaction sends 10 wei to Bob. Two variants: (1) Self-funded: Alice sends 7702 tx herself. (2) Sponsored: `Relayer` sends 7702 tx on Alice's behalf. | BAL **MUST** include Alice: `code_changes` (delegation designation `0xef0100\|\|address(Oracle)`),`nonce_changes` (increment). Bob: `balance_changes` (receives 10 wei). For sponsored variant, BAL **MUST** also include `Relayer`:`nonce_changes`.`Oracle` **MUST NOT** be present in BAL - the account is never accessed. | ✅ Completed | +| `test_bal_7702_delegation_update` | Ensure BAL captures update of existing EOA delegation | Alice first delegates to `Oracle1`, then in second tx updates delegation to `Oracle2`. Each transaction sends 10 wei to Bob. Two variants: (1) Self-funded: Alice sends both 7702 txs herself. (2) Sponsored: `Relayer` sends both 7702 txs on Alice's behalf. | BAL **MUST** include Alice: first tx has `code_changes` (delegation designation `0xef0100\|\|address(Oracle1)`),`nonce_changes`. Second tx has`code_changes` (delegation designation `0xef0100\|\|address(Oracle2)`),`nonce_changes`. Bob:`balance_changes` (receives 10 wei on each tx). For sponsored variant, BAL **MUST** also include `Relayer`:`nonce_changes` for both transactions. `Oracle1` and `Oracle2` **MUST NOT** be present in BAL - accounts are never accessed. | ✅ Completed | +| `test_bal_7702_delegation_clear` | Ensure BAL captures clearing of EOA delegation | Alice first delegates to `Oracle`, then in second tx clears delegation by authorizing to `0x0` address. Each transaction sends 10 wei to Bob. Two variants: (1) Self-funded: Alice sends both 7702 txs herself. (2) Sponsored: `Relayer` sends both 7702 txs on Alice's behalf. | BAL **MUST** include Alice: first tx has `code_changes` (delegation designation `0xef0100\|\|address(Oracle)`), `nonce_changes`. Second tx has `code_changes` (empty code - delegation cleared), `nonce_changes`. Bob: `balance_changes` (receives 10 wei on each tx). For sponsored variant, BAL **MUST** also include `Relayer`: `nonce_changes` for both transactions. `Oracle` and `0x0` address **MUST NOT** be present in BAL - accounts are never accessed. | ✅ Completed | +| `test_bal_7702_delegated_storage_access` | Ensure BAL captures storage operations when calling a delegated EIP-7702 account | Alice has delegated her account to `Oracle`. `Oracle` contract contains code that reads from storage slot `0x01` and writes to storage slot `0x02`. Bob sends 10 wei to Alice (the delegated account), which executes `Oracle`'s code. | BAL **MUST** include Alice: `balance_changes` (receives 10 wei), `storage_changes` for slot `0x02` (write operation performed in Alice's storage), `storage_reads` for slot `0x01` (read operation from Alice's storage). Bob: `nonce_changes` (sender), `balance_changes` (loses 10 wei plus gas costs). `Oracle` (account access). | ✅ Completed | +| `test_bal_7702_invalid_nonce_authorization` | Ensure BAL handles failed authorization due to wrong nonce | `Relayer` sends sponsored transaction to Bob (10 wei transfer succeeds) but Alice's authorization to delegate to `Oracle` uses incorrect nonce, causing silent authorization failure | BAL **MUST** include Alice with empty changes (account access), Bob with `balance_changes` (receives 10 wei), Relayer with `nonce_changes`. **MUST NOT** include `Oracle` (authorization failed, no delegation) | ✅ Completed | +| `test_bal_7702_invalid_chain_id_authorization` | Ensure BAL handles failed authorization due to wrong chain id | `Relayer` sends sponsored transaction to Bob (10 wei transfer succeeds) but Alice's authorization to delegate to `Oracle` uses incorrect chain id, causing authorization failure before account access | BAL **MUST** include Bob with `balance_changes` (receives 10 wei), Relayer with `nonce_changes`. **MUST NOT** include Alice (authorization fails before loading account) or `Oracle` (authorization failed, no delegation) | ✅ Completed | +| `test_bal_7702_delegated_via_call_opcode` | Ensure BAL captures delegation target when a contract uses *CALL opcodes to call a delegated account | Pre-deployed contract `Alice` delegated to `Oracle`. `Caller` contract uses CALL/CALLCODE/DELEGATECALL/STATICCALL to call `Alice`. Bob sends transaction to `Caller`. | BAL **MUST** include Bob: `nonce_changes`. `Caller`: empty changes (account access). `Alice`: empty changes (account access - delegated account being called). `Oracle`: empty changes (delegation target access). | ✅ Completed | > â„šī¸ Scope describes whether a test spans a single transaction (`tx`) or entire block (`blk`).