Esempio n. 1
0
    def test_post_multisig_transactions_with_origin(self):
        safe_owner_1 = Account.create()
        safe_create2_tx = self.deploy_test_safe(owners=[safe_owner_1.address])
        safe_address = safe_create2_tx.safe_address
        safe = Safe(safe_address, self.ethereum_client)

        response = self.client.get(reverse('v1:multisig-transactions', args=(safe_address,)), format='json')
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

        to = Account.create().address
        data = {"to": to,
                "value": 100000000000000000,
                "data": None,
                "operation": 0,
                "nonce": 0,
                "safeTxGas": 0,
                "baseGas": 0,
                "gasPrice": 0,
                "gasToken": "0x0000000000000000000000000000000000000000",
                "refundReceiver": "0x0000000000000000000000000000000000000000",
                # "contractTransactionHash": "0x1c2c77b29086701ccdda7836c399112a9b715c6a153f6c8f75c84da4297f60d3",
                "sender": safe_owner_1.address,
                "origin": 'Testing origin field',
                }

        safe_tx = safe.build_multisig_tx(data['to'], data['value'], data['data'], data['operation'],
                                         data['safeTxGas'], data['baseGas'], data['gasPrice'],
                                         data['gasToken'],
                                         data['refundReceiver'], safe_nonce=data['nonce'])
        data['contractTransactionHash'] = safe_tx.safe_tx_hash.hex()
        response = self.client.post(reverse('v1:multisig-transactions', args=(safe_address,)), format='json', data=data)
        self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
        multisig_tx_db = MultisigTransaction.objects.get(safe_tx_hash=safe_tx.safe_tx_hash)
        self.assertEqual(multisig_tx_db.origin, data['origin'])
    def test_post_multisig_transactions(self):
        safe_owner_1 = Account.create()
        safe_create2_tx = self.deploy_test_safe(owners=[safe_owner_1.address])
        safe_address = safe_create2_tx.safe_address
        safe = Safe(safe_address, self.ethereum_client)

        response = self.client.get(reverse('v1:multisig-transactions', args=(safe_address,)), format='json')
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

        to = Account.create().address
        data = {"to": to,
                "value": 100000000000000000,
                "data": None,
                "operation": 0,
                "nonce": 0,
                "safeTxGas": 0,
                "baseGas": 0,
                "gasPrice": 0,
                "gasToken": "0x0000000000000000000000000000000000000000",
                "refundReceiver": "0x0000000000000000000000000000000000000000",
                # "contractTransactionHash": "0x1c2c77b29086701ccdda7836c399112a9b715c6a153f6c8f75c84da4297f60d3",
                "sender": safe_owner_1.address,
                }
        safe_tx = safe.build_multisig_tx(data['to'], data['value'], data['data'], data['operation'],
                                         data['safeTxGas'], data['baseGas'], data['gasPrice'],
                                         data['gasToken'],
                                         data['refundReceiver'], safe_nonce=data['nonce'])
        data['contractTransactionHash'] = safe_tx.safe_tx_hash.hex()
        response = self.client.post(reverse('v1:multisig-transactions', args=(safe_address,)), format='json', data=data)
        self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)

        response = self.client.get(reverse('v1:multisig-transactions', args=(safe_address,)), format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data['results']), 1)
        self.assertIsNone(response.data['results'][0]['executor'])
        self.assertEqual(len(response.data['results'][0]['confirmations']), 0)

        # Test confirmation with signature
        data['signature'] = safe_owner_1.signHash(safe_tx.safe_tx_hash)['signature'].hex()
        response = self.client.post(reverse('v1:multisig-transactions', args=(safe_address,)), format='json', data=data)
        self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)

        response = self.client.get(reverse('v1:multisig-transactions', args=(safe_address,)), format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data['results']), 1)
        self.assertEqual(len(response.data['results'][0]['confirmations']), 1)
        self.assertEqual(response.data['results'][0]['confirmations'][0]['signature'], data['signature'])

        # Sign with a different user that sender
        random_user_account = Account.create()
        data['signature'] = random_user_account.signHash(safe_tx.safe_tx_hash)['signature'].hex()
        response = self.client.post(reverse('v1:multisig-transactions', args=(safe_address,)), format='json', data=data)
        self.assertIn('Signature does not match sender', response.data['non_field_errors'][0])
        self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)

        # Sign with a random user (not owner)
        data['sender'] = random_user_account.address
        response = self.client.post(reverse('v1:multisig-transactions', args=(safe_address,)), format='json', data=data)
        self.assertIn('User is not an owner', response.data['non_field_errors'][0])
        self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
