def validate(self, data):
        super().validate(data)

        if not SafeContract.objects.filter(address=data['safe']).exists():
            raise ValidationError(f"Safe={data['safe']} does not exist or it's still not indexed")

        ethereum_client = EthereumClientProvider()
        safe = Safe(data['safe'], ethereum_client)

        # Check owners and pending owners
        try:
            safe_owners = safe.retrieve_owners(block_identifier='pending')
        except BadFunctionCallOutput:  # Error using pending block identifier
            safe_owners = safe.retrieve_owners(block_identifier='latest')

        signature = data['signature']
        delegate = data['delegate']  # Delegate address to be added

        # Tries to find a valid delegator using multiple strategies
        for operation_hash in (DelegateSignatureHelper.calculate_hash(delegate),
                               DelegateSignatureHelper.calculate_hash(delegate, eth_sign=True),
                               DelegateSignatureHelper.calculate_hash(delegate, previous_topt=True),
                               DelegateSignatureHelper.calculate_hash(delegate, eth_sign=True, previous_topt=True)):
            delegator = self.check_signature(signature, operation_hash, safe_owners)
            if delegator:
                break

        if not delegator:
            raise ValidationError('Signing owner is not an owner of the Safe')

        data['delegator'] = delegator
        return data
Ejemplo n.º 2
0
    def validate_signature(self, signature: bytes):
        safe_tx_hash = self.context['safe_tx_hash']
        try:
            multisig_transaction = MultisigTransaction.objects.select_related(
                'ethereum_tx'
            ).get(safe_tx_hash=safe_tx_hash)
        except MultisigTransaction.DoesNotExist:
            raise NotFound(f'Multisig transaction with safe-tx-hash={safe_tx_hash} was not found')

        safe_address = multisig_transaction.safe
        if multisig_transaction.executed:
            raise ValidationError(f'Transaction with safe-tx-hash={safe_tx_hash} was already executed')

        ethereum_client = EthereumClientProvider()
        safe = Safe(safe_address, ethereum_client)
        try:
            safe_owners = safe.retrieve_owners(block_identifier='pending')
        except BadFunctionCallOutput:  # Error using pending block identifier
            safe_owners = safe.retrieve_owners(block_identifier='latest')

        parsed_signatures = SafeSignature.parse_signature(signature, safe_tx_hash)
        signature_owners = []
        for safe_signature in parsed_signatures:
            owner = safe_signature.owner
            if owner not in safe_owners:
                raise ValidationError(f'Signer={owner} is not an owner. Current owners={safe_owners}')
            if not safe_signature.is_valid(ethereum_client, safe.address):
                raise ValidationError(f'Signature={safe_signature.signature.hex()} for owner={owner} is not valid')
            if owner in signature_owners:
                raise ValidationError(f'Signature for owner={owner} is duplicated')

            signature_owners.append(owner)
        return signature
Ejemplo n.º 3
0
 def get_safe_owners(
         self, ethereum_client: EthereumClient,
         safe_address: ChecksumAddress) -> List[ChecksumAddress]:
     safe = Safe(safe_address, ethereum_client)
     try:
         return safe.retrieve_owners(block_identifier="pending")
     except BadFunctionCallOutput:  # Error using pending block identifier
         return safe.retrieve_owners(block_identifier="latest")
Ejemplo n.º 4
0
    def test_safe_cli_happy_path(self):
        accounts = [self.ethereum_test_account, Account.create()]
        account_addresses = [account.address for account in accounts]
        safe_address = self.deploy_test_safe(
            owners=account_addresses,
            threshold=2,
            initial_funding_wei=self.w3.toWei(1, "ether"),
        ).safe_address
        safe = Safe(safe_address, self.ethereum_client)
        safe_operator = SafeOperator(safe_address, self.ethereum_node_url)
        prompt_parser = PromptParser(safe_operator)
        random_address = Account.create().address

        self.assertEqual(safe_operator.accounts, set())
        prompt_parser.process_command(
            f"load_cli_owners {self.ethereum_test_account.key.hex()}")
        self.assertEqual(safe_operator.default_sender,
                         self.ethereum_test_account)
        self.assertEqual(safe_operator.accounts, {self.ethereum_test_account})

        prompt_parser.process_command(
            f"send_ether {random_address} 1")  # No enough signatures
        self.assertEqual(self.ethereum_client.get_balance(random_address), 0)

        value = 123
        prompt_parser.process_command(
            f"load_cli_owners {accounts[1].key.hex()}")
        prompt_parser.process_command(f"send_ether {random_address} {value}")
        self.assertEqual(self.ethereum_client.get_balance(random_address),
                         value)

        # Change threshold
        self.assertEqual(safe_operator.safe_cli_info.threshold, 2)
        self.assertEqual(safe.retrieve_threshold(), 2)
        prompt_parser.process_command("change_threshold 1")
        self.assertEqual(safe_operator.safe_cli_info.threshold, 1)
        self.assertEqual(safe.retrieve_threshold(), 1)

        # Approve Hash
        safe_tx_hash = Web3.keccak(text="hola")
        self.assertFalse(
            safe_operator.safe.retrieve_is_hash_approved(
                accounts[0].address, safe_tx_hash))
        prompt_parser.process_command(
            f"approve_hash {safe_tx_hash.hex()} {accounts[0].address}")
        self.assertTrue(
            safe_operator.safe.retrieve_is_hash_approved(
                accounts[0].address, safe_tx_hash))

        # Remove owner
        self.assertEqual(len(safe_operator.safe_cli_info.owners), 2)
        self.assertEqual(len(safe.retrieve_owners()), 2)
        prompt_parser.process_command(f"remove_owner {accounts[1].address}")
        self.assertEqual(safe_operator.safe_cli_info.owners,
                         [self.ethereum_test_account.address])
        self.assertEqual(safe.retrieve_owners(),
                         [self.ethereum_test_account.address])
Ejemplo n.º 5
0
def get_safe_owners(safe_address: str) -> Sequence[str]:
    ethereum_client = EthereumClientProvider()
    safe = Safe(safe_address, ethereum_client)
    try:
        return safe.retrieve_owners(block_identifier="pending")
    except BadFunctionCallOutput:  # Error using pending block identifier
        try:
            return safe.retrieve_owners(block_identifier="latest")
        except BadFunctionCallOutput:
            return []
    def validate(self, data):
        super().validate(data)

        if not SafeContract.objects.filter(address=data['safe']).exists():
            raise ValidationError(
                f"Safe={data['safe']} does not exist or it's still not indexed"
            )

        ethereum_client = EthereumClientProvider()
        safe = Safe(data['safe'], ethereum_client)

        # Check owners and pending owners
        try:
            safe_owners = safe.retrieve_owners(block_identifier='pending')
        except BadFunctionCallOutput:  # Error using pending block identifier
            safe_owners = safe.retrieve_owners(block_identifier='latest')

        signature = data['signature']
        delegate = data['delegate']
        operation_hash = DelegateSignatureHelper.calculate_hash(delegate)
        safe_signatures = SafeSignature.parse_signature(
            signature, operation_hash)
        if not safe_signatures:
            raise ValidationError('Cannot a valid signature')
        elif len(safe_signatures) > 1:
            raise ValidationError(
                'More than one signatures detected, just one is expected')

        safe_signature = safe_signatures[0]
        delegator = safe_signature.owner
        if delegator not in safe_owners:
            if safe_signature.signature_type == SafeSignatureType.EOA:
                # Maybe it's an `eth_sign` signature without Gnosis Safe `v + 4`, let's try
                safe_signatures = SafeSignature.parse_signature(
                    signature,
                    DelegateSignatureHelper.calculate_hash(delegate,
                                                           eth_sign=True))
                safe_signature = safe_signatures[0]
                delegator = safe_signature.owner
            if delegator not in safe_owners:
                raise ValidationError(
                    'Signing owner is not an owner of the Safe')

        if not safe_signature.is_valid():
            raise ValidationError(
                f'Signature of type={safe_signature.signature_type.name} for delegator={delegator} '
                f'is not valid')

        data['delegator'] = delegator
        return data
Ejemplo n.º 7
0
    def deploy_test_safe_v1_0_0(
        self,
        number_owners: int = 3,
        threshold: Optional[int] = None,
        owners: Optional[List[ChecksumAddress]] = None,
        initial_funding_wei: int = 0,
    ) -> Safe:
        owners = (owners if owners else
                  [Account.create().address for _ in range(number_owners)])
        if not threshold:
            threshold = len(owners) - 1 if len(owners) > 1 else 1
        empty_parameters = {"gas": 1, "gasPrice": 1}
        to = NULL_ADDRESS
        data = b""
        payment_token = NULL_ADDRESS
        payment = 0
        payment_receiver = NULL_ADDRESS
        initializer = HexBytes(
            self.safe_contract_V1_0_0.functions.setup(
                owners, threshold, to, data, payment_token, payment,
                payment_receiver).buildTransaction(empty_parameters)["data"])
        ethereum_tx_sent = self.proxy_factory.deploy_proxy_contract(
            self.ethereum_test_account,
            self.safe_contract_V1_0_0_address,
            initializer=initializer,
        )
        safe = Safe(ethereum_tx_sent.contract_address, self.ethereum_client)
        if initial_funding_wei:
            self.send_ether(safe.address, initial_funding_wei)

        self.assertEqual(safe.retrieve_version(), "1.0.0")
        self.assertEqual(safe.retrieve_threshold(), threshold)
        self.assertCountEqual(safe.retrieve_owners(), owners)

        return safe
