From 8885d5dd14e40cac63997d6b1917e6e6cd06e7de Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:05:19 +0100 Subject: [PATCH] fix(tests,evm): Reserve bal. & MIP-4 vs initcode execution Co-Authored-By: Claude opus-4-6 --- .../forks/monad_eight/vm/interpreter.py | 26 +- .../forks/monad_nine/vm/interpreter.py | 56 +++- .../precompiled_contracts/reserve_balance.py | 5 +- .../eip6780_selfdestruct/test_selfdestruct.py | 56 +++- .../reserve_balance/test_transfers.py | 88 ++++-- .../test_transfers.py | 280 +++++++++++++++++- 6 files changed, 452 insertions(+), 59 deletions(-) diff --git a/src/ethereum/forks/monad_eight/vm/interpreter.py b/src/ethereum/forks/monad_eight/vm/interpreter.py index a05ee10851f..ad9c65ecc0a 100644 --- a/src/ethereum/forks/monad_eight/vm/interpreter.py +++ b/src/ethereum/forks/monad_eight/vm/interpreter.py @@ -325,7 +325,21 @@ def process_message(message: Message) -> Evm: if message.depth == 0 and message.tx_env.index_in_block is not None: for addr in set(state._main_trie._data.keys()): acc = get_account(state, addr) - if acc.code == b"" or is_valid_delegation(acc.code): + # For creation txs, code hasn't been set yet on the new + # contract (set_code runs after process_message returns). Use + # evm.output which holds the code to be deployed. + if ( + isinstance(message.target, Bytes0) + and addr == message.current_target + ): + code = evm.output + else: + code = acc.code + + # NOTE: this also matches initcode ending with empty code + # deployments via `Op.STOP` or `Op.RETURN(0, 0)`, but this + # aligns with Monad EVM implementation. + if code == b"" or is_valid_delegation(code): original_balance = get_balance_original(state, addr) if message.tx_env.origin == addr: # gas_fees already deducted, need to re-add if sender @@ -339,9 +353,13 @@ def process_message(message: Message) -> Evm: threshold = reserve - gas_fees else: threshold = RESERVE_BALANCE - is_exception = not is_sender_authority( - state, addr - ) and not is_valid_delegation(acc.code) + + is_exception = ( + message.tx_env.origin == addr + and not is_sender_authority(state, addr) + and not is_valid_delegation(code) + ) + if ( acc.balance < original_balance and acc.balance < threshold diff --git a/src/ethereum/forks/monad_nine/vm/interpreter.py b/src/ethereum/forks/monad_nine/vm/interpreter.py index d407775022f..ed608f6f075 100644 --- a/src/ethereum/forks/monad_nine/vm/interpreter.py +++ b/src/ethereum/forks/monad_nine/vm/interpreter.py @@ -32,7 +32,6 @@ from ..blocks import Log from ..fork_types import Address from ..state import ( - State, account_has_code_or_nonce, account_has_storage, begin_transaction, @@ -47,7 +46,7 @@ rollback_transaction, set_code, ) -from ..vm import Message, TransactionEnvironment +from ..vm import Message from ..vm.eoa_delegation import ( get_delegated_code_address, is_valid_delegation, @@ -79,18 +78,49 @@ RESERVE_BALANCE = U256(10 * 10**18) # 10 MON -def is_reserve_balance_violated( - state: State, - tx_env: TransactionEnvironment, -) -> bool: +def is_reserve_balance_violated(evm: Evm) -> bool: """ Check if any EOA has violated the reserve balance constraint. Returns True if a violation is detected, False otherwise. """ + message = evm.message + state = message.block_env.state + tx_env = message.tx_env + + # Collect accounts_to_delete from all ancestor frames. accounts_to_delete + # only propagates upward on success (incorporate_child_on_success), so a + # child frame like a precompile call won't see deletions from its parent. + all_accounts_to_delete: Set[Address] = set() + current_evm = evm + while True: + all_accounts_to_delete.update(current_evm.accounts_to_delete) + if current_evm.message.parent_evm is not None: + current_evm = current_evm.message.parent_evm + else: + break + for addr in set(state._main_trie._data.keys()): + # Account SELFDESTRUCTed - skip explicitly. + if addr in all_accounts_to_delete: + continue + acc = get_account(state, addr) - if acc.code == b"" or is_valid_delegation(acc.code): + # For creation txs, code hasn't been set yet on the new contract + # (set_code runs after process_message returns). Use evm.output which + # holds the code to be deployed. + if ( + isinstance(message.target, Bytes0) + and addr == message.current_target + ): + code = evm.output + else: + code = acc.code + + # NOTE: this also matches initcode ending with empty code deployments + # via `Op.STOP` or `Op.RETURN(0, 0)`, AND check made during initcode + # execution, but this aligns with Monad EVM implementation. + if code == b"" or is_valid_delegation(code): original_balance = get_balance_original(state, addr) if tx_env.origin == addr: # gas_fees already deducted, need to re-add if sender @@ -101,9 +131,13 @@ def is_reserve_balance_violated( threshold = reserve - gas_fees else: threshold = RESERVE_BALANCE - is_exception = not is_sender_authority( - state, addr - ) and not is_valid_delegation(acc.code) + + is_exception = ( + message.tx_env.origin == addr + and not is_sender_authority(state, addr) + and not is_valid_delegation(code) + ) + if ( acc.balance < original_balance and acc.balance < threshold @@ -368,7 +402,7 @@ def process_message(message: Message) -> Evm: else: # FIXME: index_in_block is a proxy for not being a system tx if message.depth == 0 and message.tx_env.index_in_block is not None: - if is_reserve_balance_violated(state, message.tx_env): + if is_reserve_balance_violated(evm): rollback_transaction(state, transient_storage) evm.error = RevertOnReserveBalance() return evm diff --git a/src/ethereum/forks/monad_nine/vm/precompiled_contracts/reserve_balance.py b/src/ethereum/forks/monad_nine/vm/precompiled_contracts/reserve_balance.py index 7c0738cd674..12fe2c1d050 100644 --- a/src/ethereum/forks/monad_nine/vm/precompiled_contracts/reserve_balance.py +++ b/src/ethereum/forks/monad_nine/vm/precompiled_contracts/reserve_balance.py @@ -86,9 +86,6 @@ def reserve_balance(evm: Evm) -> None: raise RevertInMonadPrecompile # OPERATION - violation = is_reserve_balance_violated( - evm.message.block_env.state, - evm.message.tx_env, - ) + violation = is_reserve_balance_violated(evm) # Return bool encoded as uint256 (32 bytes) evm.output = U256(1 if violation else 0).to_be_bytes32() diff --git a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py index 731e9203208..9cba5bbfeb1 100644 --- a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py +++ b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py @@ -26,7 +26,7 @@ Transaction, compute_create_address, ) -from execution_testing.forks import Cancun +from execution_testing.forks import MONAD_EIGHT, Cancun REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6780.md" REFERENCE_SPEC_VERSION = "1b6a0e94cc47e859b9866e570391cf37dc55059a" @@ -378,6 +378,7 @@ def test_self_destructing_initcode( # in the same tx selfdestruct_contract_initial_balance: int, fork_extra_gas: int, + fork: Fork, ) -> None: """ Test that a contract can self-destruct in its initcode. @@ -481,15 +482,26 @@ def test_self_destructing_initcode( entry_code_address = tx.created_contract - post: Dict[Address, Account] = { - entry_code_address: Account( - storage=entry_code_storage, - ), - selfdestruct_contract_address: Account.NONEXISTENT, # type: ignore - sendall_recipient_addresses[0]: Account( - balance=sendall_amount, storage={0: 1} - ), - } + reverted = ( + fork == MONAD_EIGHT and selfdestruct_contract_initial_balance > 0 + ) + + if reverted: + post: Dict[Address, Account] = { + selfdestruct_contract_address: Account( + balance=selfdestruct_contract_initial_balance, + ), + } + else: + post = { + entry_code_address: Account( + storage=entry_code_storage, + ), + selfdestruct_contract_address: Account.NONEXISTENT, # type: ignore + sendall_recipient_addresses[0]: Account( + balance=sendall_amount, storage={0: 1} + ), + } state_test(pre=pre, post=post, tx=tx) @@ -509,6 +521,7 @@ def test_self_destructing_initcode_create_tx( sendall_recipient_addresses: List[Address], selfdestruct_contract_initial_balance: int, fork_extra_gas: int, + fork: Fork, ) -> None: """ Use a Create Transaction to execute a self-destructing initcode. @@ -537,12 +550,23 @@ def test_self_destructing_initcode_create_tx( # contract sendall_amount = selfdestruct_contract_initial_balance + tx_value - post: Dict[Address, Account] = { - selfdestruct_contract_address: Account.NONEXISTENT, # type: ignore - sendall_recipient_addresses[0]: Account( - balance=sendall_amount, storage={0: 1} - ), - } + reverted = ( + fork == MONAD_EIGHT and selfdestruct_contract_initial_balance > 0 + ) + + if reverted: + post: Dict[Address, Account] = { + selfdestruct_contract_address: Account( + balance=selfdestruct_contract_initial_balance, + ), + } + else: + post = { + selfdestruct_contract_address: Account.NONEXISTENT, # type: ignore + sendall_recipient_addresses[0]: Account( + balance=sendall_amount, storage={0: 1} + ), + } state_test(pre=pre, post=post, tx=tx) diff --git a/tests/monad_eight/reserve_balance/test_transfers.py b/tests/monad_eight/reserve_balance/test_transfers.py index 397ac6b637a..f79456696b8 100644 --- a/tests/monad_eight/reserve_balance/test_transfers.py +++ b/tests/monad_eight/reserve_balance/test_transfers.py @@ -18,9 +18,11 @@ Op, Transaction, ) +from execution_testing.forks import MONAD_EIGHT from execution_testing.forks.helpers import Fork from execution_testing.test_types.helpers import compute_create_address from execution_testing.tools.tools_code.generators import Initcode +from execution_testing.vm.bytecode import Bytecode from tests.prague.eip7702_set_code_tx.spec import Spec as Spec7702 @@ -1255,7 +1257,10 @@ def test_access_lists( ) @pytest.mark.parametrize("pre_delegated", [True, False]) @pytest.mark.parametrize("new_address_pre_funded", [True, False]) -@pytest.mark.parametrize("selfdestruct", [True, False]) +@pytest.mark.parametrize( + "selfdestruct,deploy_code", + [(True, None), (False, Bytecode()), (False, Op.STOP)], +) @pytest.mark.with_all_contract_creating_tx_types def test_creation_tx( blockchain_test: BlockchainTestFiller, @@ -1266,12 +1271,14 @@ def test_creation_tx( pre_delegated: bool, new_address_pre_funded: bool, selfdestruct: bool, + deploy_code: Bytecode | None, tx_type: int, fork: Fork, ) -> None: """ Test reserve balance violations for creation txs to: null. """ + assert selfdestruct == (deploy_code is None) pre_fund_value = 0 if pre_delegated: sender = pre.fund_eoa(balance, delegation=Address(0x1111)) @@ -1282,7 +1289,7 @@ def test_creation_tx( initcode = ( Op.SELFDESTRUCT(address=selfdestruct_target) if selfdestruct - else Initcode(deploy_code=Op.STOP) + else Initcode(deploy_code=deploy_code) ) tx_1 = Transaction( @@ -1305,13 +1312,15 @@ def test_creation_tx( ), ) - reverted = violation and pre_delegated + reverted = (violation and pre_delegated) or ( + fork == MONAD_EIGHT and selfdestruct and new_address_pre_funded + ) blockchain_test( pre=pre, post={ new_address: Account( - code=Op.STOP, + code=deploy_code, balance=value + pre_fund_value, ) if not reverted and not selfdestruct @@ -1395,7 +1404,10 @@ def test_contract_unrestricted( ) @pytest.mark.parametrize("pre_delegated", [True, False]) @pytest.mark.parametrize("pre_funded", [True, False]) -@pytest.mark.parametrize("selfdestruct", [True, False]) +@pytest.mark.parametrize( + "selfdestruct,deploy_code", + [(True, None), (False, Bytecode()), (False, Op.STOP)], +) @pytest.mark.with_all_create_opcodes def test_contract_unrestricted_with_create( blockchain_test: BlockchainTestFiller, @@ -1405,6 +1417,7 @@ def test_contract_unrestricted_with_create( pre_delegated: bool, pre_funded: bool, selfdestruct: bool, + deploy_code: Bytecode | None, create_opcode: Op, fork: Fork, ) -> None: @@ -1412,6 +1425,7 @@ def test_contract_unrestricted_with_create( Test reserve balance never affects contract spends done with a create opcode. """ + assert selfdestruct == (deploy_code is None) if pre_delegated: sender = pre.fund_eoa( Spec.RESERVE_BALANCE + balance, delegation=Address(0x1111) @@ -1424,7 +1438,7 @@ def test_contract_unrestricted_with_create( initcode = ( Op.SELFDESTRUCT(address=selfdestruct_target) if selfdestruct - else Initcode(deploy_code=Op.STOP) + else Initcode(deploy_code=deploy_code) ) initcode_bytes = initcode + b"\x00" * (32 - (len(initcode) % 32)) @@ -1457,7 +1471,7 @@ def test_contract_unrestricted_with_create( pre=pre, post={ factory_address: Account(storage=storage, balance=balance - value), - new_contract_address: Account(balance=value, code=Op.STOP) + new_contract_address: Account(balance=value, code=deploy_code) if not selfdestruct else None, selfdestruct_target: Account(balance=value) @@ -1662,7 +1676,10 @@ def test_contract_unrestricted_with_selfdestruct( @pytest.mark.parametrize("pre_delegated", [True, False]) @pytest.mark.with_all_create_opcodes @pytest.mark.parametrize("new_address_pre_funded", [True, False]) -@pytest.mark.parametrize("selfdestruct", [True, False]) +@pytest.mark.parametrize( + "selfdestruct,deploy_code", + [(True, None), (False, Bytecode()), (False, Op.STOP)], +) def test_contract_unrestricted_within_initcode( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -1672,12 +1689,14 @@ def test_contract_unrestricted_within_initcode( create_opcode: Op, new_address_pre_funded: bool, selfdestruct: bool, + deploy_code: Bytecode | None, fork: Fork, ) -> None: """ Test reserve balance never affects contract spends done from initcode context. """ + assert selfdestruct == (deploy_code is None) if pre_delegated: sender = pre.fund_eoa( Spec.RESERVE_BALANCE + balance, delegation=Address(0x1111) @@ -1696,7 +1715,7 @@ def test_contract_unrestricted_within_initcode( if selfdestruct else Initcode( initcode_prefix=Op.CALL(value=value, address=target), - deploy_code=Op.STOP, + deploy_code=deploy_code, ) ) initcode_bytes = initcode + b"\x00" * (32 - (len(initcode) % 32)) @@ -1726,7 +1745,16 @@ def test_contract_unrestricted_within_initcode( to=factory_address, sender=sender, ) - storage = {slot_code_worked: value_code_worked} + + reverted = ( + fork == MONAD_EIGHT and selfdestruct and new_address_pre_funded + ) or ( + deploy_code is not None + and len(deploy_code) == 0 + and new_address_pre_funded + and balance - value < Spec.RESERVE_BALANCE + ) + storage = {} if reverted else {slot_code_worked: value_code_worked} txs = [tx_1] if new_address_pre_funded: @@ -1741,14 +1769,16 @@ def test_contract_unrestricted_within_initcode( pre=pre, post={ factory_address: Account(storage=storage), - new_contract_address: Account( - balance=balance - value, code=Op.STOP - ) + new_contract_address: Account(balance=balance) + if reverted and new_address_pre_funded + else Account(balance=balance - value, code=deploy_code) if not selfdestruct else None, - target: Account(balance=value) if value != 0 else None, + target: Account(balance=value) + if value != 0 and not reverted + else None, selfdestruct_target: Account(balance=balance - value) - if selfdestruct + if selfdestruct and not reverted else None, }, blocks=[Block(txs=txs)], @@ -1765,7 +1795,10 @@ def test_contract_unrestricted_within_initcode( ) @pytest.mark.parametrize("pre_delegated", [True, False]) @pytest.mark.parametrize("new_address_pre_funded", [True, False]) -@pytest.mark.parametrize("selfdestruct", [True, False]) +@pytest.mark.parametrize( + "selfdestruct,deploy_code", + [(True, None), (False, Bytecode()), (False, Op.STOP)], +) @pytest.mark.with_all_contract_creating_tx_types def test_unrestricted_in_creation_tx_initcode( blockchain_test: BlockchainTestFiller, @@ -1775,6 +1808,7 @@ def test_unrestricted_in_creation_tx_initcode( pre_delegated: bool, new_address_pre_funded: bool, selfdestruct: bool, + deploy_code: Bytecode | None, tx_type: int, fork: Fork, ) -> None: @@ -1782,6 +1816,7 @@ def test_unrestricted_in_creation_tx_initcode( Test reserve balance never affects contract spends done from initcode context created via a creation tx with to: null. """ + assert selfdestruct == (deploy_code is None) if pre_delegated: sender = pre.fund_eoa( Spec.RESERVE_BALANCE + balance, delegation=Address(0x1111) @@ -1800,7 +1835,7 @@ def test_unrestricted_in_creation_tx_initcode( if selfdestruct else Initcode( initcode_prefix=Op.CALL(value=value, address=target), - deploy_code=Op.STOP, + deploy_code=deploy_code, ) ) @@ -1821,15 +1856,28 @@ def test_unrestricted_in_creation_tx_initcode( Transaction(to=new_address, value=balance, sender=pre.fund_eoa()), ) + reverted = ( + fork == MONAD_EIGHT and new_address_pre_funded and selfdestruct + ) or ( + deploy_code is not None + and len(deploy_code) == 0 + and new_address_pre_funded + and balance - value < Spec.RESERVE_BALANCE + ) + blockchain_test( pre=pre, post={ - new_address: Account(code=Op.STOP, balance=balance - value) + new_address: Account(balance=balance) + if reverted and new_address_pre_funded + else Account(code=deploy_code, balance=balance - value) if not selfdestruct else None, - target: Account(balance=value) if value != 0 else None, + target: Account(balance=value) + if value != 0 and not reverted + else None, selfdestruct_target: Account(balance=balance - value) - if selfdestruct + if selfdestruct and not reverted else None, }, blocks=[Block(txs=txs)], diff --git a/tests/monad_nine/mip4_checkreservebalance/test_transfers.py b/tests/monad_nine/mip4_checkreservebalance/test_transfers.py index 2dc4298f970..301c061d0a3 100644 --- a/tests/monad_nine/mip4_checkreservebalance/test_transfers.py +++ b/tests/monad_nine/mip4_checkreservebalance/test_transfers.py @@ -23,6 +23,7 @@ from execution_testing.forks.helpers import Fork from execution_testing.test_types.helpers import compute_create_address from execution_testing.tools.tools_code.generators import Initcode +from execution_testing.vm.bytecode import Bytecode from ...monad_eight.reserve_balance.helpers import ( Stage1Balance, @@ -1339,10 +1340,281 @@ def test_contract_unrestricted_with_selfdestruct( ) -# NOTE: skip: -# - test_contract_unrestricted_within_initcode -# - test_unrestricted_in_creation_tx_initcode -# as not providing additional coverage +@pytest.mark.parametrize( + ["value", "balance"], + [ + pytest.param(0, Spec.RESERVE_BALANCE, id="zero_value"), + pytest.param(1, Spec.RESERVE_BALANCE, id="non_zero_value"), + pytest.param(1, Spec.RESERVE_BALANCE + 1, id="non_zero_value_good"), + ], +) +@pytest.mark.with_all_create_opcodes +@pytest.mark.parametrize("new_address_pre_funded", [True, False]) +@pytest.mark.parametrize( + "selfdestruct,deploy_code", + [(True, None), (False, Bytecode()), (False, Op.STOP)], +) +def test_contract_unrestricted_within_initcode( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + refill_factory: RefillFactory, + value: int, + balance: int, + create_opcode: Op, + new_address_pre_funded: bool, + selfdestruct: bool, + deploy_code: Bytecode | None, + fork: Fork, +) -> None: + """ + Test dippedIntoReserve() for contract spends done from initcode + context via CREATE/CREATE2. + + Checks dippedIntoReserve() twice: + 1. During initcode, right after the spend (via checker) + 2. After initcode exits, in the factory (after CREATE) + """ + assert selfdestruct == (deploy_code is None) + refill_call = refill_factory() + + sender = pre.fund_eoa(Spec.RESERVE_BALANCE + balance) + + target = Address(0x1231) + selfdestruct_target = Address(0x5656) + + # Auxiliary contract: persists dippedIntoReserve() result + # during initcode (first check). + checker = pre.deploy_contract( + Op.SSTORE(slot_violation_result, call_dipped_into_reserve()) + ) + + common_initcode = Op.CALL(value=value, address=target) + Op.CALL( + address=checker + ) + if selfdestruct: + initcode = common_initcode + Op.SELFDESTRUCT( + address=selfdestruct_target + ) + else: + initcode = Initcode( + initcode_prefix=common_initcode, + deploy_code=deploy_code, + ) + initcode_size = len(initcode) + + # Factory: copy initcode from calldata into memory, CREATE, + # then second dippedIntoReserve() check after initcode + # exits, then refill to prevent end-of-tx revert. + # Save CREATE result at memory[initcode_size] so + # refill_call can read it back via MLOAD. + factory = ( + Op.CALLDATACOPY(0, 0, initcode_size) + + Op.MSTORE( + initcode_size, + create_opcode( + value=balance if not new_address_pre_funded else 0, + size=initcode_size, + ), + ) + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.SSTORE(slot_violation_result, call_dipped_into_reserve()) + + refill_call(Op.MLOAD(initcode_size)) + ) + factory_address = pre.deploy_contract( + factory, + balance=balance if not new_address_pre_funded else 0, + ) + + new_contract_address = compute_create_address( + address=factory_address, + nonce=1, + initcode=initcode, + opcode=create_opcode, + ) + + tx_1 = Transaction( + gas_limit=generous_gas(fork), + to=factory_address, + sender=sender, + data=initcode, + ) + + # First check (checker, during initcode): code=b"" on the new contract, so + # violation whenever pre-funded balance dips below RESERVE_BALANCE. + expected_violation_in_initcode = ( + 1 + if (new_address_pre_funded and balance - value < Spec.RESERVE_BALANCE) + else 0 + ) + + # Second check (factory, after CREATE): code is now set or explicitly + # selfdestructed. In case code is set to empty, the reserve balance + # violation is still expected. + expected_violation_after_create = ( + 1 + if ( + deploy_code is not None + and len(deploy_code) == 0 + and new_address_pre_funded + and balance - value < Spec.RESERVE_BALANCE + ) + else 0 + ) + + new_balance = balance - value + Spec.RESERVE_BALANCE + + txs = [tx_1] + if new_address_pre_funded: + txs.insert( + 0, + Transaction( + to=new_contract_address, + value=balance, + sender=pre.fund_eoa(), + ), + ) + + blockchain_test( + pre=pre, + post={ + checker: Account( + storage={ + slot_violation_result: expected_violation_in_initcode, + } + ), + factory_address: Account( + storage={ + slot_code_worked: value_code_worked, + slot_violation_result: expected_violation_after_create, + } + ), + new_contract_address: Account( + balance=new_balance, code=deploy_code + ) + if not selfdestruct + else None, + target: Account(balance=value) if value != 0 else None, + # SELFDESTRUCT runs during initcode (before factory + # refill), so it sends balance - value only. + selfdestruct_target: Account(balance=balance - value) + if selfdestruct + else None, + }, + blocks=[Block(txs=txs)], + ) + + +@pytest.mark.parametrize( + ["value", "balance"], + [ + pytest.param(0, Spec.RESERVE_BALANCE, id="zero_value"), + pytest.param(1, Spec.RESERVE_BALANCE, id="non_zero_value"), + pytest.param(1, Spec.RESERVE_BALANCE + 1, id="non_zero_value_good"), + ], +) +@pytest.mark.parametrize("new_address_pre_funded", [True, False]) +@pytest.mark.parametrize( + "selfdestruct,deploy_code", + [(True, None), (False, Bytecode()), (False, Op.STOP)], +) +@pytest.mark.with_all_contract_creating_tx_types +def test_unrestricted_in_creation_tx_initcode( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + refill_factory: RefillFactory, + value: int, + balance: int, + new_address_pre_funded: bool, + selfdestruct: bool, + deploy_code: Bytecode | None, + tx_type: int, + fork: Fork, +) -> None: + """ + Test dippedIntoReserve() for contract spends done from initcode + context created via a creation tx with to: null. + + Checks dippedIntoReserve() once during initcode via checker. + No second check is possible (no code runs after initcode in + a creation tx context). + """ + assert selfdestruct == (deploy_code is None) + refill_call = refill_factory() + + sender = pre.fund_eoa(Spec.RESERVE_BALANCE + balance) + target = Address(0x1231) + selfdestruct_target = Address(0x5656) + + # Auxiliary contract: persists dippedIntoReserve() result + # during initcode. + checker = pre.deploy_contract( + Op.SSTORE(slot_violation_result, call_dipped_into_reserve()) + ) + + # code=b"" on the new contract, so violation whenever pre-funded balance + # dips below RESERVE_BALANCE. + expected_violation_in_initcode = ( + 1 + if (new_address_pre_funded and balance - value < Spec.RESERVE_BALANCE) + else 0 + ) + + common_initcode = ( + Op.CALL(value=value, address=target) + + Op.CALL(address=checker) + + refill_call(Op.ADDRESS) + ) + if selfdestruct: + initcode = common_initcode + Op.SELFDESTRUCT( + address=selfdestruct_target + ) + else: + initcode = Initcode( + initcode_prefix=common_initcode, + deploy_code=deploy_code, + ) + + tx_1 = Transaction( + gas_limit=generous_gas(fork), + to=None, + ty=tx_type, + value=balance if not new_address_pre_funded else 0, + sender=sender, + data=initcode, + ) + new_address = tx_1.created_contract + + txs = [tx_1] + if new_address_pre_funded: + txs.insert( + 0, + Transaction( + to=new_address, + value=balance, + sender=pre.fund_eoa(), + ), + ) + + new_balance = balance - value + Spec.RESERVE_BALANCE + + blockchain_test( + pre=pre, + post={ + checker: Account( + storage={ + slot_violation_result: expected_violation_in_initcode, + } + ), + new_address: Account(code=deploy_code, balance=new_balance) + if not selfdestruct + else None, + target: Account(balance=value) if value != 0 else None, + selfdestruct_target: Account(balance=new_balance) + if selfdestruct + else None, + }, + blocks=[Block(txs=txs)], + ) @pytest.mark.parametrize("stage1", Stage1Balance)