From 59b31ebe370a99dd02cef537aea20db0b6371e64 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:49:03 +0100 Subject: [PATCH 1/3] Add test_contract_unrestricted_with_selfdestruct --- .../reserve_balance/test_transfers.py | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/tests/monad_eight/reserve_balance/test_transfers.py b/tests/monad_eight/reserve_balance/test_transfers.py index 488cfa003f2..229b3bcaa1c 100644 --- a/tests/monad_eight/reserve_balance/test_transfers.py +++ b/tests/monad_eight/reserve_balance/test_transfers.py @@ -22,6 +22,8 @@ from execution_testing.test_types.helpers import compute_create_address from execution_testing.tools.tools_code.generators import Initcode +from tests.prague.eip7702_set_code_tx.spec import Spec as Spec7702 + from .helpers import ( Stage1Balance, StageBalance, @@ -1367,6 +1369,183 @@ def test_contract_unrestricted_with_create( ) +@pytest.mark.parametrize("prefund_balance", [0, Spec.RESERVE_BALANCE // 2]) +@pytest.mark.parametrize("create_balance", [0, Spec.RESERVE_BALANCE // 2]) +@pytest.mark.parametrize("call_balance", [0, Spec.RESERVE_BALANCE // 2]) +@pytest.mark.parametrize("pull_balance", [0, Spec.RESERVE_BALANCE // 2]) +@pytest.mark.parametrize("same_tx", [True, False]) +@pytest.mark.parametrize("through_delegation", [True, False]) +@pytest.mark.with_all_create_opcodes +def test_contract_unrestricted_with_selfdestruct( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + # Balance available to spender in previous transaction + prefund_balance: int, + # Balance available to spender in create + create_balance: int, + # Balance available to spender in selfdestruct call + call_balance: int, + # Balance the spender pulls in the selfdestruct call + pull_balance: int, + # Whether the selfdestructing call happens in the same tx + # as the creation (c.f. EIP-6780) + same_tx: bool, + # Whether the SELFDESTRUCT should be called on behalf of + # a delegating account + through_delegation: bool, + create_opcode: Op, + fork: Fork, +) -> None: + """ + Test reserve balance never affects contract spends done with a selfdestruct + opcode, unless selfdestruct is called on behalf of a delegating EOA. + + We allow the selfdestructing contract to be funded in various stages of the + execution. + """ + value = prefund_balance + call_balance + pull_balance + delegated_address = pre.fund_eoa(amount=0) + + if through_delegation: + # If we're delegating to the selfdestructing account, + # the endowment given at creation will not be included + # in the SELFDESTRUCT transfer. + pass + else: + value += create_balance + + selfdestruct_target = Address(0x5656) + pull_funder_address = pre.deploy_contract( + Op.SELFDESTRUCT(address=Op.CALLER), balance=pull_balance + ) + deploy_code = Op.CALL(address=pull_funder_address) + Op.SELFDESTRUCT( + address=selfdestruct_target + ) + + initcode = Initcode(deploy_code=deploy_code) + new_address_offset = 0 + initcode_offset = 32 + + factory = ( + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.CALLDATACOPY(initcode_offset, 0, len(initcode)) + # create new contract and store its address to later call it + + Op.MSTORE( + new_address_offset, + create_opcode( + value=create_balance, + offset=initcode_offset, + size=len(initcode), + ), + ) + + ( + Op.CALL( + address=delegated_address + if through_delegation + else Op.MLOAD(new_address_offset), + value=call_balance, + ) + if same_tx + else Op.STOP + ) + ) + factory_address = pre.deploy_contract( + factory, balance=create_balance + (call_balance if same_tx else 0) + ) + + new_contract_address = compute_create_address( + address=factory_address, + nonce=1, + initcode=initcode, + opcode=create_opcode, + ) + + txs = [] + if prefund_balance > 0: + txs.append( + Transaction( + to=delegated_address + if through_delegation + else new_contract_address, + value=prefund_balance, + sender=pre.fund_eoa(), + ), + ) + + # The creating transaction. If same_tx is also the test tx. + txs.append( + Transaction( + gas_limit=generous_gas(fork), + to=factory_address, + sender=pre.fund_eoa(), + data=initcode, + authorization_list=[ + AuthorizationTuple( + address=new_contract_address, + nonce=0, + signer=delegated_address, + ) + ] + if through_delegation + else None, + ) + ) + + if not same_tx: + # A separate test tx follows the creating tx. + txs.append( + Transaction( + gas_limit=generous_gas(fork), + to=delegated_address + if through_delegation + else new_contract_address, + value=call_balance, + sender=pre.fund_eoa(), + ) + ) + + storage = {slot_code_worked: value_code_worked} + reverted = through_delegation and value > 0 and prefund_balance > 0 + + blockchain_test( + pre=pre, + post={ + # On no revert factory is always left with no balance. + factory_address: Account(storage=storage, balance=0), + # Deployed contract will remain if + # - destructs not in same tx (EIP-6780) + # - it destructs the delegating account + new_contract_address: Account( + balance=create_balance if through_delegation else 0, + code=deploy_code, + ) + if not same_tx or through_delegation + else None, + # Delegated account is deleted if there is no delegation + delegated_address: Account( + balance=0, + code=Spec7702.delegation_designation(new_contract_address), + ) + if through_delegation + else None, + # SELFDESTRUCT target is deleted if source was empty + selfdestruct_target: Account(balance=value) + if value != 0 + else None, + } + if not reverted + else { + # On revert factory is left with pre state if the reverting + # transaction is the one which called it (same_tx) + factory_address: Account( + storage={} if same_tx else storage, + balance=create_balance + call_balance if same_tx else 0, + ) + }, + blocks=[Block(txs=txs)], + ) + + @pytest.mark.parametrize( ["value", "balance"], [ From 77e03fbffb0003ecdce0786e5cdcd360683a08bd Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:41:49 +0000 Subject: [PATCH 2/3] Add test_delegated_eoa_auth_list Co-Authored-By: Claude claude-opus-4-6 --- .../reserve_balance/test_transfers.py | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/tests/monad_eight/reserve_balance/test_transfers.py b/tests/monad_eight/reserve_balance/test_transfers.py index 229b3bcaa1c..005300111db 100644 --- a/tests/monad_eight/reserve_balance/test_transfers.py +++ b/tests/monad_eight/reserve_balance/test_transfers.py @@ -323,6 +323,105 @@ def test_delegated_eoa_send_value( ) +@pytest.mark.parametrize( + ["value", "balance", "violation"], + [ + pytest.param(0, Spec.RESERVE_BALANCE, False, id="zero_value"), + pytest.param(1, Spec.RESERVE_BALANCE, True, id="non_zero_value"), + pytest.param( + 1, Spec.RESERVE_BALANCE + 1, False, id="non_zero_value_good" + ), + ], +) +@pytest.mark.parametrize("pre_delegated", [True, False]) +@pytest.mark.parametrize( + "delegation_targets", + [ + pytest.param([], id="no_auths"), + pytest.param( + [Address(0x1111), Address(0)], + id="delegate_undelegate", + ), + pytest.param( + [Address(0), Address(0x1111)], + id="undelegate_delegate", + ), + pytest.param( + [Address(0x1111), Address(0x1111)], + id="delegate_twice", + ), + pytest.param( + [Address(0), Address(0)], + id="undelegate_twice", + ), + pytest.param( + [Address(0x1111), Address(0), Address(0x1111)], + id="delegate_undelegate_delegate", + ), + pytest.param( + [Address(0), Address(0x1111), Address(0)], + id="undelegate_delegate_undelegate", + ), + pytest.param( + [ + Address(0x1111) if i % 2 == 0 else Address(0) + for i in range(1024) + ], + id="large", + ), + ], +) +def test_delegated_eoa_auth_list( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + value: int, + balance: int, + violation: bool, + pre_delegated: bool, + delegation_targets: list[Address], + fork: Fork, +) -> None: + """ + Test reserve balance violations for an EOA with various sequences + of delegation targets in the authorization list. + """ + target_address = Address(0x1111) + if pre_delegated: + sender = pre.fund_eoa(balance, delegation=target_address) + else: + sender = pre.fund_eoa(balance) + + authorization_list = [ + AuthorizationTuple( + address=target, + nonce=sender.nonce + 1 + i, + signer=sender, + ) + for i, target in enumerate(delegation_targets) + ] + + contract = Op.SSTORE(slot_code_worked, value_code_worked) + Op.STOP + contract_address = pre.deploy_contract(contract) + + auth_gas = len(delegation_targets) * Spec7702.PER_EMPTY_ACCOUNT_COST + tx_1 = Transaction( + gas_limit=generous_gas(fork) + auth_gas, + to=contract_address, + value=value, + sender=sender, + authorization_list=authorization_list or None, + ) + any_delegation = pre_delegated or len(delegation_targets) > 0 + reverted = violation and any_delegation + storage = {} if reverted else {slot_code_worked: value_code_worked} + + blockchain_test( + pre=pre, + post={contract_address: Account(storage=storage)}, + blocks=[Block(txs=[tx_1])], + ) + + @pytest.mark.parametrize( ["value", "balance", "violation"], value_balance_violation_param_list, From e8a02f75f426f386a29371fab04182c020295ae2 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:28:37 +0100 Subject: [PATCH 3/3] Tighter post state assertions in new test Co-Authored-By: greptile --- tests/monad_eight/reserve_balance/test_transfers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/monad_eight/reserve_balance/test_transfers.py b/tests/monad_eight/reserve_balance/test_transfers.py index 005300111db..397ac6b637a 100644 --- a/tests/monad_eight/reserve_balance/test_transfers.py +++ b/tests/monad_eight/reserve_balance/test_transfers.py @@ -1639,7 +1639,13 @@ def test_contract_unrestricted_with_selfdestruct( factory_address: Account( storage={} if same_tx else storage, balance=create_balance + call_balance if same_tx else 0, - ) + ), + # Delegated account retains its prefunded balance on revert + delegated_address: Account(balance=prefund_balance) + if through_delegation and prefund_balance > 0 + else None, + # SELFDESTRUCT target should not receive value on revert + selfdestruct_target: None, }, blocks=[Block(txs=txs)], )