Ejemplo n.º 8
0
    def test_deploy_proxy_contract_with_nonce(self):
        salt_nonce = generate_salt_nonce()
        owners = [Account.create().address for _ in range(2)]
        threshold = 2
        payment_token = None
        safe_create2_tx = Safe.build_safe_create2_tx(
            self.ethereum_client, self.safe_contract_address,
            self.proxy_factory_contract_address, salt_nonce, owners, threshold,
            self.gas_price, payment_token)
        # Send ether for safe deploying costs
        self.send_tx(
            {
                'to': safe_create2_tx.safe_address,
                'value': safe_create2_tx.payment
            }, self.ethereum_test_account)

        proxy_factory = ProxyFactory(self.proxy_factory_contract_address,
                                     self.ethereum_client)
        ethereum_tx_sent = proxy_factory.deploy_proxy_contract_with_nonce(
            self.ethereum_test_account,
            safe_create2_tx.master_copy_address,
            safe_create2_tx.safe_setup_data,
            salt_nonce,
            safe_create2_tx.gas,
            gas_price=self.gas_price)
        receipt = self.ethereum_client.get_transaction_receipt(
            ethereum_tx_sent.tx_hash, timeout=20)
        self.assertEqual(receipt.status, 1)
        safe = Safe(ethereum_tx_sent.contract_address, self.ethereum_client)
        self.assertEqual(ethereum_tx_sent.contract_address,
                         safe_create2_tx.safe_address)
        self.assertEqual(set(safe.retrieve_owners()), set(owners))
        self.assertEqual(safe.retrieve_master_copy_address(),
                         safe_create2_tx.master_copy_address)
Ejemplo n.º 9
0
    def test_remove_owner(self):
        safe_address = self.deploy_test_safe(
            owners=[self.ethereum_test_account.address]).safe_address
        safe_operator = SafeOperator(safe_address, self.ethereum_node_url)
        random_address = Account.create().address
        with self.assertRaises(NonExistingOwnerException):
            safe_operator.remove_owner(random_address)

        safe_operator.load_cli_owners([self.ethereum_test_account.key.hex()])
        new_owner = Account.create().address
        safe = Safe(safe_address, self.ethereum_client)
        self.assertTrue(safe_operator.add_owner(new_owner))
        self.assertIn(new_owner, safe.retrieve_owners())

        self.assertTrue(safe_operator.remove_owner(new_owner))
        self.assertNotIn(new_owner, safe_operator.accounts)
        self.assertNotIn(new_owner, safe.retrieve_owners())
 def retrieve_safe_info(self, address: str) -> SafeInfo:
     safe = Safe(address, self.ethereum_client)
     if not self.ethereum_client.is_contract(address):
         raise SafeNotDeployed('Safe with address=%s not deployed' %
                               address)
     nonce = safe.retrieve_nonce()
     threshold = safe.retrieve_threshold()
     owners = safe.retrieve_owners()
     master_copy = safe.retrieve_master_copy_address()
     version = safe.retrieve_version()
     return SafeInfo(address, nonce, threshold, owners, master_copy,
                     version)
