Skip to content
Merged
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
1 change: 1 addition & 0 deletions tests/monad_eight/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""MONAD_EIGHT fork tests."""
64 changes: 61 additions & 3 deletions tests/monad_eight/reserve_balance/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand All @@ -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
98 changes: 97 additions & 1 deletion tests/monad_eight/reserve_balance/test_transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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])],
)