Esempio n. 3
0
    def test_create_multisig_tx(self):
        w3 = self.w3

        # The balance we will send to the safe
        safe_balance = w3.toWei(0.02, 'ether')

        # Create Safe
        funder_account = self.ethereum_test_account
        funder = funder_account.address
        accounts = [self.create_account(), self.create_account()]
        # Signatures must be sorted!
        accounts.sort(key=lambda account: account.address.lower())
        owners = [x.address for x in accounts]
        threshold = len(accounts)

        safe_creation = self.deploy_test_safe(owners=owners,
                                              threshold=threshold)
        my_safe_address = safe_creation.safe_address
        my_safe_contract = get_safe_contract(w3, my_safe_address)
        SafeContractFactory(address=my_safe_address)

        to = funder
        value = safe_balance // 4
        data = HexBytes('')
        operation = 0
        safe_tx_gas = 100000
        data_gas = 300000
        gas_price = self.transaction_service._get_minimum_gas_price()
        gas_token = NULL_ADDRESS
        refund_receiver = NULL_ADDRESS
        safe = Safe(my_safe_address, self.ethereum_client)
        nonce = safe.retrieve_nonce()
        safe_tx = safe.build_multisig_tx(to,
                                         value,
                                         data,
                                         operation,
                                         safe_tx_gas,
                                         data_gas,
                                         gas_price,
                                         gas_token,
                                         refund_receiver,
                                         safe_nonce=nonce).safe_tx_hash

        # Just to make sure we are not miscalculating tx_hash
        contract_multisig_tx_hash = my_safe_contract.functions.getTransactionHash(
            to, value, data, operation, safe_tx_gas, data_gas, gas_price,
            gas_token, refund_receiver, nonce).call()

        self.assertEqual(safe_tx, contract_multisig_tx_hash)

        signatures = [account.signHash(safe_tx) for account in accounts]

        # Check owners are the same
        contract_owners = my_safe_contract.functions.getOwners().call()
        self.assertEqual(set(contract_owners), set(owners))

        invalid_proxy = self.deploy_example_erc20(1, Account.create().address)
        with self.assertRaises(InvalidProxyContract):
            SafeContractFactory(address=invalid_proxy.address)
            self.transaction_service.create_multisig_tx(
                invalid_proxy.address,
                to,
                value,
                data,
                operation,
                safe_tx_gas,
                data_gas,
                gas_price,
                gas_token,
                refund_receiver,
                nonce,
                signatures,
            )

        # Use invalid master copy
        random_master_copy = Account.create().address
        proxy_create_tx = get_paying_proxy_contract(self.w3).constructor(
            random_master_copy, b'', NULL_ADDRESS, NULL_ADDRESS,
            0).buildTransaction({'from': self.ethereum_test_account.address})
        tx_hash = self.ethereum_client.send_unsigned_transaction(
            proxy_create_tx, private_key=self.ethereum_test_account.key)
        tx_receipt = self.ethereum_client.get_transaction_receipt(tx_hash,
                                                                  timeout=60)
        proxy_address = tx_receipt.contractAddress
        with self.assertRaises(InvalidMasterCopyAddress):
            SafeContractFactory(address=proxy_address)
            self.transaction_service.create_multisig_tx(
                proxy_address,
                to,
                value,
                data,
                operation,
                safe_tx_gas,
                data_gas,
                gas_price,
                gas_token,
                refund_receiver,
                nonce,
                signatures,
            )

        with self.assertRaises(NotEnoughFundsForMultisigTx):
            self.transaction_service.create_multisig_tx(
                my_safe_address,
                to,
                value,
                data,
                operation,
                safe_tx_gas,
                data_gas,
                gas_price,
                gas_token,
                refund_receiver,
                nonce,
                signatures,
            )

        # Send something to the safe
        self.send_tx({
            'to': my_safe_address,
            'value': safe_balance
        }, funder_account)

        bad_refund_receiver = get_eth_address_with_key()[0]
        with self.assertRaises(InvalidRefundReceiver):
            self.transaction_service.create_multisig_tx(
                my_safe_address,
                to,
                value,
                data,
                operation,
                safe_tx_gas,
                data_gas,
                gas_price,
                gas_token,
                bad_refund_receiver,
                nonce,
                signatures,
            )

        invalid_gas_price = 0
        with self.assertRaises(RefundMustBeEnabled):
            self.transaction_service.create_multisig_tx(
                my_safe_address,
                to,
                value,
                data,
                operation,
                safe_tx_gas,
                data_gas,
                invalid_gas_price,
                gas_token,
                refund_receiver,
                nonce,
                signatures,
            )

        with self.assertRaises(GasPriceTooLow):
            self.transaction_service.create_multisig_tx(
                my_safe_address, to, value, data, operation, safe_tx_gas,
                data_gas,
                self.transaction_service._estimate_tx_gas_price(
                    self.transaction_service._get_minimum_gas_price(),
                    gas_token) - 1, gas_token, refund_receiver, nonce,
                signatures)

        with self.assertRaises(InvalidGasToken):
            invalid_gas_token = Account.create().address
            self.transaction_service.create_multisig_tx(
                my_safe_address, to, value, data, operation, safe_tx_gas,
                data_gas, gas_price, invalid_gas_token, refund_receiver, nonce,
                reversed(signatures))

        with self.assertRaises(SignaturesNotSorted):
            self.transaction_service.create_multisig_tx(
                my_safe_address, to, value, data, operation, safe_tx_gas,
                data_gas, gas_price, gas_token, refund_receiver, nonce,
                reversed(signatures))

        with self.assertRaises(SignerIsBanned):
            for account in accounts:
                BannedSignerFactory(address=account.address)
            self.transaction_service.create_multisig_tx(
                my_safe_address, to, value, data, operation, safe_tx_gas,
                data_gas, gas_price, gas_token, refund_receiver, nonce,
                signatures)
        BannedSigner.objects.all().delete()
        self.assertEqual(BannedSigner.objects.count(), 0)

        sender = self.transaction_service.tx_sender_account.address
        sender_balance = w3.eth.getBalance(sender)
        safe_balance = w3.eth.getBalance(my_safe_address)

        safe_multisig_tx = self.transaction_service.create_multisig_tx(
            my_safe_address,
            to,
            value,
            data,
            operation,
            safe_tx_gas,
            data_gas,
            gas_price,
            gas_token,
            refund_receiver,
            nonce,
            signatures,
        )

        with self.assertRaises(SafeMultisigTxExists):
            self.transaction_service.create_multisig_tx(
                my_safe_address,
                to,
                value,
                data,
                operation,
                safe_tx_gas,
                data_gas,
                gas_price,
                gas_token,
                refund_receiver,
                nonce,
                signatures,
            )

        tx_receipt = w3.eth.waitForTransactionReceipt(
            safe_multisig_tx.ethereum_tx.tx_hash)
        self.assertTrue(tx_receipt['status'])
        self.assertEqual(w3.toChecksumAddress(tx_receipt['from']), sender)
        self.assertEqual(w3.toChecksumAddress(tx_receipt['to']),
                         my_safe_address)
        self.assertGreater(safe_multisig_tx.ethereum_tx.gas_price,
                           gas_price)  # We used minimum gas price

        sender_new_balance = w3.eth.getBalance(sender)
        gas_used = tx_receipt['gasUsed']
        tx_fees = gas_used * safe_multisig_tx.ethereum_tx.gas_price
        estimated_refund = (
            safe_multisig_tx.data_gas +
            safe_multisig_tx.safe_tx_gas) * safe_multisig_tx.gas_price
        real_refund = safe_balance - w3.eth.getBalance(my_safe_address) - value
        # Real refund can be less if not all the `safe_tx_gas` is used
        self.assertGreaterEqual(estimated_refund, real_refund)
        self.assertEqual(sender_new_balance,
                         sender_balance - tx_fees + real_refund)
        self.assertEqual(safe.retrieve_nonce(), 1)

        # Send again the tx and check that works
        nonce += 1
        value = 0
        safe_tx = safe.build_multisig_tx(to,
                                         value,
                                         data,
                                         operation,
                                         safe_tx_gas,
                                         data_gas,
                                         gas_price,
                                         gas_token,
                                         refund_receiver,
                                         safe_nonce=nonce)

        # Use invalid signatures
        with self.assertRaises(InvalidOwners):
            signatures = [
                Account.create().signHash(safe_tx.safe_tx_hash)
                for _ in range(len(accounts))
            ]
            self.transaction_service.create_multisig_tx(
                my_safe_address,
                to,
                value,
                data,
                operation,
                safe_tx_gas,
                data_gas,
                gas_price,
                gas_token,
                refund_receiver,
                nonce,
                signatures,
            )

        signatures = [
            account.signHash(safe_tx.safe_tx_hash) for account in accounts
        ]
        safe_multisig_tx = self.transaction_service.create_multisig_tx(
            my_safe_address,
            to,
            value,
            data,
            operation,
            safe_tx_gas,
            data_gas,
            gas_price,
            gas_token,
            refund_receiver,
            nonce,
            signatures,
        )
        tx_receipt = w3.eth.waitForTransactionReceipt(
            safe_multisig_tx.ethereum_tx.tx_hash)
        self.assertTrue(tx_receipt['status'])
    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')
    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
