diff --git a/safe_cli/operators/exceptions.py b/safe_cli/operators/exceptions.py index bd7a0a04..0d57e4f7 100644 --- a/safe_cli/operators/exceptions.py +++ b/safe_cli/operators/exceptions.py @@ -50,6 +50,10 @@ class SafeAlreadyUpdatedException(SafeOperatorException): pass +class SafeVersionNotSupportedException(SafeOperatorException): + pass + + class UpdateAddressesNotValid(SafeOperatorException): pass diff --git a/safe_cli/operators/safe_operator.py b/safe_cli/operators/safe_operator.py index 81d50d58..6503a706 100644 --- a/safe_cli/operators/safe_operator.py +++ b/safe_cli/operators/safe_operator.py @@ -50,6 +50,7 @@ NotEnoughEtherToSend, NotEnoughSignatures, SafeAlreadyUpdatedException, + SafeVersionNotSupportedException, SameFallbackHandlerException, SameGuardException, SameMasterCopyException, @@ -213,26 +214,30 @@ def safe_cli_info(self) -> SafeCliInfo: self._safe_cli_info = self.refresh_safe_cli_info() return self._safe_cli_info + def refresh_safe_cli_info(self) -> SafeCliInfo: + self._safe_cli_info = self.get_safe_cli_info() + return self._safe_cli_info + def is_version_updated(self) -> bool: """ :return: True if Safe Master Copy is updated, False otherwise """ - if self._safe_cli_info.master_copy == self.last_safe_contract_address: + last_safe_contract_address = self.last_safe_contract_address + if self.safe_cli_info.master_copy == last_safe_contract_address: return True else: # Check versions, maybe safe-cli addresses were not updated try: - safe_contract_version = self.safe.retrieve_version() + safe_contract_version = Safe( + last_safe_contract_address, self.ethereum_client + ).retrieve_version() except BadFunctionCallOutput: # Safe master copy is not deployed or errored, maybe custom network return True # We cannot say you are not updated ¯\_(ツ)_/¯ + return semantic_version.parse( self.safe_cli_info.version ) >= semantic_version.parse(safe_contract_version) - def refresh_safe_cli_info(self) -> SafeCliInfo: - self._safe_cli_info = self.get_safe_cli_info() - return self._safe_cli_info - def load_cli_owners_from_words(self, words: List[str]): if len(words) == 1: # Reading seed from Environment Variable words = os.environ.get(words[0], default="").strip().split(" ") @@ -531,6 +536,12 @@ def change_master_copy(self, new_master_copy: str) -> bool: if new_master_copy == self.safe_cli_info.master_copy: raise SameMasterCopyException(new_master_copy) else: + safe_version = self.safe.retrieve_version() + if semantic_version.parse(safe_version) >= semantic_version.parse("1.3.0"): + raise SafeVersionNotSupportedException( + f"{safe_version} cannot be updated (yet)" + ) + try: Safe(new_master_copy, self.ethereum_client).retrieve_version() except BadFunctionCallOutput: @@ -550,8 +561,15 @@ def update_version(self) -> Optional[bool]: :return: """ + + safe_version = self.safe.retrieve_version() + if semantic_version.parse(safe_version) >= semantic_version.parse("1.3.0"): + raise SafeVersionNotSupportedException( + f"{safe_version} cannot be updated (yet)" + ) + if self.is_version_updated(): - raise SafeAlreadyUpdatedException() + raise SafeAlreadyUpdatedException(f"{safe_version} already updated") addresses = ( self.last_safe_contract_address, diff --git a/safe_cli/prompt_parser.py b/safe_cli/prompt_parser.py index 202d150f..e6b7e668 100644 --- a/safe_cli/prompt_parser.py +++ b/safe_cli/prompt_parser.py @@ -25,6 +25,8 @@ NotEnoughSignatures, NotEnoughTokenToSend, SafeAlreadyUpdatedException, + SafeOperatorException, + SafeVersionNotSupportedException, SameFallbackHandlerException, SameMasterCopyException, SenderRequiredException, @@ -110,6 +112,8 @@ def wrapper(*args, **kwargs): print_formatted_text(HTML(f"{e.args[0]}")) except SafeAlreadyUpdatedException: print_formatted_text(HTML("Safe is already updated")) + except SafeVersionNotSupportedException as e: + print_formatted_text(HTML(f"{e.args[0]}")) except (NotEnoughEtherToSend, NotEnoughTokenToSend) as e: print_formatted_text( HTML( @@ -127,6 +131,8 @@ def wrapper(*args, **kwargs): print_formatted_text( HTML(f"HwDevice exception: {e.args[0]}") ) + except SafeOperatorException as e: + print_formatted_text(HTML(f"{e.args[0]}")) return wrapper diff --git a/tests/test_safe_operator.py b/tests/test_safe_operator.py index a6ef3791..2c42a50b 100644 --- a/tests/test_safe_operator.py +++ b/tests/test_safe_operator.py @@ -1,7 +1,7 @@ import unittest from functools import lru_cache from unittest import mock -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock from eth_account import Account from eth_typing import ChecksumAddress @@ -25,6 +25,7 @@ NonExistingOwnerException, NotEnoughEtherToSend, NotEnoughSignatures, + SafeVersionNotSupportedException, SameFallbackHandlerException, SameGuardException, SameMasterCopyException, @@ -38,6 +39,18 @@ class TestSafeOperator(SafeCliTestCaseMixin, unittest.TestCase): + @lru_cache(maxsize=None) + def _deploy_l2_migration_contract(self) -> ChecksumAddress: + # Deploy L2 migration contract + safe_to_l2_migration_contract = self.w3.eth.contract( + abi=safe_to_l2_migration["abi"], bytecode=safe_to_l2_migration["bytecode"] + ) + tx_hash = safe_to_l2_migration_contract.constructor().transact( + {"from": self.ethereum_test_account.address} + ) + tx_receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) + return tx_receipt["contractAddress"] + def test_setup_operator(self): for number_owners in range(1, 4): safe_operator = self.setup_operator(number_owners=number_owners) @@ -211,6 +224,10 @@ def test_change_guard(self): self.assertEqual(safe.retrieve_guard(), new_guard) def test_change_master_copy(self): + safe_operator = self.setup_operator(version="1.3.0") + with self.assertRaises(SafeVersionNotSupportedException): + safe_operator.change_master_copy(self.safe_contract_V1_4_1.address) + safe_operator = self.setup_operator(version="1.1.1") safe = Safe(safe_operator.address, self.ethereum_client) current_master_copy = safe.retrieve_master_copy_address() @@ -246,17 +263,42 @@ def test_send_ether(self): self.assertTrue(safe_operator.send_ether(random_address, value)) self.assertEqual(self.ethereum_client.get_balance(random_address), value) - @lru_cache(maxsize=None) - def _deploy_l2_migration_contract(self) -> ChecksumAddress: - # Deploy L2 migration contract - safe_to_l2_migration_contract = self.w3.eth.contract( - abi=safe_to_l2_migration["abi"], bytecode=safe_to_l2_migration["bytecode"] + @mock.patch.object( + SafeOperator, "last_default_fallback_handler_address", new_callable=PropertyMock + ) + @mock.patch.object( + SafeOperator, "last_safe_contract_address", new_callable=PropertyMock + ) + def test_update_version( + self, + last_safe_contract_address_mock: PropertyMock, + last_default_fallback_handler_address: PropertyMock, + ): + last_safe_contract_address_mock.return_value = self.safe_contract_V1_4_1.address + last_default_fallback_handler_address.return_value = ( + self.compatibility_fallback_handler.address ) - tx_hash = safe_to_l2_migration_contract.constructor().transact( - {"from": self.ethereum_test_account.address} + + safe_operator_v130 = self.setup_operator(version="1.3.0") + with self.assertRaises(SafeVersionNotSupportedException): + safe_operator_v130.update_version() + + safe_operator_v111 = self.setup_operator(version="1.1.1") + with mock.patch.object( + MultiSend, + "MULTISEND_CALL_ONLY_ADDRESSES", + [self.multi_send_contract.address], + ): + safe_operator_v111.update_version() + + self.assertEqual( + safe_operator_v111.safe.retrieve_master_copy_address(), + last_safe_contract_address_mock.return_value, + ) + self.assertEqual( + safe_operator_v111.safe.retrieve_fallback_handler(), + last_default_fallback_handler_address.return_value, ) - tx_receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) - return tx_receipt["contractAddress"] def test_update_to_l2_v111(self): migration_contract_address = self._deploy_l2_migration_contract()