Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2090,6 +2090,98 @@ def test_bal_multiple_storage_writes_same_slot(
)


@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"),
],
)
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: Dict[Address, BalAccountExpectation | None] = {
alice: BalAccountExpectation(
nonce_changes=[BalNonceChange(tx_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}),
},
)


def test_bal_create_transaction_empty_code(
pre: Alloc,
blockchain_test: BlockchainTestFiller,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
| `test_bal_oog_7702_delegated_warm_cold` | Ensure BAL handles OOG during EIP-7702 delegated account loading when first account is warm, second is cold | Alice calls warm delegated account Bob (7702) which delegates to cold `TargetContract` with insufficient gas for second cold load | BAL **MUST** include Bob in `account_changes` (warm load succeeds) but **MUST NOT** include `TargetContract` (cold load fails due to OOG) | 🟑 Planned |
| `test_bal_multiple_balance_changes_same_account` | Ensure BAL tracks multiple balance changes to same account across transactions | Alice funds Bob (starts at 0) in tx0 with exact amount needed. Bob spends everything in tx1 to Charlie. Bob's balance: 0 β†’ funding_amount β†’ 0 | BAL **MUST** include Bob with two `balance_changes`: one at txIndex=1 (receives funds) and one at txIndex=2 (balance returns to 0). This tests balance tracking across two transactions. | βœ… Completed |
| `test_bal_multiple_storage_writes_same_slot` | Ensure BAL tracks multiple writes to same storage slot across transactions | Alice calls contract 3 times in same block. Contract increments slot 1 on each call: 0 β†’ 1 β†’ 2 β†’ 3 | BAL **MUST** include contract with slot 1 having three `slot_changes`: txIndex=1 (value 1), txIndex=2 (value 2), txIndex=3 (value 3). Each transaction's write must be recorded separately. | βœ… Completed |
| `test_bal_nested_delegatecall_storage_writes_net_zero` | Ensure BAL correctly filters net-zero storage changes across nested DELEGATECALL frames | Parametrized by nesting depth (1-3). Root contract has slot 0 = 1. Each frame writes a different intermediate value via DELEGATECALL chain, deepest frame writes back to original value (1). Example depth=2: 1 β†’ 2 β†’ 3 β†’ 1 | BAL **MUST** include root contract with `storage_reads` for slot 0 but **MUST NOT** include `storage_changes` (net-zero). All delegate contracts **MUST** have empty changes. Tests that frame merging correctly removes parent's intermediate writes when child reverts to pre-tx value. | βœ… Completed |
| `test_bal_create_transaction_empty_code` | Ensure BAL does not record spurious code changes for CREATE transaction deploying empty code | Alice sends CREATE transaction with empty initcode (deploys code `b""`). Contract address gets nonce = 1 and code = `b""`. | BAL **MUST** include Alice with `nonce_changes` and created contract with `nonce_changes` but **MUST NOT** include `code_changes` for contract (setting `b"" -> b""` is net-zero). | βœ… Completed |
| `test_bal_cross_tx_storage_revert_to_zero` | Ensure BAL captures storage changes when tx2 reverts slot back to original value (blobhash regression test) | Alice sends tx1 writing slot 0=0xABCD (from 0x0), then tx2 writing slot 0=0x0 (back to original) | BAL **MUST** include contract with slot 0 having two `slot_changes`: txIndex=1 (0xABCD) and txIndex=2 (0x0). Cross-transaction net-zero **MUST NOT** be filtered. | βœ… Completed |
| `test_bal_create_contract_init_revert` | Ensure BAL correctly handles CREATE when parent call reverts | Caller calls factory, factory executes CREATE (succeeds), then factory REVERTs rolling back the CREATE | BAL **MUST** include Alice with `nonce_changes`. Caller and factory with no changes (reverted). Created contract address appears in BAL but **MUST NOT** have `nonce_changes` or `code_changes` (CREATE was rolled back). Contract address **MUST NOT** exist in post-state. | βœ… Completed |
Expand Down
Loading