Esempio n. 6
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
Esempio n. 7
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 test_resend_txs(self):
        # Nothing happens
        call_command(resend_txs.Command())

        w3 = self.w3
        # The balance we will send to the safe
        safe_balance = w3.toWei(0.02, 'ether')

        # Create Safe
        accounts = [self.create_account(), self.create_account()]

        # Signatures must be sorted!
        accounts.sort(key=lambda account: account.address.lower())

        safe_creation = self.deploy_test_safe(
            owners=[x.address for x in accounts],
            threshold=len(accounts),
            initial_funding_wei=safe_balance)
        my_safe_address = safe_creation.safe_address

        to = Account().create().address
        value = safe_balance // 4
        data = HexBytes('')
        operation = 0
        safe_tx_gas = 100000
        data_gas = 300000
        gas_price = self.transaction_service._get_minimum_gas_price()
        gas_token = NULL_ADDRESS
        refund_receiver = NULL_ADDRESS
        safe = Safe(my_safe_address, self.ethereum_client)
        nonce = safe.retrieve_nonce()
        safe_multisig_tx_hash = safe.build_multisig_tx(
            to,
            value,
            data,
            operation,
            safe_tx_gas,
            data_gas,
            gas_price,
            gas_token,
            refund_receiver,
            safe_nonce=nonce).safe_tx_hash

        signatures = [
            account.signHash(safe_multisig_tx_hash) for account in accounts
        ]
        sender = self.transaction_service.tx_sender_account.address

        # Ganache snapshot
        snapshot_id = w3.testing.snapshot()
        safe_multisig_tx = self.transaction_service.create_multisig_tx(
            my_safe_address,
            to,
            value,
            data,
            operation,
            safe_tx_gas,
            data_gas,
            gas_price,
            gas_token,
            refund_receiver,
            nonce,
            signatures,
        )

        tx_receipt = w3.eth.waitForTransactionReceipt(
            safe_multisig_tx.ethereum_tx.tx_hash)
        self.assertTrue(tx_receipt['status'])
        self.assertEqual(w3.toChecksumAddress(tx_receipt['from']), sender)
        self.assertEqual(w3.toChecksumAddress(tx_receipt['to']),
                         my_safe_address)
        self.assertEqual(w3.eth.getBalance(to), value)

        w3.testing.revert(snapshot_id)  # Revert to snapshot in ganache
        self.assertEqual(w3.eth.getBalance(to), 0)

        old_multisig_tx: SafeMultisigTx = SafeMultisigTx.objects.all().first()
        old_multisig_tx.created = timezone.now() - timedelta(days=1)
        old_multisig_tx.save()
        new_gas_price = old_multisig_tx.ethereum_tx.gas_price + 1

        call_command(resend_txs.Command(), gas_price=new_gas_price)
        multisig_tx: SafeMultisigTx = SafeMultisigTx.objects.all().first()
        self.assertNotEqual(multisig_tx.ethereum_tx_id,
                            old_multisig_tx.ethereum_tx_id)
        self.assertEqual(multisig_tx.ethereum_tx.gas_price, new_gas_price)
        self.assertEqual(w3.eth.getBalance(to), value)  # Tx is executed again
        self.assertEqual(multisig_tx.get_safe_tx().__dict__,
                         old_multisig_tx.get_safe_tx().__dict__)
