diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cd57c5f56bb..e521e96f707 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -42,6 +42,7 @@ Test fixtures for use by clients are available for each release on the [Github r - ✨ Add EIP-7928 successful and OOG single-opcode tests ([#2118](https://github.com/ethereum/execution-spec-tests/pull/2118)). - ✨ Add EIP-7928 tests for EIP-2930 interactions ([#2167](https://github.com/ethereum/execution-spec-tests/pull/2167)). - ✨ Add EIP-7928 tests for NOOP operations ([#2178](https://github.com/ethereum/execution-spec-tests/pull/2178)). +- ✨ Add EIP-7928 tests for net-zero balance transfers ([#2280](https://github.com/ethereum/execution-spec-tests/pull/2280)). ## [v5.0.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v5.0.0) - 2025-09-05 diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index ad19a2687b5..c71a40c4f19 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -947,6 +947,109 @@ def test_bal_zero_value_transfer( blockchain_test(pre=pre, blocks=[block], post={}) +@pytest.mark.parametrize( + "initial_balance,transfer_amount,transfer_mechanism", + [ + pytest.param(0, 0, "call", id="zero_balance_zero_transfer_call"), + pytest.param(0, 0, "selfdestruct", id="zero_balance_zero_transfer_selfdestruct"), + pytest.param(1, 1, "call", id="nonzero_balance_net_zero"), + pytest.param(100, 50, "call", id="larger_balance_net_zero"), + ], +) +def test_bal_net_zero_balance_transfer( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + initial_balance: int, + transfer_amount: int, + transfer_mechanism: str, +): + """ + Test that BAL does not record balance changes when net change is zero. + + A contract starts with `initial_balance`, receives `transfer_amount` + (increasing its balance), then sends `transfer_amount` to a recipient + (decreasing its balance back to `initial_balance`). The net change is zero, + so BAL should not record any balance changes for this contract. + + The contract verifies this by reading its own balance with SELFBALANCE, + storing it in slot 0, then sending that amount to the recipient. + """ + alice = pre.fund_eoa() + recipient = pre.fund_eoa(amount=0) + + net_zero_bal_contract_code = ( + Op.SSTORE(0, Op.SELFBALANCE) + Op.SELFDESTRUCT(recipient) + if transfer_mechanism == "selfdestruct" + # store current balance in slot 0 + else ( + Op.SSTORE(0, Op.SELFBALANCE) + # send only the `transfer_amount` received to recipient (net zero) + + Op.CALL(0, recipient, Op.CALLVALUE, 0, 0, 0, 0) + + Op.STOP + ) + ) + net_zero_bal_contract = pre.deploy_contract( + code=net_zero_bal_contract_code, balance=initial_balance + ) + + tx = Transaction( + sender=alice, + to=net_zero_bal_contract, + value=transfer_amount, + gas_limit=1_000_000, + gas_price=0xA, + ) + + expected_balance_in_slot = initial_balance + transfer_amount + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + net_zero_bal_contract: BalAccountExpectation( + # receives transfer_amount and sends transfer_amount away + # (net-zero change) + balance_changes=[], + storage_reads=[0x00] if expected_balance_in_slot == 0 else [], + storage_changes=[ + BalStorageSlot( + slot=0x00, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=expected_balance_in_slot) + ], + ) + ] + if expected_balance_in_slot > 0 + else [], + ), + # recipient receives transfer_amount + recipient: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=transfer_amount)] + if transfer_amount > 0 + else [], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={ + net_zero_bal_contract: Account( + balance=initial_balance, + storage={0x00: expected_balance_in_slot} if expected_balance_in_slot > 0 else {}, + ), + recipient: Account(balance=transfer_amount) + if transfer_amount > 0 + else Account.NONEXISTENT, + }, + ) + + def test_bal_pure_contract_call( pre: Alloc, blockchain_test: BlockchainTestFiller, 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..b54f67806c0 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -21,6 +21,7 @@ | `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_net_zero_balance_transfer` | BAL includes accounts with net-zero balance change but excludes them from balance changes | Contract receives and sends same amount to recipient using CALL or SELFDESTRUCT | BAL **MUST** include contract in `account_changes` without `balance_changes` (net zero). BAL **MUST** record non-zero `balance_changes` for recipient. | ✅ 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 | | `test_bal_system_dequeue_withdrawals_eip7002` | BAL tracks post-exec system dequeues for withdrawals | Pre-populate EIP-7002 withdrawal requests; produce a block where dequeues occur | BAL MUST include the 7002 system contract with `storage_changes` (queue head/tail slots 0–3) using `tx_index = len(txs)` and balance changes for withdrawal recipients. | 🟡 Planned | | `test_bal_system_dequeue_consolidations_eip7251` | BAL tracks post-exec system dequeues for consolidations | Pre-populate EIP-7251 consolidation requests; produce a block where dequeues occur | BAL MUST include the 7251 system contract with `storage_changes` (queue slots 0–3) using `tx_index = len(txs)`. | 🟡 Planned |