Ejemplo n.º 11
0
    def test_safe_creation(self):
        salt_nonce = generate_salt_nonce()
        owners = [Account.create().address for _ in range(2)]
        data = {
            'saltNonce': salt_nonce,
            'owners': owners,
            'threshold': len(owners)
        }
        response = self.client.post(reverse('v3:safe-creation'), data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        response_json = response.json()
        safe_address = response_json['safe']
        self.assertTrue(check_checksum(safe_address))
        self.assertTrue(check_checksum(response_json['paymentReceiver']))
        self.assertEqual(response_json['paymentToken'], NULL_ADDRESS)
        self.assertEqual(int(response_json['payment']),
                         int(response_json['gasEstimated']) * int(response_json['gasPriceEstimated']))
        self.assertGreater(int(response_json['gasEstimated']), 0)
        self.assertGreater(int(response_json['gasPriceEstimated']), 0)
        self.assertGreater(len(response_json['setupData']), 2)
        self.assertEqual(response_json['masterCopy'], settings.SAFE_CONTRACT_ADDRESS)

        self.assertTrue(SafeContract.objects.filter(address=safe_address))
        self.assertTrue(SafeCreation2.objects.filter(owners__contains=[owners[0]]))
        safe_creation = SafeCreation2.objects.get(safe=safe_address)
        self.assertEqual(safe_creation.payment_token, None)
        # Payment includes deployment gas + gas to send eth to the deployer
        self.assertEqual(safe_creation.payment, safe_creation.wei_estimated_deploy_cost())

        # Deploy the Safe to check it
        self.send_ether(safe_address, int(response_json['payment']))
        safe_creation2 = SafeCreationServiceProvider().deploy_create2_safe_tx(safe_address)
        self.ethereum_client.get_transaction_receipt(safe_creation2.tx_hash, timeout=20)
        safe = Safe(safe_address, self.ethereum_client)
        self.assertEqual(safe.retrieve_master_copy_address(), response_json['masterCopy'])
        self.assertEqual(safe.retrieve_owners(), owners)

        # Test exception when same Safe is created
        response = self.client.post(reverse('v3:safe-creation'), data, format='json')
        self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
        self.assertIn('SafeAlreadyExistsException', response.json()['exception'])

        data = {
            'salt_nonce': -1,
            'owners': owners,
            'threshold': 2
        }
        response = self.client.post(reverse('v3:safe-creation'), data, format='json')
        self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
Ejemplo n.º 12
0
    def test_safe_cli_happy_path(self):
        accounts = [self.ethereum_test_account, Account.create()]
        account_addresses = [account.address for account in accounts]
        safe_address = self.deploy_test_safe(owners=account_addresses, threshold=2,
                                             initial_funding_wei=self.w3.toWei(1, 'ether')).safe_address
        safe = Safe(safe_address, self.ethereum_client)
        safe_operator = SafeOperator(safe_address, self.ethereum_node_url)
        prompt_parser = PromptParser(safe_operator)
        random_address = Account.create().address

        self.assertEqual(safe_operator.accounts, set())
        prompt_parser.process_command(f'load_cli_owners {self.ethereum_test_account.key.hex()}')
        self.assertEqual(safe_operator.default_sender, self.ethereum_test_account)
        self.assertEqual(safe_operator.accounts, {self.ethereum_test_account})

        prompt_parser.process_command(f'send_ether {random_address} 1')  # No enough signatures
        self.assertEqual(self.ethereum_client.get_balance(random_address), 0)

        value = 123
        prompt_parser.process_command(f'load_cli_owners {accounts[1].key.hex()}')
        prompt_parser.process_command(f'send_ether {random_address} {value}')
        self.assertEqual(self.ethereum_client.get_balance(random_address), value)

        # Change threshold
        self.assertEqual(safe_operator.safe_cli_info.threshold, 2)
        self.assertEqual(safe.retrieve_threshold(), 2)
        prompt_parser.process_command('change_threshold 1')
        self.assertEqual(safe_operator.safe_cli_info.threshold, 1)
        self.assertEqual(safe.retrieve_threshold(), 1)

        # Remove owner
        self.assertEqual(len(safe_operator.safe_cli_info.owners), 2)
        self.assertEqual(len(safe.retrieve_owners()), 2)
        prompt_parser.process_command(f'remove_owner {accounts[1].address}')
        self.assertEqual(safe_operator.safe_cli_info.owners, [self.ethereum_test_account.address])
        self.assertEqual(safe.retrieve_owners(), [self.ethereum_test_account.address])
Ejemplo n.º 13
0
    def test_add_owner(self):
        safe_address = self.deploy_test_safe(
            owners=[self.ethereum_test_account.address]).safe_address
        safe_operator = SafeOperator(safe_address, self.ethereum_node_url)
        with self.assertRaises(ExistingOwnerException):
            safe_operator.add_owner(self.ethereum_test_account.address)

        new_owner = Account.create().address
        with self.assertRaises(SenderRequiredException):
            safe_operator.add_owner(new_owner)

        safe_operator.default_sender = self.ethereum_test_account
        with self.assertRaises(NotEnoughSignatures):
            safe_operator.add_owner(new_owner)

        safe_operator.accounts.add(self.ethereum_test_account)
        safe = Safe(safe_address, self.ethereum_client)
        self.assertTrue(safe_operator.add_owner(new_owner))
        self.assertIn(self.ethereum_test_account, safe_operator.accounts)
        self.assertIn(new_owner, safe.retrieve_owners())
Ejemplo n.º 14
0
    def test_deploy_proxy_contract(self):
        s = 15
        owners = [Account.create().address for _ in range(2)]
        threshold = 2
        payment_token = None
        safe_creation_tx = Safe.build_safe_creation_tx(
            self.ethereum_client,
            self.safe_contract_V0_0_1_address,
            s,
            owners,
            threshold,
            self.gas_price,
            payment_token,
            payment_receiver=self.ethereum_test_account.address,
        )
        # Send ether for safe deploying costs
        self.send_tx(
            {"to": safe_creation_tx.safe_address, "value": safe_creation_tx.payment},
            self.ethereum_test_account,
        )

        proxy_factory = ProxyFactory(
            self.proxy_factory_contract_address, self.ethereum_client
        )
        ethereum_tx_sent = proxy_factory.deploy_proxy_contract(
            self.ethereum_test_account,
            safe_creation_tx.master_copy,
            safe_creation_tx.safe_setup_data,
            safe_creation_tx.gas,
            gas_price=self.gas_price,
        )
        receipt = self.ethereum_client.get_transaction_receipt(
            ethereum_tx_sent.tx_hash, timeout=20
        )
        self.assertEqual(receipt.status, 1)
        safe = Safe(ethereum_tx_sent.contract_address, self.ethereum_client)
        self.assertEqual(
            safe.retrieve_master_copy_address(), safe_creation_tx.master_copy
        )
        self.assertEqual(set(safe.retrieve_owners()), set(owners))
Ejemplo n.º 15
0
 def test_setup_operator(self):
     for number_owners in range(1, 4):
         safe_operator = self.setup_operator(number_owners=number_owners)
         self.assertEqual(len(safe_operator.accounts), number_owners)
         safe = Safe(safe_operator.address, self.ethereum_client)
         self.assertEqual(len(safe.retrieve_owners()), number_owners)
Ejemplo n.º 16
0
class SafeOperator:
    def __init__(self, address: str, node_url: str):
        self.address = address
        self.node_url = node_url
        self.ethereum_client = EthereumClient(self.node_url)
        self.ens = ENS.fromWeb3(self.ethereum_client.w3)
        self.network: EthereumNetwork = self.ethereum_client.get_network()
        self.etherscan = Etherscan.from_network_number(self.network.value)
        self.safe_tx_service = TransactionService.from_network_number(
            self.network.value)
        self.safe_relay_service = RelayService.from_network_number(
            self.network.value)
        self.safe = Safe(address, self.ethereum_client)
        self.safe_contract = self.safe.get_contract()
        self.accounts: Set[LocalAccount] = set()
        self.default_sender: Optional[LocalAccount] = None
        self.executed_transactions: List[str] = []
        self._safe_cli_info: Optional[
            SafeCliInfo] = None  # Cache for SafeCliInfo

    @cached_property
    def ens_domain(self) -> Optional[str]:
        # FIXME After web3.py fixes the middleware copy
        if self.network == EthereumNetwork.MAINNET:
            return self.ens.name(self.address)

    @property
    def safe_cli_info(self) -> SafeCliInfo:
        if not self._safe_cli_info:
            self._safe_cli_info = self.refresh_safe_cli_info()
        return self._safe_cli_info

    def _require_default_sender(self) -> NoReturn:
        """
        Throws SenderRequiredException if not default sender configured
        """
        if not self.default_sender:
            raise SenderRequiredException()

    def is_version_updated(self) -> bool:
        """
        :return: True if Safe Master Copy is updated, False otherwise
        """

        if self._safe_cli_info.master_copy == LAST_SAFE_CONTRACT:
            return True
        else:  # Check versions, maybe safe-cli addresses were not updated
            safe_contract = get_safe_contract(self.ethereum_client.w3,
                                              LAST_SAFE_CONTRACT)
            try:
                safe_contract_version = safe_contract.functions.VERSION().call(
                )
            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 get_balances(self):
        if not self.safe_tx_service:  # TODO Maybe use Etherscan
            print_formatted_text(
                HTML(f'<ansired>No tx service available for '
                     f'network={self.network.name}</ansired>'))
        else:
            balances = self.safe_tx_service.get_balances(self.address)
            headers = ['name', 'balance', 'symbol', 'decimals', 'tokenAddress']
            rows = []
            for balance in balances:
                if balance['tokenAddress']:  # Token
                    row = [
                        balance['token']['name'],
                        f"{int(balance['balance']) / 10**int(balance['token']['decimals']):.5f}",
                        balance['token']['symbol'],
                        balance['token']['decimals'], balance['tokenAddress']
                    ]
                else:  # Ether
                    row = [
                        'ETHER', f"{int(balance['balance']) / 10 ** 18:.5f}",
                        'Ξ', 18, ''
                    ]
                rows.append(row)
            print(tabulate(rows, headers=headers))

    def get_transaction_history(self):
        if not self.safe_tx_service:
            print_formatted_text(
                HTML(f'<ansired>No tx service available for '
                     f'network={self.network.name}</ansired>'))
            if self.etherscan.base_url:
                url = f'{self.etherscan.base_url}/address/{self.address}'
                print_formatted_text(
                    HTML(f'<b>Try Etherscan instead</b> {url}'))
        else:
            transactions = self.safe_tx_service.get_transactions(self.address)
            headers = ['nonce', 'to', 'value', 'transactionHash', 'safeTxHash']
            rows = []
            last_executed_tx = False
            for transaction in transactions:
                row = [transaction[header] for header in headers]
                data_decoded: Dict[str, Any] = transaction.get('dataDecoded')
                if data_decoded:
                    row.append(
                        self.safe_tx_service.data_decoded_to_text(
                            data_decoded))
                if transaction['transactionHash'] and transaction[
                        'isSuccessful']:
                    row[0] = Fore.GREEN + str(
                        row[0])  # For executed transactions we use green
                    if not last_executed_tx:
                        row[0] = Style.BRIGHT + row[0]
                        last_executed_tx = True
                elif transaction['transactionHash']:
                    row[0] = Fore.RED + str(row[0])  # For transactions failed
                else:
                    row[0] = Fore.YELLOW + str(
                        row[0])  # For non executed transactions we use yellow

                row[0] = Style.RESET_ALL + row[0]  # Reset all just in case
                rows.append(row)

            headers.append('dataDecoded')
            headers[0] = Style.BRIGHT + headers[0]
            print(tabulate(rows, headers=headers))

    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(" ")
        parsed_words = ' '.join(words)
        try:
            for index in range(100):  # Try first accounts of seed phrase
                account = get_account_from_words(parsed_words, index=index)
                if account.address in self.safe_cli_info.owners:
                    self.load_cli_owners([account.key.hex()])
            if not index:
                print_formatted_text(
                    HTML(
                        f'<ansired>Cannot generate any valid owner for this Safe</ansired>'
                    ))
        except ValidationError:
            print_formatted_text(
                HTML(f'<ansired>Cannot load owners from words</ansired>'))

    def load_cli_owners(self, keys: List[str]):
        for key in keys:
            try:
                account = Account.from_key(os.environ.get(
                    key, default=key))  # Try to get key from `environ`
                self.accounts.add(account)
                balance = self.ethereum_client.get_balance(account.address)
                print_formatted_text(
                    HTML(f'Loaded account <b>{account.address}</b> '
                         f'with balance={Web3.fromWei(balance, "ether")} ether'
                         ))
                if not self.default_sender and balance > 0:
                    print_formatted_text(
                        HTML(
                            f'Set account <b>{account.address}</b> as default sender of txs'
                        ))
                    self.default_sender = account
            except ValueError:
                print_formatted_text(
                    HTML(f'<ansired>Cannot load key=f{key}</ansired>'))

    def unload_cli_owners(self, owners: List[str]):
        accounts_to_remove: Set[Account] = set()
        for owner in owners:
            for account in self.accounts:
                if account.address == owner:
                    if self.default_sender and self.default_sender.address == owner:
                        self.default_sender = None
                    accounts_to_remove.add(account)
                    break
        self.accounts = self.accounts.difference(accounts_to_remove)
        if accounts_to_remove:
            print_formatted_text(
                HTML(f'<ansigreen>Accounts have been deleted</ansigreen>'))
        else:
            print_formatted_text(
                HTML(f'<ansired>No account was deleted</ansired>'))

    def show_cli_owners(self):
        if not self.accounts:
            print_formatted_text(
                HTML(f'<ansired>No accounts loaded</ansired>'))
        else:
            for account in self.accounts:
                print_formatted_text(
                    HTML(
                        f'<ansigreen><b>Account</b> {account.address} loaded</ansigreen>'
                    ))
            if self.default_sender:
                print_formatted_text(
                    HTML(
                        f'<ansigreen><b>Default sender:</b> {self.default_sender.address}'
                        f'</ansigreen>'))
            else:
                print_formatted_text(
                    HTML(f'<ansigreen>Not default sender set </ansigreen>'))

    def add_owner(self, new_owner: str) -> bool:
        if new_owner in self.safe_cli_info.owners:
            raise ExistingOwnerException(new_owner)
        else:
            # TODO Allow to set threshold
            threshold = self.safe_cli_info.threshold
            transaction = self.safe_contract.functions.addOwnerWithThreshold(
                new_owner, threshold).buildTransaction({
                    'from': self.address,
                    'gas': 0,
                    'gasPrice': 0
                })
            if self.execute_safe_internal_transaction(transaction['data']):
                self.safe_cli_info.owners = self.safe.retrieve_owners()
                self.safe_cli_info.threshold = threshold
                return True
            return False

    def remove_owner(self, owner_to_remove: str):
        if owner_to_remove not in self.safe_cli_info.owners:
            raise NonExistingOwnerException(owner_to_remove)
        elif len(self.safe_cli_info.owners) == self.safe_cli_info.threshold:
            raise ThresholdLimitException()
        else:
            index_owner = self.safe_cli_info.owners.index(owner_to_remove)
            prev_owner = self.safe_cli_info.owners[
                index_owner - 1] if index_owner else SENTINEL_ADDRESS
            threshold = self.safe_cli_info.threshold
            transaction = self.safe_contract.functions.removeOwner(
                prev_owner, owner_to_remove, threshold).buildTransaction({
                    'from':
                    self.address,
                    'gas':
                    0,
                    'gasPrice':
                    0
                })
            if self.execute_safe_internal_transaction(transaction['data']):
                self.safe_cli_info.owners = self.safe.retrieve_owners()
                self.safe_cli_info.threshold = threshold
                return True
            return False

    def send_custom(
        self,
        to: str,
        value: int,
        data: bytes,
        safe_nonce: Optional[int] = None,
        delegate_call: bool = False,
        destination: TransactionDestination = TransactionDestination.BLOCKCHAIN
    ) -> bool:
        if value > 0:
            safe_balance = self.ethereum_client.get_balance(self.address)
            if safe_balance < value:
                raise NotEnoughEtherToSend(safe_balance)
        operation = SafeOperation.DELEGATE_CALL if delegate_call else SafeOperation.CALL
        if destination == TransactionDestination.BLOCKCHAIN:
            return self.execute_safe_transaction(to,
                                                 value,
                                                 data,
                                                 operation,
                                                 safe_nonce=safe_nonce)
        elif destination == TransactionDestination.TRANSACTION_SERVICE:
            return self.post_transaction_to_tx_service(to,
                                                       value,
                                                       data,
                                                       operation,
                                                       safe_nonce=safe_nonce)
        elif destination == TransactionDestination.RELAY_SERVICE:
            return self.post_transaction_to_relay_service(
                to, value, data, operation)

    def send_ether(self, to: str, value: int, **kwargs) -> bool:
        return self.send_custom(to, value, b'', **kwargs)

    def send_erc20(self, to: str, token_address: str, amount: int,
                   **kwargs) -> bool:
        transaction = get_erc20_contract(self.ethereum_client.w3,
                                         token_address).functions.transfer(
                                             to, amount).buildTransaction({
                                                 'from':
                                                 self.address,
                                                 'gas':
                                                 0,
                                                 'gasPrice':
                                                 0
                                             })
        return self.send_custom(token_address, 0,
                                HexBytes(transaction['data']), **kwargs)

    def send_erc721(self, to: str, token_address: str, token_id: int,
                    **kwargs) -> bool:
        transaction = get_erc721_contract(
            self.ethereum_client.w3,
            token_address).functions.transferFrom(self.address, to,
                                                  token_id).buildTransaction({
                                                      'from':
                                                      self.address,
                                                      'gas':
                                                      0,
                                                      'gasPrice':
                                                      0
                                                  })
        return self.send_custom(token_address, 0, transaction['data'],
                                **kwargs)

    def change_fallback_handler(self, new_fallback_handler: str) -> bool:
        if new_fallback_handler == self.safe_cli_info.fallback_handler:
            raise SameFallbackHandlerException(new_fallback_handler)
        elif semantic_version.parse(
                self.safe_cli_info.version) < semantic_version.parse('1.1.0'):
            raise FallbackHandlerNotSupportedException()
        else:
            # TODO Check that fallback handler is valid
            transaction = self.safe_contract.functions.setFallbackHandler(
                new_fallback_handler).buildTransaction({
                    'from': self.address,
                    'gas': 0,
                    'gasPrice': 0
                })
            if self.execute_safe_internal_transaction(transaction['data']):
                self.safe_cli_info.fallback_handler = new_fallback_handler
                self.safe_cli_info.version = self.safe.retrieve_version()
                return True

    def change_master_copy(self, new_master_copy: str) -> bool:
        # TODO Check that master copy is valid
        if new_master_copy == self.safe_cli_info.master_copy:
            raise SameMasterCopyException(new_master_copy)
        else:
            try:
                Safe(new_master_copy, self.ethereum_client).retrieve_version()
            except BadFunctionCallOutput:
                raise InvalidMasterCopyException(new_master_copy)

            transaction = self.safe_contract.functions.changeMasterCopy(
                new_master_copy).buildTransaction({
                    'from': self.address,
                    'gas': 0,
                    'gasPrice': 0
                })
            if self.execute_safe_internal_transaction(transaction['data']):
                self.safe_cli_info.master_copy = new_master_copy
                self.safe_cli_info.version = self.safe.retrieve_version()
                return True

    def update_version(self) -> Optional[bool]:
        """
        Update Safe Master Copy and Fallback handler to the last version
        :return:
        """
        if self.is_version_updated():
            raise SafeAlreadyUpdatedException()

        multisend = MultiSend(LAST_MULTISEND_CONTRACT, self.ethereum_client)
        tx_params = {'from': self.address, 'gas': 0, 'gasPrice': 0}
        multisend_txs = [
            MultiSendTx(MultiSendOperation.CALL, self.address, 0, data)
            for data in (self.safe_contract.functions.changeMasterCopy(
                LAST_SAFE_CONTRACT).buildTransaction(tx_params)['data'],
                         self.safe_contract.functions.setFallbackHandler(
                             LAST_DEFAULT_CALLBACK_HANDLER).buildTransaction(
                                 tx_params)['data'])
        ]

        multisend_data = multisend.build_tx_data(multisend_txs)

        if self.execute_safe_transaction(
                multisend.address,
                0,
                multisend_data,
                operation=SafeOperation.DELEGATE_CALL):
            self.safe_cli_info.master_copy = LAST_SAFE_CONTRACT
            self.safe_cli_info.fallback_handler = LAST_DEFAULT_CALLBACK_HANDLER
            self.safe_cli_info.version = self.safe.retrieve_version()

    def change_threshold(self, threshold: int):
        if threshold == self.safe_cli_info.threshold:
            print_formatted_text(
                HTML(f'<ansired>Threshold is already {threshold}</ansired>'))
        elif threshold > len(self.safe_cli_info.owners):
            print_formatted_text(
                HTML(f'<ansired>Threshold={threshold} bigger than number '
                     f'of owners={len(self.safe_cli_info.owners)}</ansired>'))
        else:
            transaction = self.safe_contract.functions.changeThreshold(
                threshold).buildTransaction({
                    'from': self.address,
                    'gas': 0,
                    'gasPrice': 0
                })

            if self.execute_safe_internal_transaction(transaction['data']):
                self.safe_cli_info.threshold = threshold

    def enable_module(self, module_address: str):
        if module_address in self.safe_cli_info.modules:
            print_formatted_text(
                HTML(
                    f'<ansired>Module {module_address} is already enabled</ansired>'
                ))
        else:
            transaction = self.safe_contract.functions.enableModule(
                module_address).buildTransaction({
                    'from': self.address,
                    'gas': 0,
                    'gasPrice': 0
                })
            if self.execute_safe_internal_transaction(transaction['data']):
                self.safe_cli_info.modules = self.safe.retrieve_modules()

    def disable_module(self, module_address: str):
        if module_address not in self.safe_cli_info.modules:
            print_formatted_text(
                HTML(
                    f'<ansired>Module {module_address} is not enabled</ansired>'
                ))
        else:
            pos = self.safe_cli_info.modules.index(module_address)
            if pos == 0:
                previous_address = SENTINEL_ADDRESS
            else:
                previous_address = self.safe_cli_info.modules[pos - 1]
            transaction = self.safe_contract.functions.disableModule(
                previous_address, module_address).buildTransaction({
                    'from':
                    self.address,
                    'gas':
                    0,
                    'gasPrice':
                    0
                })
            if self.execute_safe_internal_transaction(transaction['data']):
                self.safe_cli_info.modules = self.safe.retrieve_modules()

    def print_info(self):
        for key, value in dataclasses.asdict(self.safe_cli_info).items():
            print_formatted_text(
                HTML(f'<b><ansigreen>{key.capitalize()}</ansigreen></b>='
                     f'<ansiblue>{value}</ansiblue>'))
        if self.ens_domain:
            print_formatted_text(
                HTML(f'<b><ansigreen>Ens domain</ansigreen></b>='
                     f'<ansiblue>{self.ens_domain}</ansiblue>'))
        if self.safe_tx_service:
            url = f'{self.safe_tx_service.base_url}/api/v1/safes/{self.address}/transactions/'
            print_formatted_text(
                HTML(f'<b><ansigreen>Safe Tx Service</ansigreen></b>='
                     f'<ansiblue>{url}</ansiblue>'))

        if self.safe_relay_service:
            url = f'{self.safe_relay_service.base_url}/api/v1/safes/{self.address}/transactions/'
            print_formatted_text(
                HTML(f'<b><ansigreen>Safe Relay Service</ansigreen></b>='
                     f'<ansiblue>{url}</ansiblue>'))

        if self.etherscan.base_url:
            url = f'{self.etherscan.base_url}/address/{self.address}'
            print_formatted_text(
                HTML(f'<b><ansigreen>Etherscan</ansigreen></b>='
                     f'<ansiblue>{url}</ansiblue>'))

        if not self.is_version_updated():
            print_formatted_text(
                HTML(
                    f'<ansired>Safe is not updated! You can use <b>update</b> command to update '
                    f'the Safe to a newest version</ansired>'))

    def get_safe_cli_info(self) -> SafeCliInfo:
        safe = self.safe
        balance_ether = Web3.fromWei(
            self.ethereum_client.get_balance(self.address), 'ether')
        safe_info = safe.retrieve_all_info()
        return SafeCliInfo(self.address, safe_info.nonce, safe_info.threshold,
                           safe_info.owners, safe_info.master_copy,
                           safe_info.modules, safe_info.fallback_handler,
                           balance_ether, safe_info.version)

    def get_threshold(self):
        print_formatted_text(self.safe.retrieve_threshold())

    def get_nonce(self):
        print_formatted_text(self.safe.retrieve_nonce())

    def get_owners(self):
        print_formatted_text(self.safe.retrieve_owners())

    def execute_safe_internal_transaction(self, data: bytes) -> bool:
        return self.execute_safe_transaction(self.address, 0, data)

    def execute_safe_transaction(self,
                                 to: str,
                                 value: int,
                                 data: bytes,
                                 operation: SafeOperation = SafeOperation.CALL,
                                 safe_nonce: Optional[int] = None) -> bool:
        self._require_default_sender(
        )  # Throws Exception if default sender not found
        # TODO Test tx is successful
        safe_tx = self.safe.build_multisig_tx(to,
                                              value,
                                              data,
                                              operation=operation.value,
                                              safe_nonce=safe_nonce)
        self.sign_transaction(
            safe_tx)  # Raises exception if it cannot be signed

        try:
            call_result = safe_tx.call(self.default_sender.address)
            print_formatted_text(
                HTML(f'Result: <ansigreen>{call_result}</ansigreen>'))
            tx_hash, _ = safe_tx.execute(self.default_sender.key)
            self.executed_transactions.append(tx_hash.hex())
            print_formatted_text(
                HTML(
                    f'<ansigreen>Executed tx with tx-hash={tx_hash.hex()} '
                    f'and safe-nonce={safe_tx.safe_nonce}, waiting for receipt</ansigreen>'
                ))
            if self.ethereum_client.get_transaction_receipt(tx_hash,
                                                            timeout=120):
                self.safe_cli_info.nonce += 1
                return True
            else:
                print_formatted_text(
                    HTML(
                        f'<ansired>Tx with tx-hash={tx_hash.hex()} still not mined</ansired>'
                    ))
            return False
        except InvalidInternalTx as invalid_internal_tx:
            print_formatted_text(
                HTML(
                    f'Result: <ansired>InvalidTx - {invalid_internal_tx}</ansired>'
                ))
            return False

    def post_transaction_to_tx_service(
            self,
            to: str,
            value: int,
            data: bytes,
            operation: SafeOperation = SafeOperation.CALL,
            safe_nonce: Optional[int] = None):
        if not self.safe_tx_service:
            raise ServiceNotAvailable(self.network.name)

        safe_tx = self.safe.build_multisig_tx(to,
                                              value,
                                              data,
                                              operation=operation.value,
                                              safe_nonce=safe_nonce)
        for account in self.accounts:
            safe_tx.sign(
                account.key)  # Raises exception if it cannot be signed
        self.safe_tx_service.post_transaction(self.address, safe_tx)

    def post_transaction_to_relay_service(
            self,
            to: str,
            value: int,
            data: bytes,
            operation: SafeOperation = SafeOperation.CALL,
            gas_token: Optional[str] = None):
        if not self.safe_relay_service:
            raise ServiceNotAvailable(self.network.name)

        safe_tx = self.safe.build_multisig_tx(to,
                                              value,
                                              data,
                                              operation=operation.value,
                                              gas_token=gas_token)
        estimation = self.safe_relay_service.get_estimation(
            self.address, safe_tx)
        safe_tx.base_gas = estimation['baseGas']
        safe_tx.safe_tx_gas = estimation['safeTxGas']
        safe_tx.gas_price = estimation['gasPrice']
        safe_tx.safe_nonce = estimation['lastUsedNonce'] + 1
        safe_tx.refund_receiver = estimation['refundReceiver']
        self.sign_transaction(safe_tx)
        transaction_data = self.safe_relay_service.send_transaction(
            self.address, safe_tx)
        tx_hash = transaction_data['txHash']
        print_formatted_text(
            HTML(f'<ansigreen>Gnosis Safe Relay has queued transaction with '
                 f'transaction-hash <b>{tx_hash}</b></ansigreen>'))

    # TODO Set sender so we can save gas in that signature
    def sign_transaction(self, safe_tx: SafeTx) -> NoReturn:
        owners = self.safe_cli_info.owners
        threshold = self.safe_cli_info.threshold
        selected_accounts: List[Account] = [
        ]  # Some accounts that are not an owner can be loaded
        for account in self.accounts:
            if account.address in owners:
                selected_accounts.append(account)
                threshold -= 1
                if threshold == 0:
                    break

        if threshold > 0:
            raise NotEnoughSignatures(threshold)

        for selected_account in selected_accounts:
            safe_tx.sign(selected_account.key)
        """
        selected_accounts.sort(key=lambda a: a.address.lower())
        signatures: bytes = b''
        for selected_account in selected_accounts:
            signatures += selected_account.signHash(safe_tx_hash)
        return signatures
        """

    def process_command(self, first_command: str,
                        rest_command: List[str]) -> bool:
        if first_command == 'help':
            print_formatted_text('I still cannot help you')
        elif first_command == 'history':
            self.get_transaction_history()
        elif first_command == 'refresh':
            print_formatted_text('Reloading Safe information')
            self.refresh_safe_cli_info()

        return False
Ejemplo n.º 17
0
class SafeOperator:
    def __init__(self, address: str, node_url: str):
        self.address = address
        self.node_url = node_url
        self.ethereum_client = EthereumClient(self.node_url)
        self.ens = ENS.fromWeb3(self.ethereum_client.w3)
        self.network: EthereumNetwork = self.ethereum_client.get_network()
        self.etherscan = EtherscanApi.from_ethereum_client(
            self.ethereum_client)
        self.safe_relay_service = RelayServiceApi.from_ethereum_client(
            self.ethereum_client)
        self.safe_tx_service = TransactionServiceApi.from_ethereum_client(
            self.ethereum_client)
        self.safe = Safe(address, self.ethereum_client)
        self.safe_contract = self.safe.get_contract()
        self.safe_contract_1_1_0 = get_safe_V1_1_1_contract(
            self.ethereum_client.w3, address=self.address)
        self.accounts: Set[LocalAccount] = set()
        self.default_sender: Optional[LocalAccount] = None
        self.executed_transactions: List[str] = []
        self._safe_cli_info: Optional[
            SafeCliInfo] = None  # Cache for SafeCliInfo
        self.require_all_signatures = (
            True  # Require all signatures to be present to send a tx
        )

    @cached_property
    def ens_domain(self) -> Optional[str]:
        # FIXME After web3.py fixes the middleware copy
        if self.network == EthereumNetwork.MAINNET:
            return self.ens.name(self.address)

    @property
    def safe_cli_info(self) -> SafeCliInfo:
        if not self._safe_cli_info:
            self._safe_cli_info = self.refresh_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 == LAST_SAFE_CONTRACT:
            return True
        else:  # Check versions, maybe safe-cli addresses were not updated
            safe_contract = get_safe_contract(self.ethereum_client.w3,
                                              LAST_SAFE_CONTRACT)
            try:
                safe_contract_version = safe_contract.functions.VERSION().call(
                )
            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(" ")
        parsed_words = " ".join(words)
        try:
            for index in range(100):  # Try first accounts of seed phrase
                account = get_account_from_words(parsed_words, index=index)
                if account.address in self.safe_cli_info.owners:
                    self.load_cli_owners([account.key.hex()])
            if not index:
                print_formatted_text(
                    HTML(
                        "<ansired>Cannot generate any valid owner for this Safe</ansired>"
                    ))
        except ValidationError:
            print_formatted_text(
                HTML("<ansired>Cannot load owners from words</ansired>"))

    def load_cli_owners(self, keys: List[str]):
        for key in keys:
            try:
                account = Account.from_key(os.environ.get(
                    key, default=key))  # Try to get key from `environ`
                self.accounts.add(account)
                balance = self.ethereum_client.get_balance(account.address)
                print_formatted_text(
                    HTML(f"Loaded account <b>{account.address}</b> "
                         f'with balance={Web3.fromWei(balance, "ether")} ether'
                         ))
                if not self.default_sender and balance > 0:
                    print_formatted_text(
                        HTML(
                            f"Set account <b>{account.address}</b> as default sender of txs"
                        ))
                    self.default_sender = account
            except ValueError:
                print_formatted_text(
                    HTML(f"<ansired>Cannot load key={key}</ansired>"))

    def unload_cli_owners(self, owners: List[str]):
        accounts_to_remove: Set[Account] = set()
        for owner in owners:
            for account in self.accounts:
                if account.address == owner:
                    if self.default_sender and self.default_sender.address == owner:
                        self.default_sender = None
                    accounts_to_remove.add(account)
                    break
        self.accounts = self.accounts.difference(accounts_to_remove)
        if accounts_to_remove:
            print_formatted_text(
                HTML("<ansigreen>Accounts have been deleted</ansigreen>"))
        else:
            print_formatted_text(
                HTML("<ansired>No account was deleted</ansired>"))

    def show_cli_owners(self):
        if not self.accounts:
            print_formatted_text(HTML("<ansired>No accounts loaded</ansired>"))
        else:
            for account in self.accounts:
                print_formatted_text(
                    HTML(
                        f"<ansigreen><b>Account</b> {account.address} loaded</ansigreen>"
                    ))
            if self.default_sender:
                print_formatted_text(
                    HTML(
                        f"<ansigreen><b>Default sender:</b> {self.default_sender.address}"
                        f"</ansigreen>"))
            else:
                print_formatted_text(
                    HTML("<ansigreen>Not default sender set </ansigreen>"))

    def approve_hash(self, hash_to_approve: HexBytes, sender: str) -> bool:
        sender_account = [
            account for account in self.accounts if account.address == sender
        ]
        if not sender_account:
            raise AccountNotLoadedException(sender)
        elif sender not in self.safe_cli_info.owners:
            raise NonExistingOwnerException(sender)
        elif self.safe.retrieve_is_hash_approved(self.default_sender.address,
                                                 hash_to_approve):
            raise HashAlreadyApproved(hash_to_approve,
                                      self.default_sender.address)
        else:
            sender_account = sender_account[0]
            transaction_to_send = self.safe_contract.functions.approveHash(
                hash_to_approve).buildTransaction({
                    "from":
                    sender_account.address,
                    "nonce":
                    self.ethereum_client.get_nonce_for_account(
                        sender_account.address),
                })
            if self.ethereum_client.is_eip1559_supported():
                transaction_to_send = self.ethereum_client.set_eip1559_fees(
                    transaction_to_send)
            call_result = self.ethereum_client.w3.eth.call(transaction_to_send)
            if call_result:  # There's revert message
                return False
            else:
                signed_transaction = sender_account.sign_transaction(
                    transaction_to_send)
                tx_hash = self.ethereum_client.send_raw_transaction(
                    signed_transaction["rawTransaction"])
                print_formatted_text(
                    HTML(
                        f"<ansigreen>Sent tx with tx-hash {tx_hash.hex()} from owner "
                        f"{self.default_sender.address}, waiting for receipt</ansigreen>"
                    ))
                if self.ethereum_client.get_transaction_receipt(tx_hash,
                                                                timeout=120):
                    return True
                else:
                    print_formatted_text(
                        HTML(
                            f"<ansired>Tx with tx-hash {tx_hash.hex()} still not mined</ansired>"
                        ))
                    return False

    def add_owner(self,
                  new_owner: str,
                  threshold: Optional[int] = None) -> bool:
        threshold = threshold if threshold is not None else self.safe_cli_info.threshold
        if new_owner in self.safe_cli_info.owners:
            raise ExistingOwnerException(new_owner)
        else:
            # TODO Allow to set threshold
            transaction = self.safe_contract.functions.addOwnerWithThreshold(
                new_owner, threshold).buildTransaction({
                    "from": self.address,
                    "gas": 0,
                    "gasPrice": 0
                })
            if self.execute_safe_internal_transaction(transaction["data"]):
                self.safe_cli_info.owners = self.safe.retrieve_owners()
                self.safe_cli_info.threshold = threshold
                return True
            return False

    def remove_owner(self,
                     owner_to_remove: str,
                     threshold: Optional[int] = None):
        threshold = threshold if threshold is not None else self.safe_cli_info.threshold
        if owner_to_remove not in self.safe_cli_info.owners:
            raise NonExistingOwnerException(owner_to_remove)
        elif len(self.safe_cli_info.owners) == threshold:
            raise ThresholdLimitException()
        else:
            index_owner = self.safe_cli_info.owners.index(owner_to_remove)
            prev_owner = (self.safe_cli_info.owners[index_owner - 1]
                          if index_owner else SENTINEL_ADDRESS)
            transaction = self.safe_contract.functions.removeOwner(
                prev_owner, owner_to_remove, threshold).buildTransaction({
                    "from":
                    self.address,
                    "gas":
                    0,
                    "gasPrice":
                    0
                })
            if self.execute_safe_internal_transaction(transaction["data"]):
                self.safe_cli_info.owners = self.safe.retrieve_owners()
                self.safe_cli_info.threshold = threshold
                return True
            return False

    def send_custom(
        self,
        to: str,
        value: int,
        data: bytes,
        safe_nonce: Optional[int] = None,
        delegate_call: bool = False,
    ) -> bool:
        if value > 0:
            safe_balance = self.ethereum_client.get_balance(self.address)
            if safe_balance < value:
                raise NotEnoughEtherToSend(safe_balance)
        operation = SafeOperation.DELEGATE_CALL if delegate_call else SafeOperation.CALL
        return self.prepare_and_execute_safe_transaction(to,
                                                         value,
                                                         data,
                                                         operation,
                                                         safe_nonce=safe_nonce)

    def send_ether(self, to: str, value: int, **kwargs) -> bool:
        return self.send_custom(to, value, b"", **kwargs)

    def send_erc20(self, to: str, token_address: str, amount: int,
                   **kwargs) -> bool:
        transaction = (get_erc20_contract(self.ethereum_client.w3,
                                          token_address).functions.transfer(
                                              to, amount).buildTransaction({
                                                  "from":
                                                  self.address,
                                                  "gas":
                                                  0,
                                                  "gasPrice":
                                                  0
                                              }))
        return self.send_custom(token_address, 0,
                                HexBytes(transaction["data"]), **kwargs)

    def send_erc721(self, to: str, token_address: str, token_id: int,
                    **kwargs) -> bool:
        transaction = (get_erc721_contract(
            self.ethereum_client.w3,
            token_address).functions.transferFrom(self.address, to,
                                                  token_id).buildTransaction({
                                                      "from":
                                                      self.address,
                                                      "gas":
                                                      0,
                                                      "gasPrice":
                                                      0
                                                  }))
        return self.send_custom(token_address, 0, transaction["data"],
                                **kwargs)

    def change_fallback_handler(self, new_fallback_handler: str) -> bool:
        if new_fallback_handler == self.safe_cli_info.fallback_handler:
            raise SameFallbackHandlerException(new_fallback_handler)
        elif semantic_version.parse(
                self.safe_cli_info.version) < semantic_version.parse("1.1.0"):
            raise FallbackHandlerNotSupportedException()
        elif (new_fallback_handler != NULL_ADDRESS
              and not self.ethereum_client.is_contract(new_fallback_handler)):
            raise InvalidFallbackHandlerException(
                f"{new_fallback_handler} address is not a contract")
        else:
            transaction = self.safe_contract.functions.setFallbackHandler(
                new_fallback_handler).buildTransaction({
                    "from": self.address,
                    "gas": 0,
                    "gasPrice": 0
                })
            if self.execute_safe_internal_transaction(transaction["data"]):
                self.safe_cli_info.fallback_handler = new_fallback_handler
                self.safe_cli_info.version = self.safe.retrieve_version()
                return True

    def change_guard(self, guard: str) -> bool:
        if guard == self.safe_cli_info.guard:
            raise SameGuardException(guard)
        elif semantic_version.parse(
                self.safe_cli_info.version) < semantic_version.parse("1.3.0"):
            raise GuardNotSupportedException()
        elif guard != NULL_ADDRESS and not self.ethereum_client.is_contract(
                guard):
            raise InvalidGuardException(f"{guard} address is not a contract")
        else:
            transaction = self.safe_contract.functions.setGuard(
                guard).buildTransaction({
                    "from": self.address,
                    "gas": 0,
                    "gasPrice": 0
                })
            if self.execute_safe_internal_transaction(transaction["data"]):
                self.safe_cli_info.guard = guard
                self.safe_cli_info.version = self.safe.retrieve_version()
                return True

    def change_master_copy(self, new_master_copy: str) -> bool:
        # TODO Check that master copy is valid
        if new_master_copy == self.safe_cli_info.master_copy:
            raise SameMasterCopyException(new_master_copy)
        else:
            try:
                Safe(new_master_copy, self.ethereum_client).retrieve_version()
            except BadFunctionCallOutput:
                raise InvalidMasterCopyException(new_master_copy)

            transaction = self.safe_contract_1_1_0.functions.changeMasterCopy(
                new_master_copy).buildTransaction({
                    "from": self.address,
                    "gas": 0,
                    "gasPrice": 0
                })
            if self.execute_safe_internal_transaction(transaction["data"]):
                self.safe_cli_info.master_copy = new_master_copy
                self.safe_cli_info.version = self.safe.retrieve_version()
                return True

    def update_version(self) -> Optional[bool]:
        """
        Update Safe Master Copy and Fallback handler to the last version
        :return:
        """
        if self.is_version_updated():
            raise SafeAlreadyUpdatedException()

        addresses = (LAST_SAFE_CONTRACT, LAST_DEFAULT_CALLBACK_HANDLER)
        if not all(
                self.ethereum_client.is_contract(contract)
                for contract in addresses):
            raise UpdateAddressesNotValid("Not valid addresses to update Safe",
                                          *addresses)

        multisend = MultiSend(LAST_MULTISEND_CONTRACT, self.ethereum_client)
        tx_params = {"from": self.address, "gas": 0, "gasPrice": 0}
        multisend_txs = [
            MultiSendTx(MultiSendOperation.CALL, self.address, 0, data)
            for data in (
                self.safe_contract_1_1_0.functions.changeMasterCopy(
                    LAST_SAFE_CONTRACT).buildTransaction(tx_params)["data"],
                self.safe_contract_1_1_0.functions.setFallbackHandler(
                    LAST_DEFAULT_CALLBACK_HANDLER).buildTransaction(tx_params)
                ["data"],
            )
        ]

        multisend_data = multisend.build_tx_data(multisend_txs)

        if self.prepare_and_execute_safe_transaction(
                multisend.address,
                0,
                multisend_data,
                operation=SafeOperation.DELEGATE_CALL):
            self.safe_cli_info.master_copy = LAST_SAFE_CONTRACT
            self.safe_cli_info.fallback_handler = LAST_DEFAULT_CALLBACK_HANDLER
            self.safe_cli_info.version = self.safe.retrieve_version()

    def change_threshold(self, threshold: int):
        if threshold == self.safe_cli_info.threshold:
            print_formatted_text(
                HTML(f"<ansired>Threshold is already {threshold}</ansired>"))
        elif threshold > len(self.safe_cli_info.owners):
            print_formatted_text(
                HTML(f"<ansired>Threshold={threshold} bigger than number "
                     f"of owners={len(self.safe_cli_info.owners)}</ansired>"))
        else:
            transaction = self.safe_contract.functions.changeThreshold(
                threshold).buildTransaction({
                    "from": self.address,
                    "gas": 0,
                    "gasPrice": 0
                })

            if self.execute_safe_internal_transaction(transaction["data"]):
                self.safe_cli_info.threshold = threshold

    def enable_module(self, module_address: str):
        if module_address in self.safe_cli_info.modules:
            print_formatted_text(
                HTML(
                    f"<ansired>Module {module_address} is already enabled</ansired>"
                ))
        else:
            transaction = self.safe_contract.functions.enableModule(
                module_address).buildTransaction({
                    "from": self.address,
                    "gas": 0,
                    "gasPrice": 0
                })
            if self.execute_safe_internal_transaction(transaction["data"]):
                self.safe_cli_info.modules = self.safe.retrieve_modules()

    def disable_module(self, module_address: str):
        if module_address not in self.safe_cli_info.modules:
            print_formatted_text(
                HTML(
                    f"<ansired>Module {module_address} is not enabled</ansired>"
                ))
        else:
            pos = self.safe_cli_info.modules.index(module_address)
            if pos == 0:
                previous_address = SENTINEL_ADDRESS
            else:
                previous_address = self.safe_cli_info.modules[pos - 1]
            transaction = self.safe_contract.functions.disableModule(
                previous_address, module_address).buildTransaction({
                    "from":
                    self.address,
                    "gas":
                    0,
                    "gasPrice":
                    0
                })
            if self.execute_safe_internal_transaction(transaction["data"]):
                self.safe_cli_info.modules = self.safe.retrieve_modules()

    def print_info(self):
        for key, value in dataclasses.asdict(self.safe_cli_info).items():
            print_formatted_text(
                HTML(f"<b><ansigreen>{key.capitalize()}</ansigreen></b>="
                     f"<ansiblue>{value}</ansiblue>"))
        if self.ens_domain:
            print_formatted_text(
                HTML(f"<b><ansigreen>Ens domain</ansigreen></b>="
                     f"<ansiblue>{self.ens_domain}</ansiblue>"))
        if self.safe_tx_service:
            url = f"{self.safe_tx_service.base_url}/api/v1/safes/{self.address}/transactions/"
            print_formatted_text(
                HTML(f"<b><ansigreen>Safe Tx Service</ansigreen></b>="
                     f"<ansiblue>{url}</ansiblue>"))

        if self.safe_relay_service:
            url = f"{self.safe_relay_service.base_url}/api/v1/safes/{self.address}/transactions/"
            print_formatted_text(
                HTML(f"<b><ansigreen>Safe Relay Service</ansigreen></b>="
                     f"<ansiblue>{url}</ansiblue>"))

        if self.etherscan:
            url = f"{self.etherscan.base_url}/address/{self.address}"
            print_formatted_text(
                HTML(f"<b><ansigreen>Etherscan</ansigreen></b>="
                     f"<ansiblue>{url}</ansiblue>"))

        if not self.is_version_updated():
            print_formatted_text(
                HTML(
                    "<ansired>Safe is not updated! You can use <b>update</b> command to update "
                    "the Safe to a newest version</ansired>"))

    def get_safe_cli_info(self) -> SafeCliInfo:
        safe = self.safe
        balance_ether = Web3.fromWei(
            self.ethereum_client.get_balance(self.address), "ether")
        safe_info = safe.retrieve_all_info()
        return SafeCliInfo(
            self.address,
            safe_info.nonce,
            safe_info.threshold,
            safe_info.owners,
            safe_info.master_copy,
            safe_info.modules,
            safe_info.fallback_handler,
            safe_info.guard,
            balance_ether,
            safe_info.version,
        )

    def get_threshold(self):
        print_formatted_text(self.safe.retrieve_threshold())

    def get_nonce(self):
        print_formatted_text(self.safe.retrieve_nonce())

    def get_owners(self):
        print_formatted_text(self.safe.retrieve_owners())

    def execute_safe_internal_transaction(self, data: bytes) -> bool:
        return self.prepare_and_execute_safe_transaction(self.address, 0, data)

    def prepare_safe_transaction(
        self,
        to: str,
        value: int,
        data: bytes,
        operation: SafeOperation = SafeOperation.CALL,
        safe_nonce: Optional[int] = None,
    ) -> SafeTx:
        safe_tx = self.safe.build_multisig_tx(to,
                                              value,
                                              data,
                                              operation=operation.value,
                                              safe_nonce=safe_nonce)
        self.sign_transaction(
            safe_tx)  # Raises exception if it cannot be signed
        return safe_tx

    def prepare_and_execute_safe_transaction(
        self,
        to: str,
        value: int,
        data: bytes,
        operation: SafeOperation = SafeOperation.CALL,
        safe_nonce: Optional[int] = None,
    ) -> bool:
        safe_tx = self.prepare_safe_transaction(to,
                                                value,
                                                data,
                                                operation,
                                                safe_nonce=safe_nonce)
        return self.execute_safe_transaction(safe_tx)

    @require_default_sender  # Throws Exception if default sender not found
    def execute_safe_transaction(self, safe_tx: SafeTx):
        try:
            call_result = safe_tx.call(self.default_sender.address)
            print_formatted_text(
                HTML(f"Result: <ansigreen>{call_result}</ansigreen>"))
            if yes_or_no_question("Do you want to execute tx " + str(safe_tx)):
                tx_hash, tx = safe_tx.execute(self.default_sender.key,
                                              eip1559_speed=TxSpeed.NORMAL)
                self.executed_transactions.append(tx_hash.hex())
                print_formatted_text(
                    HTML(
                        f"<ansigreen>Sent tx with tx-hash {tx_hash.hex()} "
                        f"and safe-nonce {safe_tx.safe_nonce}, waiting for receipt</ansigreen>"
                    ))
                tx_receipt = self.ethereum_client.get_transaction_receipt(
                    tx_hash, timeout=120)
                if tx_receipt:
                    fees = self.ethereum_client.w3.fromWei(
                        tx_receipt["gasUsed"] * tx_receipt.get(
                            "effectiveGasPrice", tx.get("gasPrice", 0)),
                        "ether",
                    )
                    print_formatted_text(
                        HTML(
                            f"<ansigreen>Tx was executed on block-number={tx_receipt['blockNumber']}, fees "
                            f"deducted={fees}</ansigreen>"))
                    self.safe_cli_info.nonce += 1
                    return True
                else:
                    print_formatted_text(
                        HTML(
                            f"<ansired>Tx with tx-hash {tx_hash.hex()} still not mined</ansired>"
                        ))
        except InvalidInternalTx as invalid_internal_tx:
            print_formatted_text(
                HTML(
                    f"Result: <ansired>InvalidTx - {invalid_internal_tx}</ansired>"
                ))
        return False

    # TODO Set sender so we can save gas in that signature
    def sign_transaction(self, safe_tx: SafeTx) -> SafeTx:
        permitted_signers = self.get_permitted_signers()
        threshold = self.safe_cli_info.threshold
        selected_accounts: List[Account] = [
        ]  # Some accounts that are not an owner can be loaded
        for account in self.accounts:
            if account.address in permitted_signers:
                selected_accounts.append(account)
                threshold -= 1
                if threshold == 0:
                    break

        if self.require_all_signatures and threshold > 0:
            raise NotEnoughSignatures(threshold)

        for selected_account in selected_accounts:
            safe_tx.sign(selected_account.key)

        return safe_tx

    @require_tx_service
    def _require_tx_service_mode(self):
        print_formatted_text(
            HTML(
                "<ansired>First enter tx-service mode using <b>tx-service</b> command</ansired>"
            ))

    def get_delegates(self):
        return self._require_tx_service_mode()

    def add_delegate(self, delegate_address: str, label: str,
                     signer_address: str):
        return self._require_tx_service_mode()

    def remove_delegate(self, delegate_address: str, signer_address: str):
        return self._require_tx_service_mode()

    def submit_signatures(self, safe_tx_hash: bytes) -> bool:
        return self._require_tx_service_mode()

    def get_balances(self):
        return self._require_tx_service_mode()

    def get_transaction_history(self):
        return self._require_tx_service_mode()

    def batch_txs(self, safe_nonce: int,
                  safe_tx_hashes: Sequence[bytes]) -> bool:
        return self._require_tx_service_mode()

    def execute_tx(self, safe_tx_hash: Sequence[bytes]) -> bool:
        return self._require_tx_service_mode()

    def get_permitted_signers(self) -> Set[str]:
        return set(self.safe_cli_info.owners)

    def process_command(self, first_command: str,
                        rest_command: List[str]) -> bool:
        if first_command == "help":
            print_formatted_text("I still cannot help you")
        elif first_command == "refresh":
            print_formatted_text("Reloading Safe information")
            self.refresh_safe_cli_info()

        return False
    def validate(self, data):
        super().validate(data)

        ethereum_client = EthereumClientProvider()
        safe = Safe(data['safe'], ethereum_client)
        safe_tx = safe.build_multisig_tx(data['to'],
                                         data['value'],
                                         data['data'],
                                         data['operation'],
                                         data['safe_tx_gas'],
                                         data['base_gas'],
                                         data['gas_price'],
                                         data['gas_token'],
                                         data['refund_receiver'],
                                         safe_nonce=data['nonce'])
        contract_transaction_hash = safe_tx.safe_tx_hash

        # Check safe tx hash matches
        if contract_transaction_hash != data['contract_transaction_hash']:
            raise ValidationError(
                f'Contract-transaction-hash={contract_transaction_hash.hex()} '
                f'does not match provided contract-tx-hash={data["contract_transaction_hash"].hex()}'
            )

        # Check there's not duplicated tx with same `nonce` or same `safeTxHash` for the same Safe.
        # We allow duplicated if existing tx is not executed
        multisig_transactions = MultisigTransaction.objects.filter(
            safe=safe.address, nonce=data['nonce']).executed()
        if multisig_transactions:
            for multisig_transaction in multisig_transactions:
                if multisig_transaction.safe_tx_hash == contract_transaction_hash.hex(
                ):
                    raise ValidationError(
                        f'Tx with safe-tx-hash={contract_transaction_hash.hex()} '
                        f'for safe={safe.address} was already executed in '
                        f'tx-hash={multisig_transaction.ethereum_tx_id}')

            raise ValidationError(
                f'Tx with nonce={safe_tx.safe_nonce} for safe={safe.address} '
                f'already executed in tx-hash={multisig_transactions[0].ethereum_tx_id}'
            )

        # Check owners and pending owners
        try:
            safe_owners = safe.retrieve_owners(block_identifier='pending')
        except BadFunctionCallOutput:  # Error using pending block identifier
            safe_owners = safe.retrieve_owners(block_identifier='latest')

        data['safe_owners'] = safe_owners

        delegates = SafeContractDelegate.objects.get_delegates_for_safe(
            safe.address)
        allowed_senders = safe_owners + delegates
        if not data['sender'] in allowed_senders:
            raise ValidationError(
                f'Sender={data["sender"]} is not an owner or delegate. '
                f'Current owners={safe_owners}. Delegates={delegates}')

        signature_owners = []
        # TODO Make signature mandatory
        signature = data.get('signature', b'')
        parsed_signatures = SafeSignature.parse_signature(
            signature, contract_transaction_hash)
        data['parsed_signatures'] = parsed_signatures
        for safe_signature in parsed_signatures:
            owner = safe_signature.owner
            if not safe_signature.is_valid(ethereum_client, safe.address):
                raise ValidationError(
                    f'Signature={safe_signature.signature.hex()} for owner={owner} is not valid'
                )

            if owner in delegates and len(parsed_signatures) > 1:
                raise ValidationError(
                    f'Just one signature is expected if using delegates')
            if owner not in allowed_senders:
                raise ValidationError(
                    f'Signer={owner} is not an owner or delegate. '
                    f'Current owners={safe_owners}. Delegates={delegates}')
            if owner in signature_owners:
                raise ValidationError(
                    f'Signature for owner={owner} is duplicated')

            signature_owners.append(owner)

        # TODO Make signature mandatory. len(signature_owners) must be >= 1
        if signature_owners and data['sender'] not in signature_owners:
            raise ValidationError(
                f'Signature does not match sender={data["sender"]}. '
                f'Calculated owners={signature_owners}')

        return data
    def _send_multisig_tx(self,
                          safe_address: str,
                          to: str,
                          value: int,
                          data: bytes,
                          operation: int,
                          safe_tx_gas: int,
                          base_gas: int,
                          gas_price: int,
                          gas_token: str,
                          refund_receiver: str,
                          safe_nonce: int,
                          signatures: bytes,
                          block_identifier='latest') -> Tuple[bytes, bytes, Dict[str, Any]]:
        """
        This function calls the `send_multisig_tx` of the Safe, but has some limitations to prevent abusing
        the relay
        :return: Tuple(tx_hash, safe_tx_hash, tx)
        :raises: InvalidMultisigTx: If user tx cannot go through the Safe
        """

        safe = Safe(safe_address, self.ethereum_client)
        data = data or b''
        gas_token = gas_token or NULL_ADDRESS
        refund_receiver = refund_receiver or NULL_ADDRESS
        to = to or NULL_ADDRESS

        # Make sure refund receiver is set to 0x0 so that the contract refunds the gas costs to tx.origin
        if not self._check_refund_receiver(refund_receiver):
            raise InvalidRefundReceiver(refund_receiver)

        self._check_safe_gas_price(gas_token, gas_price)

        # Make sure proxy contract is ours
        if not self.proxy_factory.check_proxy_code(safe_address):
            raise InvalidProxyContract(safe_address)

        # Make sure master copy is valid
        safe_master_copy_address = safe.retrieve_master_copy_address()
        if safe_master_copy_address not in self.safe_valid_contract_addresses:
            raise InvalidMasterCopyAddress(safe_master_copy_address)

        # Check enough funds to pay for the gas
        if not safe.check_funds_for_tx_gas(safe_tx_gas, base_gas, gas_price, gas_token):
            raise NotEnoughFundsForMultisigTx

        threshold = safe.retrieve_threshold()
        number_signatures = len(signatures) // 65  # One signature = 65 bytes
        if number_signatures < threshold:
            raise SignaturesNotFound('Need at least %d signatures' % threshold)

        safe_tx_gas_estimation = safe.estimate_tx_gas(to, value, data, operation)
        safe_base_gas_estimation = safe.estimate_tx_base_gas(to, value, data, operation, gas_token,
                                                             safe_tx_gas_estimation)
        if safe_tx_gas < safe_tx_gas_estimation or base_gas < safe_base_gas_estimation:
            raise InvalidGasEstimation("Gas should be at least equal to safe-tx-gas=%d and base-gas=%d. Current is "
                                       "safe-tx-gas=%d and base-gas=%d" %
                                       (safe_tx_gas_estimation, safe_base_gas_estimation, safe_tx_gas, base_gas))

        # We use fast tx gas price, if not txs could be stuck
        tx_gas_price = self._get_configured_gas_price()
        tx_sender_private_key = self.tx_sender_account.key
        tx_sender_address = Account.from_key(tx_sender_private_key).address

        safe_tx = safe.build_multisig_tx(
            to,
            value,
            data,
            operation,
            safe_tx_gas,
            base_gas,
            gas_price,
            gas_token,
            refund_receiver,
            signatures,
            safe_nonce=safe_nonce,
            safe_version=safe.retrieve_version()
        )

        owners = safe.retrieve_owners()
        signers = safe_tx.signers
        if set(signers) - set(owners):  # All the signers must be owners
            raise InvalidOwners('Signers=%s are not valid owners of the safe. Owners=%s', safe_tx.signers, owners)

        if signers != safe_tx.sorted_signers:
            raise SignaturesNotSorted('Safe-tx-hash=%s - Signatures are not sorted by owner: %s' %
                                      (safe_tx.safe_tx_hash.hex(), safe_tx.signers))

        if banned_signers := BannedSigner.objects.filter(address__in=signers):
            raise SignerIsBanned(f'Signers {list(banned_signers)} are banned')
Ejemplo n.º 20
0
    def validate(self, data):
        super().validate(data)

        ethereum_client = EthereumClientProvider()
        safe = Safe(data["safe"], ethereum_client)
        try:
            safe_version = safe.retrieve_version()
        except BadFunctionCallOutput as e:
            raise ValidationError(
                f"Could not get Safe version from blockchain, check contract exists on network "
                f"{ethereum_client.get_network().name}") from e
        except IOError:
            raise ValidationError(
                "Problem connecting to the ethereum node, please try again later"
            )

        safe_tx = safe.build_multisig_tx(
            data["to"],
            data["value"],
            data["data"],
            data["operation"],
            data["safe_tx_gas"],
            data["base_gas"],
            data["gas_price"],
            data["gas_token"],
            data["refund_receiver"],
            safe_nonce=data["nonce"],
            safe_version=safe_version,
        )
        contract_transaction_hash = safe_tx.safe_tx_hash

        # Check safe tx hash matches
        if contract_transaction_hash != data["contract_transaction_hash"]:
            raise ValidationError(
                f"Contract-transaction-hash={contract_transaction_hash.hex()} "
                f'does not match provided contract-tx-hash={data["contract_transaction_hash"].hex()}'
            )

        # Check there's not duplicated tx with same `nonce` or same `safeTxHash` for the same Safe.
        # We allow duplicated if existing tx is not executed
        multisig_transactions = MultisigTransaction.objects.filter(
            safe=safe.address, nonce=data["nonce"]).executed()
        if multisig_transactions:
            for multisig_transaction in multisig_transactions:
                if multisig_transaction.safe_tx_hash == contract_transaction_hash.hex(
                ):
                    raise ValidationError(
                        f"Tx with safe-tx-hash={contract_transaction_hash.hex()} "
                        f"for safe={safe.address} was already executed in "
                        f"tx-hash={multisig_transaction.ethereum_tx_id}")

            raise ValidationError(
                f"Tx with nonce={safe_tx.safe_nonce} for safe={safe.address} "
                f"already executed in tx-hash={multisig_transactions[0].ethereum_tx_id}"
            )

        # Check owners and pending owners
        try:
            safe_owners = safe.retrieve_owners(block_identifier="pending")
        except BadFunctionCallOutput:  # Error using pending block identifier
            safe_owners = safe.retrieve_owners(block_identifier="latest")
        except IOError:
            raise ValidationError(
                "Problem connecting to the ethereum node, please try again later"
            )

        data["safe_owners"] = safe_owners

        delegates = SafeContractDelegate.objects.get_delegates_for_safe_and_owners(
            safe.address, safe_owners)
        allowed_senders = set(safe_owners) | delegates
        if not data["sender"] in allowed_senders:
            raise ValidationError(
                f'Sender={data["sender"]} is not an owner or delegate. '
                f"Current owners={safe_owners}. Delegates={delegates}")

        signature_owners = []
        # TODO Make signature mandatory
        signature = data.get("signature", b"")
        parsed_signatures = SafeSignature.parse_signature(
            signature, contract_transaction_hash)
        data["parsed_signatures"] = parsed_signatures
        # If there's at least one signature, transaction is trusted (until signatures are mandatory)
        data["trusted"] = bool(parsed_signatures)
        for safe_signature in parsed_signatures:
            owner = safe_signature.owner
            if not safe_signature.is_valid(ethereum_client, safe.address):
                raise ValidationError(
                    f"Signature={safe_signature.signature.hex()} for owner={owner} is not valid"
                )

            if owner in delegates and len(parsed_signatures) > 1:
                raise ValidationError(
                    "Just one signature is expected if using delegates")
            if owner not in allowed_senders:
                raise ValidationError(
                    f"Signer={owner} is not an owner or delegate. "
                    f"Current owners={safe_owners}. Delegates={delegates}")
            if owner in signature_owners:
                raise ValidationError(
                    f"Signature for owner={owner} is duplicated")

            signature_owners.append(owner)

        # TODO Make signature mandatory. len(signature_owners) must be >= 1
        if signature_owners and data["sender"] not in signature_owners:
            raise ValidationError(
                f'Signature does not match sender={data["sender"]}. '
                f"Calculated owners={signature_owners}")

        return data