Esempio n. 9
0
    def test_safe_events_indexer(self):
        owner_account_1 = self.ethereum_test_account
        owners = [owner_account_1.address]
        threshold = 1
        to = NULL_ADDRESS
        data = b""
        fallback_handler = NULL_ADDRESS
        payment_token = NULL_ADDRESS
        payment = 0
        payment_receiver = NULL_ADDRESS
        initializer = HexBytes(
            self.safe_contract.functions.setup(
                owners,
                threshold,
                to,
                data,
                fallback_handler,
                payment_token,
                payment,
                payment_receiver,
            ).buildTransaction({
                "gas": 1,
                "gasPrice": 1
            })["data"])
        initial_block_number = self.ethereum_client.current_block_number
        safe_l2_master_copy = SafeMasterCopyFactory(
            address=self.safe_contract.address,
            initial_block_number=initial_block_number,
            tx_block_number=initial_block_number,
            version="1.3.0",
            l2=True,
        )
        ethereum_tx_sent = self.proxy_factory.deploy_proxy_contract(
            self.ethereum_test_account,
            self.safe_contract.address,
            initializer=initializer,
        )
        safe_address = ethereum_tx_sent.contract_address
        safe = Safe(safe_address, self.ethereum_client)
        safe_contract = get_safe_V1_3_0_contract(self.w3, safe_address)
        self.assertEqual(safe_contract.functions.VERSION().call(), "1.3.0")

        self.assertEqual(InternalTx.objects.count(), 0)
        self.assertEqual(InternalTxDecoded.objects.count(), 0)
        self.assertEqual(self.safe_events_indexer.start(), 2)
        self.assertEqual(InternalTxDecoded.objects.count(), 1)
        self.assertEqual(InternalTx.objects.count(),
                         2)  # Proxy factory and setup
        create_internal_tx = InternalTx.objects.filter(
            contract_address=safe_address).get()
        setup_internal_tx = InternalTx.objects.filter(
            contract_address=None).get()

        self.assertEqual(create_internal_tx.trace_address, "1")
        self.assertEqual(create_internal_tx.tx_type,
                         InternalTxType.CREATE.value)
        self.assertIsNone(create_internal_tx.call_type)
        self.assertTrue(create_internal_tx.is_relevant)

        self.assertEqual(setup_internal_tx.trace_address, "1,0")

        txs_decoded_queryset = InternalTxDecoded.objects.pending_for_safes()
        self.assertEqual(SafeStatus.objects.count(), 0)
        self.safe_tx_processor.process_decoded_transactions(
            txs_decoded_queryset.all())
        self.assertEqual(SafeStatus.objects.count(), 1)
        safe_status = SafeStatus.objects.get()
        safe_last_status = SafeLastStatus.objects.get(address=safe_address)
        self.assertEqual(safe_status,
                         SafeStatus.from_status_instance(safe_last_status))
        self.assertEqual(safe_status.master_copy, self.safe_contract.address)
        self.assertEqual(safe_status.owners, owners)
        self.assertEqual(safe_status.threshold, threshold)
        self.assertEqual(safe_status.nonce, 0)
        self.assertEqual(safe_status.enabled_modules, [])
        self.assertIsNone(safe_status.guard)
        self.assertEqual(MultisigTransaction.objects.count(), 0)
        self.assertEqual(MultisigConfirmation.objects.count(), 0)

        # Add an owner but don't update the threshold (nonce: 0) --------------------------------------------------
        owner_account_2 = Account.create()
        data = HexBytes(
            self.safe_contract.functions.addOwnerWithThreshold(
                owner_account_2.address, 1).buildTransaction({
                    "gas": 1,
                    "gasPrice": 1
                })["data"])

        multisig_tx = safe.build_multisig_tx(safe_address, 0, data)
        multisig_tx.sign(owner_account_1.key)
        multisig_tx.execute(self.ethereum_test_account.key)
        # Process events: SafeMultiSigTransaction, AddedOwner, ExecutionSuccess
        self.assertEqual(self.safe_events_indexer.start(), 3)
        self.assertEqual(InternalTx.objects.count(), 5)
        self.safe_tx_processor.process_decoded_transactions(
            txs_decoded_queryset.all())
        # Add one SafeStatus increasing the nonce and another one adding the owner
        self.assertEqual(SafeStatus.objects.count(), 3)
        safe_status = SafeStatus.objects.last_for_address(
            safe_address)  # Processed execTransaction and addOwner
        safe_last_status = SafeLastStatus.objects.get(address=safe_address)
        self.assertEqual(safe_status,
                         SafeStatus.from_status_instance(safe_last_status))
        self.assertEqual(safe_status.owners,
                         [owner_account_2.address, owner_account_1.address])
        self.assertEqual(safe_status.nonce, 1)

        safe_status = SafeStatus.objects.sorted_by_mined()[
            1]  # Just processed execTransaction
        self.assertEqual(safe_status.owners, [owner_account_1.address])
        self.assertEqual(safe_status.nonce, 1)

        self.assertEqual(MultisigTransaction.objects.count(), 1)
        self.assertEqual(
            MultisigTransaction.objects.get().safe_tx_hash,
            multisig_tx.safe_tx_hash.hex(),
        )
        self.assertEqual(MultisigConfirmation.objects.count(), 1)

        # Change threshold (nonce: 1) ------------------------------------------------------------------------------
        data = HexBytes(
            self.safe_contract.functions.changeThreshold(2).buildTransaction({
                "gas":
                1,
                "gasPrice":
                1
            })["data"])

        multisig_tx = safe.build_multisig_tx(safe_address, 0, data)
        multisig_tx.sign(owner_account_1.key)
        multisig_tx.execute(self.ethereum_test_account.key)
        # Process events: SafeMultiSigTransaction, ChangedThreshold, ExecutionSuccess
        self.assertEqual(self.safe_events_indexer.start(), 3)
        self.safe_tx_processor.process_decoded_transactions(
            txs_decoded_queryset.all())
        # Add one SafeStatus increasing the nonce and another one changing the threshold
        self.assertEqual(SafeStatus.objects.count(), 5)
        safe_status = SafeStatus.objects.last_for_address(
            safe_address)  # Processed execTransaction and changeThreshold
        safe_last_status = SafeLastStatus.objects.get(address=safe_address)
        self.assertEqual(safe_status,
                         SafeStatus.from_status_instance(safe_last_status))
        self.assertEqual(safe_status.nonce, 2)
        self.assertEqual(safe_status.threshold, 2)

        safe_status = SafeStatus.objects.sorted_by_mined()[
            1]  # Just processed execTransaction
        self.assertEqual(safe_status.nonce, 2)
        self.assertEqual(safe_status.threshold, 1)

        self.assertEqual(MultisigTransaction.objects.count(), 2)
        self.assertEqual(
            MultisigTransaction.objects.order_by("-nonce")[0].safe_tx_hash,
            multisig_tx.safe_tx_hash.hex(),
        )
        self.assertEqual(MultisigConfirmation.objects.count(), 2)

        # Remove an owner and change threshold back to 1 (nonce: 2) --------------------------------------------------
        data = HexBytes(
            self.safe_contract.functions.removeOwner(SENTINEL_ADDRESS,
                                                     owner_account_2.address,
                                                     1).buildTransaction({
                                                         "gas":
                                                         1,
                                                         "gasPrice":
                                                         1
                                                     })["data"])

        multisig_tx = safe.build_multisig_tx(safe_address, 0, data)
        multisig_tx.sign(owner_account_1.key)
        multisig_tx.sign(owner_account_2.key)
        multisig_tx.execute(self.ethereum_test_account.key)
        # Process events: SafeMultiSigTransaction, RemovedOwner, ChangedThreshold, ExecutionSuccess
        self.assertEqual(self.safe_events_indexer.start(), 4)
        self.safe_tx_processor.process_decoded_transactions(
            txs_decoded_queryset.all())
        # Add one SafeStatus increasing the nonce and another one removing the owner
        self.assertEqual(SafeStatus.objects.count(), 8)
        safe_status = SafeStatus.objects.last_for_address(
            safe_address
        )  # Processed execTransaction, removeOwner and changeThreshold
        safe_last_status = SafeLastStatus.objects.get(address=safe_address)
        self.assertEqual(safe_status,
                         SafeStatus.from_status_instance(safe_last_status))
        self.assertEqual(safe_status.nonce, 3)
        self.assertEqual(safe_status.threshold, 1)
        self.assertEqual(safe_status.owners, [owner_account_1.address])

        safe_status = SafeStatus.objects.sorted_by_mined()[
            1]  # Processed execTransaction and removeOwner
        self.assertEqual(safe_status.nonce, 3)
        self.assertEqual(safe_status.threshold, 2)
        self.assertEqual(safe_status.owners, [owner_account_1.address])

        safe_status = SafeStatus.objects.sorted_by_mined()[
            2]  # Just processed execTransaction
        self.assertEqual(safe_status.nonce, 3)
        self.assertEqual(safe_status.threshold, 2)
        self.assertCountEqual(
            safe_status.owners,
            [owner_account_1.address, owner_account_2.address])

        self.assertEqual(MultisigTransaction.objects.count(), 3)
        self.assertEqual(
            MultisigTransaction.objects.order_by("-nonce")[0].safe_tx_hash,
            multisig_tx.safe_tx_hash.hex(),
        )
        self.assertEqual(MultisigConfirmation.objects.count(), 4)

        # Enable module (nonce: 3) ---------------------------------------------------------------------
        module_address = Account.create().address
        data = HexBytes(
            self.safe_contract.functions.enableModule(
                module_address).buildTransaction({
                    "gas": 1,
                    "gasPrice": 1
                })["data"])

        multisig_tx = safe.build_multisig_tx(safe_address, 0, data)
        multisig_tx.sign(owner_account_1.key)
        multisig_tx.execute(self.ethereum_test_account.key)
        # Process events: SafeMultiSigTransaction, EnabledModule, ExecutionSuccess
        self.assertEqual(self.safe_events_indexer.start(), 3)
        self.safe_tx_processor.process_decoded_transactions(
            txs_decoded_queryset.all())
        # Add one SafeStatus increasing the nonce and another one enabling the module
        self.assertEqual(SafeStatus.objects.count(), 10)
        safe_status = SafeStatus.objects.last_for_address(
            safe_address)  # Processed execTransaction and enableModule
        safe_last_status = SafeLastStatus.objects.get(address=safe_address)
        self.assertEqual(safe_status,
                         SafeStatus.from_status_instance(safe_last_status))
        self.assertEqual(safe_status.enabled_modules, [module_address])
        self.assertEqual(safe_status.nonce, 4)

        safe_status = SafeStatus.objects.sorted_by_mined()[
            1]  # Just processed execTransaction
        self.assertEqual(safe_status.enabled_modules, [])
        self.assertEqual(safe_status.nonce, 4)

        self.assertEqual(MultisigTransaction.objects.count(), 4)
        self.assertEqual(
            MultisigTransaction.objects.order_by("-nonce")[0].safe_tx_hash,
            multisig_tx.safe_tx_hash.hex(),
        )
        self.assertEqual(MultisigConfirmation.objects.count(), 5)

        # Check SafeReceived (ether received) on Safe -----------------------------------------------------------------
        value = 1256
        self.ethereum_client.get_transaction_receipt(
            self.send_ether(safe_address, value))
        # Process events: SafeReceived
        self.assertEqual(self.safe_events_indexer.start(), 1)
        self.safe_tx_processor.process_decoded_transactions(
            txs_decoded_queryset.all())
        # Check there's an ether transaction
        internal_tx_queryset = InternalTx.objects.filter(
            value=value,
            tx_type=InternalTxType.CALL.value,
            call_type=EthereumTxCallType.CALL.value,
        )
        self.assertTrue(internal_tx_queryset.exists())
        self.assertTrue(internal_tx_queryset.get().is_ether_transfer)

        # Set fallback handler (nonce: 4) --------------------------------------------------------------------------
        new_fallback_handler = Account.create().address
        data = HexBytes(
            self.safe_contract.functions.setFallbackHandler(
                new_fallback_handler).buildTransaction({
                    "gas": 1,
                    "gasPrice": 1
                })["data"])

        multisig_tx = safe.build_multisig_tx(safe_address, 0, data)
        multisig_tx.sign(owner_account_1.key)
        multisig_tx.execute(self.ethereum_test_account.key)
        # Process events: SafeMultiSigTransaction, ChangedFallbackHandler, ExecutionSuccess
        self.assertEqual(self.safe_events_indexer.start(), 3)
        self.safe_tx_processor.process_decoded_transactions(
            txs_decoded_queryset.all())
        # Add one SafeStatus increasing the nonce and another one changing the fallback handler
        self.assertEqual(SafeStatus.objects.count(), 12)
        safe_status = SafeStatus.objects.last_for_address(
            safe_address)  # Processed execTransaction and setFallbackHandler
        safe_last_status = SafeLastStatus.objects.get(address=safe_address)
        self.assertEqual(safe_status,
                         SafeStatus.from_status_instance(safe_last_status))
        self.assertEqual(safe_status.fallback_handler, new_fallback_handler)
        self.assertEqual(safe_status.enabled_modules, [module_address])
        self.assertEqual(safe_status.nonce, 5)

        safe_status = SafeStatus.objects.sorted_by_mined()[
            1]  # Just processed execTransaction
        self.assertEqual(safe_status.fallback_handler, fallback_handler)
        self.assertEqual(safe_status.enabled_modules, [module_address])
        self.assertEqual(safe_status.nonce, 5)

        self.assertEqual(MultisigTransaction.objects.count(), 5)
        self.assertEqual(
            MultisigTransaction.objects.order_by("-nonce")[0].safe_tx_hash,
            multisig_tx.safe_tx_hash.hex(),
        )
        self.assertEqual(MultisigConfirmation.objects.count(), 6)

        # Disable Module (nonce: 5) ----------------------------------------------------------------------------------
        data = HexBytes(
            self.safe_contract.functions.disableModule(
                SENTINEL_ADDRESS, module_address).buildTransaction({
                    "gas":
                    1,
                    "gasPrice":
                    1
                })["data"])

        multisig_tx = safe.build_multisig_tx(safe_address, 0, data)
        multisig_tx.sign(owner_account_1.key)
        multisig_tx.execute(self.ethereum_test_account.key)
        # Process events: SafeMultiSigTransaction, DisabledModule, ExecutionSuccess
        self.assertEqual(self.safe_events_indexer.start(), 3)
        self.safe_tx_processor.process_decoded_transactions(
            txs_decoded_queryset.all())
        # Add one SafeStatus increasing the nonce and another one disabling the module
        self.assertEqual(SafeStatus.objects.count(), 14)
        safe_status = SafeStatus.objects.last_for_address(
            safe_address)  # Processed execTransaction and disableModule
        safe_last_status = SafeLastStatus.objects.get(address=safe_address)
        self.assertEqual(safe_status,
                         SafeStatus.from_status_instance(safe_last_status))
        self.assertEqual(safe_status.nonce, 6)
        self.assertEqual(safe_status.enabled_modules, [])

        safe_status = SafeStatus.objects.sorted_by_mined()[
            1]  # Just processed execTransaction
        self.assertEqual(safe_status.nonce, 6)
        self.assertEqual(safe_status.enabled_modules, [module_address])

        self.assertEqual(MultisigTransaction.objects.count(), 6)
        self.assertEqual(
            MultisigTransaction.objects.order_by("-nonce")[0].safe_tx_hash,
            multisig_tx.safe_tx_hash.hex(),
        )
        self.assertEqual(MultisigConfirmation.objects.count(), 7)

        # ApproveHash (no nonce) ------------------------------------------------------------------------------------
        random_hash = self.w3.keccak(text="Get schwifty")
        tx = (safe.get_contract().functions.approveHash(
            random_hash).buildTransaction({
                "from":
                owner_account_1.address,
                "nonce":
                self.ethereum_client.get_nonce_for_account(
                    owner_account_1.address),
            }))
        tx = owner_account_1.sign_transaction(tx)
        self.w3.eth.send_raw_transaction(tx["rawTransaction"])
        # Process events: ApproveHash
        self.assertEqual(self.safe_events_indexer.start(), 1)
        self.safe_tx_processor.process_decoded_transactions(
            txs_decoded_queryset.all())
        # No SafeStatus was added
        self.assertEqual(SafeStatus.objects.count(), 14)
        # Check a MultisigConfirmation was created
        self.assertTrue(
            MultisigConfirmation.objects.filter(
                multisig_transaction_hash=random_hash.hex()).exists())
        self.assertEqual(MultisigTransaction.objects.count(),
                         6)  # No MultisigTransaction was created
        self.assertEqual(MultisigConfirmation.objects.count(),
                         8)  # A MultisigConfirmation was created

        # Send ether (nonce: 6) ----------------------------------------------------------------------------------
        data = b""
        value = 122
        to = Account.create().address
        multisig_tx = safe.build_multisig_tx(to, value, data)
        multisig_tx.sign(owner_account_1.key)
        multisig_tx.execute(self.ethereum_test_account.key)
        # Process events: SafeMultiSigTransaction, ExecutionSuccess
        self.assertEqual(self.safe_events_indexer.start(), 2)
        self.safe_tx_processor.process_decoded_transactions(
            txs_decoded_queryset.all())
        # Add one SafeStatus increasing the nonce
        self.assertEqual(SafeStatus.objects.count(), 15)
        safe_status = SafeStatus.objects.last_for_address(
            safe_address)  # Processed execTransaction
        safe_last_status = SafeLastStatus.objects.get(address=safe_address)
        self.assertEqual(safe_status,
                         SafeStatus.from_status_instance(safe_last_status))
        self.assertEqual(safe_status.nonce, 7)
        self.assertTrue(
            InternalTx.objects.filter(value=value,
                                      to=to).get().is_ether_transfer)

        self.assertEqual(MultisigTransaction.objects.count(), 7)
        self.assertEqual(
            MultisigTransaction.objects.order_by("-nonce")[0].safe_tx_hash,
            multisig_tx.safe_tx_hash.hex(),
        )
        self.assertEqual(MultisigConfirmation.objects.count(), 9)

        # Set guard (nonce: 7) INVALIDATES SAFE, as no more transactions can be done ---------------------------------
        guard_address = Account.create().address
        data = HexBytes(
            self.safe_contract.functions.setGuard(
                guard_address).buildTransaction({
                    "gas": 1,
                    "gasPrice": 1
                })["data"])

        multisig_tx = safe.build_multisig_tx(safe_address, 0, data)
        multisig_tx.sign(owner_account_1.key)
        multisig_tx.execute(self.ethereum_test_account.key)
        # Process events: SafeMultiSigTransaction, ChangedGuard, ExecutionSuccess
        self.assertEqual(self.safe_events_indexer.start(), 3)
        self.safe_tx_processor.process_decoded_transactions(
            txs_decoded_queryset.all())
        # Add one SafeStatus increasing the nonce and another one changing the guard
        self.assertEqual(SafeStatus.objects.count(), 17)
        safe_status = SafeStatus.objects.last_for_address(
            safe_address)  # Processed execTransaction and setGuard
        safe_last_status = SafeLastStatus.objects.get(address=safe_address)
        self.assertEqual(safe_status,
                         SafeStatus.from_status_instance(safe_last_status))
        self.assertEqual(safe_status.nonce, 8)
        self.assertEqual(safe_status.guard, guard_address)

        safe_status = SafeStatus.objects.sorted_by_mined()[
            1]  # Just processed execTransaction
        self.assertEqual(safe_status.nonce, 8)
        self.assertIsNone(safe_status.guard)

        # Check master copy did not change during the execution
        self.assertEqual(
            SafeStatus.objects.last_for_address(safe_address).master_copy,
            self.safe_contract.address,
        )

        self.assertEqual(
            MultisigTransaction.objects.order_by("-nonce")[0].safe_tx_hash,
            multisig_tx.safe_tx_hash.hex(),
        )
        expected_multisig_transactions = 8
        expected_multisig_confirmations = 10
        expected_safe_statuses = 17
        expected_internal_txs = 29
        expected_internal_txs_decoded = 18
        self.assertEqual(MultisigTransaction.objects.count(),
                         expected_multisig_transactions)
        self.assertEqual(MultisigConfirmation.objects.count(),
                         expected_multisig_confirmations)
        self.assertEqual(SafeStatus.objects.count(), expected_safe_statuses)
        self.assertEqual(InternalTx.objects.count(), expected_internal_txs)
        self.assertEqual(InternalTxDecoded.objects.count(),
                         expected_internal_txs_decoded)

        # Event processing should be idempotent, so no changes must be done if everything is processed again
        self.assertTrue(
            self.safe_events_indexer._is_setup_indexed(safe_address))
        safe_l2_master_copy.tx_block_number = initial_block_number
        safe_l2_master_copy.save(update_fields=["tx_block_number"])
        self.assertEqual(self.safe_events_indexer.start(),
                         0)  # No new events are processed when reindexing
        InternalTxDecoded.objects.update(processed=False)
        SafeStatus.objects.all().delete()
        self.assertEqual(
            len(
                self.safe_tx_processor.process_decoded_transactions(
                    txs_decoded_queryset.all())),
            expected_internal_txs_decoded,
        )
        self.assertEqual(MultisigTransaction.objects.count(),
                         expected_multisig_transactions)
        self.assertEqual(MultisigConfirmation.objects.count(),
                         expected_multisig_confirmations)
        self.assertEqual(SafeStatus.objects.count(), expected_safe_statuses)
        self.assertEqual(InternalTx.objects.count(), expected_internal_txs)
        self.assertEqual(InternalTxDecoded.objects.count(),
                         expected_internal_txs_decoded)
    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` for the same Safe.
        # We allow duplicated if existing tx is not executed
        try:
            multisig_transaction: MultisigTransaction = MultisigTransaction.objects.exclude(
                ethereum_tx=None).exclude(
                    safe_tx_hash=contract_transaction_hash).get(
                        safe=safe.address, nonce=data['nonce'])
            if multisig_transaction.safe_tx_hash != contract_transaction_hash:
                raise ValidationError(
                    f'Tx with nonce={safe_tx.safe_nonce} for safe={safe.address} already executed in '
                    f'tx-hash={multisig_transaction.ethereum_tx_id}')
        except MultisigTransaction.DoesNotExist:
            pass

        # Check owners and old owners, owner might be removed but that tx can still be signed by that owner
        if not safe.retrieve_is_owner(data['sender']):
            try:
                # TODO Fix this, we can use SafeStatus now
                if not safe.retrieve_is_owner(
                        data['sender'],
                        block_identifier=max(
                            0, ethereum_client.current_block_number - 20)):
                    raise ValidationError('User is not an owner')
            except BadFunctionCallOutput:  # If it didn't exist 20 blocks ago
                raise ValidationError('User is not an owner')

        signature = data.get('signature')
        if signature is not None:
            #  TODO Support signatures with multiple owners
            if len(signature) != 65:
                raise ValidationError(
                    'Signatures with more than one owner still not supported')

            safe_signature = SafeSignature(signature,
                                           contract_transaction_hash)
            #  TODO Support contract signatures and approved hashes
            if safe_signature.signature_type == SafeSignatureType.CONTRACT_SIGNATURE:
                raise ValidationError('Contract signatures not supported')
            elif safe_signature.signature_type == SafeSignatureType.APPROVED_HASH:
                # Index it automatically later
                del data['signature']

            address = safe_signature.owner
            if address != data['sender']:
                raise ValidationError(
                    f'Signature does not match sender={data["sender"]}. Calculated owner={address}'
                )

        return data
Esempio n. 11
0
    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,
                          tx_gas=None,
                          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 data-gas=%d. Current is "
                                       "safe-tx-gas=%d and data-gas=%d" %
                                       (safe_tx_gas_estimation, safe_base_gas_estimation, safe_tx_gas, base_gas))

        # Use user provided gasPrice for TX if more than our stardard gas price
        standard_gas = self._get_configured_gas_price()
        if gas_price > standard_gas :
            tx_gas_price = gas_price
        else:
            tx_gas_price = standard_gas

        tx_sender_private_key = self.tx_sender_account.privateKey
        tx_sender_address = Account.privateKeyToAccount(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()
        )

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

        safe_tx.call(tx_sender_address=tx_sender_address, block_identifier=block_identifier)

        logger.info('_send_multisig_tx about to execute tx for safe=%s and nonce=%s', safe_address, safe_nonce)
        with EthereumNonceLock(self.redis, self.ethereum_client, self.tx_sender_account.address,
                               timeout=60 * 2) as tx_nonce:
            logger.info('_send_multisig_tx executing tx for safe=%s and nonce=%s', safe_address, safe_nonce)
            tx_hash, tx = safe_tx.execute(tx_sender_private_key, tx_gas=tx_gas, tx_gas_price=tx_gas_price,
                                          tx_nonce=tx_nonce, block_identifier=block_identifier)
            return tx_hash, safe_tx.tx_hash, tx
Esempio n. 12
0
    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,
                          tx_gas: Optional[int] = None,
                          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()
        )

        if safe_tx.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))

        logger.info('Safe=%s safe-nonce=%d Check `call()` before sending transaction', safe_address, safe_nonce)
        # Set `gasLimit` for `call()`. It will use the same that it will be used later for execution
        tx_gas = safe_tx.base_gas + safe_tx.safe_tx_gas + 25000
        safe_tx.call(tx_gas=tx_gas, tx_sender_address=tx_sender_address, block_identifier=block_identifier)
        logger.info('Safe=%s safe-nonce=%d `call()` was successful', safe_address, safe_nonce)

        with EthereumNonceLock(self.redis, self.ethereum_client, self.tx_sender_account.address,
                               lock_timeout=60 * 2) as tx_nonce:
            tx_hash, tx = safe_tx.execute(tx_sender_private_key, tx_gas=tx_gas, tx_gas_price=tx_gas_price,
                                          tx_nonce=tx_nonce, block_identifier=block_identifier)
            logger.info('Safe=%s, Sent transaction with nonce=%d tx-hash=%s for safe-tx-hash=%s safe-nonce=%d',
                        safe_address, tx_nonce, tx_hash.hex(), safe_tx.safe_tx_hash.hex(), safe_tx.safe_nonce)
            return tx_hash, safe_tx.safe_tx_hash, tx
    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