diff --git a/tests/monad_eight/__init__.py b/tests/monad_eight/__init__.py new file mode 100644 index 0000000000..fb106275ed --- /dev/null +++ b/tests/monad_eight/__init__.py @@ -0,0 +1 @@ +"""MONAD_EIGHT fork tests.""" diff --git a/tests/monad_eight/reserve_balance/helpers.py b/tests/monad_eight/reserve_balance/helpers.py index f298fe6fac..3621f8279a 100644 --- a/tests/monad_eight/reserve_balance/helpers.py +++ b/tests/monad_eight/reserve_balance/helpers.py @@ -2,11 +2,14 @@ Helper types, functions and classes for testing reserve balance. """ -from execution_testing import ( - Op, -) +from enum import Enum, auto, unique +from typing import List + +from execution_testing import Op from execution_testing.forks.helpers import Fork +from .spec import Spec + def generous_gas(fork: Fork) -> int: """ @@ -25,3 +28,58 @@ def generous_gas(fork: Fork) -> int: + 5 * access_cost + selfdestruct_cost ) + + +@unique +class Stage1Balance(Enum): + """Initial balance states for Stage 1.""" + + BELOW_RESERVE = auto() + AT_RESERVE = auto() + ABOVE_RESERVE = auto() + + def __str__(self) -> str: + """Return string representation.""" + return self.name.lower() + + def compute_balance(self) -> int: + """Compute the actual balance for this stage.""" + match self: + case Stage1Balance.BELOW_RESERVE: + return Spec.RESERVE_BALANCE // 2 + case Stage1Balance.AT_RESERVE: + return Spec.RESERVE_BALANCE + case Stage1Balance.ABOVE_RESERVE: + return 2 * Spec.RESERVE_BALANCE + + +@unique +class StageBalance(Enum): + """Balance states for Stage 2/3 relative to min/max of reserve, initial.""" + + BELOW_MIN = auto() + AT_MIN = auto() + BETWEEN = auto() + AT_MAX = auto() + ABOVE_MAX = auto() + + def __str__(self) -> str: + """Return string representation.""" + return self.name.lower() + + def compute_balance(self, previous_balances: List[int]) -> int: + """Compute the actual balance for this stage.""" + min_val = min(Spec.RESERVE_BALANCE, *previous_balances) + max_val = max(Spec.RESERVE_BALANCE, *previous_balances) + match self: + case StageBalance.BELOW_MIN: + assert min_val >= 1 + return min_val - 1 + case StageBalance.AT_MIN: + return min_val + case StageBalance.BETWEEN: + return (min_val + max_val) // 2 + case StageBalance.AT_MAX: + return max_val + case StageBalance.ABOVE_MAX: + return max_val + 1 diff --git a/tests/monad_eight/reserve_balance/test_transfers.py b/tests/monad_eight/reserve_balance/test_transfers.py index 8035652a08..bc09d41e42 100644 --- a/tests/monad_eight/reserve_balance/test_transfers.py +++ b/tests/monad_eight/reserve_balance/test_transfers.py @@ -21,7 +21,11 @@ from execution_testing.test_types.helpers import compute_create_address from execution_testing.tools.tools_code.generators import Initcode -from .helpers import generous_gas +from .helpers import ( + Stage1Balance, + StageBalance, + generous_gas, +) from .spec import Spec, ref_spec_7702 REFERENCE_SPEC_GIT_PATH = ref_spec_7702.git_path @@ -197,6 +201,18 @@ def target_address( False, id="well_above_reserve_maxed_balance", ), + pytest.param( + 0, + 2**256 - 1, + False, + id="zero_maxed_balance", + ), + pytest.param( + 1, + 2**256 - 1, + False, + id="one_maxed_balance", + ), pytest.param( 2**256 - 1 - TX_FEE, 2**256 - 1, @@ -1533,3 +1549,83 @@ def test_unrestricted_in_creation_tx_initcode( }, blocks=[Block(txs=txs)], ) + + +@pytest.mark.parametrize("stage1", Stage1Balance) +@pytest.mark.parametrize("stage2", StageBalance) +@pytest.mark.parametrize("stage3", StageBalance) +def test_two_step_balance_change( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + stage1: Stage1Balance, + stage2: StageBalance, + stage3: StageBalance, +) -> None: + """ + Test reserve balance rules when a delegated account's balance changes + in 2 steps. + + The test verifies that a transaction reverts if and only if: + A) The balance decreased from Stage 1 to Stage 3 (final < initial) + B) The balance at Stage 3 is below reserve balance + + Both conditions must be true for the transaction to revert. + """ + balance1 = stage1.compute_balance() + balance2 = stage2.compute_balance([balance1]) + balance3 = stage3.compute_balance([balance1, balance2]) + + delta1 = balance2 - balance1 + delta2 = balance3 - balance2 + + sink = Address(0x5111) + + wallet_code = Op.CALL(address=sink, value=Op.CALLDATALOAD(0)) + wallet_address = pre.deploy_contract(code=wallet_code) + + sender = pre.fund_eoa(balance1, delegation=wallet_address) + + contract_code = Op.SSTORE(slot_code_worked, value_code_worked) + + if delta1 <= 0: + contract_code += Op.MSTORE(0, -delta1) + contract_code += Op.CALL(address=sender, args_size=32) + elif delta1 > 0: + funder1 = pre.deploy_contract( + code=Op.SELFDESTRUCT(sender), + balance=delta1, + ) + contract_code += Op.CALL(address=funder1) + + if delta2 <= 0: + contract_code += Op.MSTORE(0, -delta2) + contract_code += Op.CALL(address=sender, args_size=32) + elif delta2 > 0: + funder2 = pre.deploy_contract( + code=Op.SELFDESTRUCT(sender), + balance=delta2, + ) + contract_code += Op.CALL(address=funder2) + + contract_address = pre.deploy_contract(contract_code) + + tx = Transaction( + gas_limit=generous_gas(fork), + to=contract_address, + sender=sender, + ) + + balance_decreased = balance3 < balance1 + final_below_reserve = balance3 < Spec.RESERVE_BALANCE + + if balance_decreased and final_below_reserve: + storage = {} + else: + storage = {slot_code_worked: value_code_worked} + + blockchain_test( + pre=pre, + post={contract_address: Account(storage=storage)}, + blocks=[Block(txs=[tx])], + )