Example #1
0
 def save(self, **kwargs):
     safe_address = self.context['safe_address']
     ethereum_client = EthereumClientProvider()
     safe = Safe(safe_address, ethereum_client)
     safe_tx_gas = safe.estimate_tx_gas(self.validated_data['to'], self.validated_data['value'],
                                        self.validated_data['data'], self.validated_data['operation'])
     return {'safe_tx_gas': safe_tx_gas}
    def validate(self, data):
        super().validate(data)

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

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

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

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

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

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

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

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

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

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

            signature_owners.append(owner)
        return signature
    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)
Example #5
0
    def deploy_test_safe_v1_0_0(
        self,
        number_owners: int = 3,
        threshold: Optional[int] = None,
        owners: Optional[List[ChecksumAddress]] = None,
        initial_funding_wei: int = 0,
    ) -> Safe:
        owners = (owners if owners else
                  [Account.create().address for _ in range(number_owners)])
        if not threshold:
            threshold = len(owners) - 1 if len(owners) > 1 else 1
        empty_parameters = {"gas": 1, "gasPrice": 1}
        to = NULL_ADDRESS
        data = b""
        payment_token = NULL_ADDRESS
        payment = 0
        payment_receiver = NULL_ADDRESS
        initializer = HexBytes(
            self.safe_contract_V1_0_0.functions.setup(
                owners, threshold, to, data, payment_token, payment,
                payment_receiver).buildTransaction(empty_parameters)["data"])
        ethereum_tx_sent = self.proxy_factory.deploy_proxy_contract(
            self.ethereum_test_account,
            self.safe_contract_V1_0_0_address,
            initializer=initializer,
        )
        safe = Safe(ethereum_tx_sent.contract_address, self.ethereum_client)
        if initial_funding_wei:
            self.send_ether(safe.address, initial_funding_wei)

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

        return safe
Example #6
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'])
Example #7
0
    def test_deploy_proxy_contract_with_nonce(self):
        salt_nonce = generate_salt_nonce()
        owners = [Account.create().address for _ in range(2)]
        threshold = 2
        payment_token = None
        safe_create2_tx = Safe.build_safe_create2_tx(
            self.ethereum_client, self.safe_contract_address,
            self.proxy_factory_contract_address, salt_nonce, owners, threshold,
            self.gas_price, payment_token)
        # Send ether for safe deploying costs
        self.send_tx(
            {
                'to': safe_create2_tx.safe_address,
                'value': safe_create2_tx.payment
            }, self.ethereum_test_account)

        proxy_factory = ProxyFactory(self.proxy_factory_contract_address,
                                     self.ethereum_client)
        ethereum_tx_sent = proxy_factory.deploy_proxy_contract_with_nonce(
            self.ethereum_test_account,
            safe_create2_tx.master_copy_address,
            safe_create2_tx.safe_setup_data,
            salt_nonce,
            safe_create2_tx.gas,
            gas_price=self.gas_price)
        receipt = self.ethereum_client.get_transaction_receipt(
            ethereum_tx_sent.tx_hash, timeout=20)
        self.assertEqual(receipt.status, 1)
        safe = Safe(ethereum_tx_sent.contract_address, self.ethereum_client)
        self.assertEqual(ethereum_tx_sent.contract_address,
                         safe_create2_tx.safe_address)
        self.assertEqual(set(safe.retrieve_owners()), set(owners))
        self.assertEqual(safe.retrieve_master_copy_address(),
                         safe_create2_tx.master_copy_address)
    def test_change_fallback_handler(self):
        safe_operator = self.setup_operator()
        safe = Safe(safe_operator.address, self.ethereum_client)
        current_fallback_handler = safe.retrieve_fallback_handler()
        with self.assertRaises(SameFallbackHandlerException):
            safe_operator.change_fallback_handler(current_fallback_handler)

        new_fallback_handler = Account.create().address
        with self.assertRaises(
            InvalidFallbackHandlerException
        ):  # Contract does not exist
            self.assertTrue(safe_operator.change_fallback_handler(new_fallback_handler))

        with mock.patch.object(
            EthereumClient, "is_contract", autospec=True, return_value=True
        ):
            self.assertTrue(safe_operator.change_fallback_handler(new_fallback_handler))
        self.assertEqual(
            safe_operator.safe_cli_info.fallback_handler, new_fallback_handler
        )
        self.assertEqual(safe.retrieve_fallback_handler(), new_fallback_handler)

        safe_operator.change_master_copy(self.safe_old_contract_address)
        with self.assertRaises(FallbackHandlerNotSupportedException):
            safe_operator.change_fallback_handler(Account.create().address)
Example #9
0
 def get_safe_info(self, safe_address: str) -> SafeInfo:
     try:
         safe = Safe(safe_address, self.ethereum_client)
         return safe.retrieve_all_info()
     except IOError as exc:
         raise NodeConnectionError from exc
     except CannotRetrieveSafeInfoException as e:
         raise CannotGetSafeInfo from e
 def get_safe_owners(
         self, ethereum_client: EthereumClient,
         safe_address: ChecksumAddress) -> List[ChecksumAddress]:
     safe = Safe(safe_address, ethereum_client)
     try:
         return safe.retrieve_owners(block_identifier="pending")
     except BadFunctionCallOutput:  # Error using pending block identifier
         return safe.retrieve_owners(block_identifier="latest")
Example #11
0
    def retrieve_safe_info(self, address: str) -> SafeInfo:
        safe = Safe(address, self.ethereum_client)
        if not self.ethereum_client.is_contract(address):
            raise SafeNotDeployed('Safe with address=%s not deployed' %
                                  address)

        try:
            return safe.retrieve_all_info()
        except CannotRetrieveSafeInfoException as e:
            raise CannotRetrieveSafeInfo(address) from e
Example #12
0
def get_safe_owners(safe_address: str) -> Sequence[str]:
    ethereum_client = EthereumClientProvider()
    safe = Safe(safe_address, ethereum_client)
    try:
        return safe.retrieve_owners(block_identifier="pending")
    except BadFunctionCallOutput:  # Error using pending block identifier
        try:
            return safe.retrieve_owners(block_identifier="latest")
        except BadFunctionCallOutput:
            return []
Example #13
0
    def resend(self, gas_price: int, multisig_tx: SafeMultisigTx) -> Optional[EthereumTx]:
        """
        Resend transaction with `gas_price` if it's higher or equal than transaction gas price. Setting equal
        `gas_price` is allowed as sometimes a transaction can be out of the mempool but `gas_price` does not need
        to be increased when resending
        :param gas_price: New gas price for the transaction. Must be >= old gas price
        :param multisig_tx: Multisig Tx not mined to be sent again
        :return: If a new transaction is sent is returned, `None` if not
        """
        if multisig_tx.ethereum_tx.gas_price > gas_price:
            logger.info(
                '%s tx gas price is %d > %d. Nothing to do here',
                multisig_tx.ethereum_tx_id, multisig_tx.ethereum_tx.gas_price, gas_price
            )
            return None

        assert multisig_tx.ethereum_tx.block_id is None, 'Block is present!'
        safe = Safe(multisig_tx.safe_id, self.ethereum_client)
        try:
            if safe.retrieve_nonce() > multisig_tx.nonce:
                multisig_tx.delete()  # Transaction is not valid anymore
                return None
        except (ValueError, BadFunctionCallOutput):
            logger.error('Something is wrong with Safe %s, cannot retrieve nonce', multisig_tx.safe_id,
                         exc_info=True)
            return None

        logger.info(
            '%s tx gas price was %d. Resending with new gas price %d',
            multisig_tx.ethereum_tx_id, multisig_tx.ethereum_tx.gas_price, gas_price
        )
        safe_tx = multisig_tx.get_safe_tx(self.ethereum_client)
        tx_gas = safe_tx.recommended_gas()
        try:
            tx_hash, tx = safe_tx.execute(self.tx_sender_account.key, tx_gas=tx_gas, tx_gas_price=gas_price,
                                          tx_nonce=multisig_tx.ethereum_tx.nonce)
        except ValueError:
            # ValueError({'code': -32010, 'message': 'Transaction nonce is too low. Try incrementing the nonce.'})
            try:
                # Check that transaction is still valid
                safe_tx.call(tx_sender_address=self.tx_sender_account.address, tx_gas=tx_gas)
            except InvalidMultisigTx:
                # Maybe there's a transaction with a lower nonce that must be mined before
                # It doesn't matter, as soon as a transaction with a newer nonce is added it will be deleted
                return None
            # Send transaction again with a new 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(self.tx_sender_account.key, tx_gas=tx_gas, tx_gas_price=gas_price,
                                              tx_nonce=tx_nonce)

        multisig_tx.ethereum_tx = EthereumTx.objects.create_from_tx_dict(tx, tx_hash)
        multisig_tx.full_clean(validate_unique=False)
        multisig_tx.save(update_fields=['ethereum_tx'])
        return multisig_tx.ethereum_tx
 def get_last_used_nonce(self, safe_address: str) -> Optional[int]:
     safe = Safe(safe_address, self.ethereum_client)
     last_used_nonce = SafeMultisigTx.objects.get_last_nonce_for_safe(safe_address)
     last_used_nonce = last_used_nonce if last_used_nonce is not None else -1
     try:
         blockchain_nonce = safe.retrieve_nonce()
         last_used_nonce = max(last_used_nonce, blockchain_nonce - 1)
         if last_used_nonce < 0:  # There's no last_used_nonce
             last_used_nonce = None
         return last_used_nonce
     except BadFunctionCallOutput:  # If Safe does not exist
         raise SafeDoesNotExist(f'Safe={safe_address} does not exist')
 def setUpClass(cls) -> None:
     cls.ethereum_node_url = cls.ETHEREUM_NODE_URL
     cls.ethereum_client = EthereumClient(cls.ethereum_node_url)
     cls.w3 = cls.ethereum_client.w3
     cls.ethereum_test_account = Account.from_key(cls.ETHEREUM_ACCOUNT_KEY)
     cls.safe_contract_address = Safe.deploy_master_contract(cls.ethereum_client,
                                                             cls.ethereum_test_account).contract_address
     cls.safe_old_contract_address = Safe.deploy_master_contract_v1_0_0(cls.ethereum_client,
                                                                        cls.ethereum_test_account).contract_address
     cls.proxy_factory = ProxyFactory(
         ProxyFactory.deploy_proxy_factory_contract(cls.ethereum_client,
                                                    cls.ethereum_test_account).contract_address,
         cls.ethereum_client)
 def save(self, **kwargs):
     safe_address = self.context['safe_address']
     ethereum_client = EthereumClientProvider()
     safe = Safe(safe_address, ethereum_client)
     try:
         safe_tx_gas = safe.estimate_tx_gas(
             self.validated_data['to'], self.validated_data['value'],
             self.validated_data['data'], self.validated_data['operation'])
     except IOError as exc:
         raise NodeConnectionError(
             f'Node connection error when estimating gas for safe {safe_address}'
         ) from exc
     return {'safe_tx_gas': safe_tx_gas}
    def validate(self, data):
        super().validate(data)

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

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

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

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

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

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

        data['delegator'] = delegator
        return data
    def test_safe_creation(self):
        salt_nonce = generate_salt_nonce()
        owners = [Account.create().address for _ in range(2)]
        data = {
            'saltNonce': salt_nonce,
            'owners': owners,
            'threshold': len(owners)
        }
        response = self.client.post(reverse('v3:safe-creation'), data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        response_json = response.json()
        safe_address = response_json['safe']
        self.assertTrue(check_checksum(safe_address))
        self.assertTrue(check_checksum(response_json['paymentReceiver']))
        self.assertEqual(response_json['paymentToken'], NULL_ADDRESS)
        self.assertEqual(int(response_json['payment']),
                         int(response_json['gasEstimated']) * int(response_json['gasPriceEstimated']))
        self.assertGreater(int(response_json['gasEstimated']), 0)
        self.assertGreater(int(response_json['gasPriceEstimated']), 0)
        self.assertGreater(len(response_json['setupData']), 2)
        self.assertEqual(response_json['masterCopy'], settings.SAFE_CONTRACT_ADDRESS)

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

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

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

        data = {
            'salt_nonce': -1,
            'owners': owners,
            'threshold': 2
        }
        response = self.client.post(reverse('v3:safe-creation'), data, format='json')
        self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
Example #19
0
    def build_test_safe(
        self,
        number_owners: int = 3,
        threshold: Optional[int] = None,
        owners: Optional[List[str]] = None,
        fallback_handler: Optional[str] = None,
    ) -> SafeCreate2Tx:
        salt_nonce = generate_salt_nonce()
        owners = (owners if owners else
                  [Account.create().address for _ in range(number_owners)])
        threshold = threshold if threshold else len(owners) - 1

        gas_price = self.ethereum_client.w3.eth.gas_price
        return Safe.build_safe_create2_tx(
            self.ethereum_client,
            self.safe_contract_address,
            self.proxy_factory_contract_address,
            salt_nonce,
            owners,
            threshold,
            fallback_handler=fallback_handler,
            gas_price=gas_price,
            payment_token=None,
            fixed_creation_cost=0,
        )
Example #20
0
 def estimate_safe_creation2(
         self,
         number_owners: int,
         payment_token: Optional[str] = None) -> SafeCreationEstimate:
     """
     :param number_owners:
     :param payment_token:
     :return:
     :raises: InvalidPaymentToken
     """
     payment_token = payment_token or NULL_ADDRESS
     payment_token_eth_value = self._get_token_eth_value_or_raise(
         payment_token)
     gas_price = self._get_configured_gas_price()
     fixed_creation_cost = self.safe_fixed_creation_cost
     return Safe.estimate_safe_creation_2(
         self.ethereum_client,
         self.safe_contract_address,
         self.proxy_factory.address,
         number_owners,
         gas_price,
         payment_token,
         payment_receiver=self.funder_account.address,
         fallback_handler=self.default_callback_handler,
         payment_token_eth_value=payment_token_eth_value,
         fixed_creation_cost=fixed_creation_cost)
Example #21
0
 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.network_name: str = self.network.name
     self.network_number: int = self.network.value
     self.etherscan = Etherscan.from_network_number(self.network_number)
     self.safe_tx_service_url = TransactionService.from_network_number(self.network_number)
     self.safe_relay_service_url = RelayService.from_network_number(self.network_number)
     self.safe = Safe(address, self.ethereum_client)
     self.safe_contract = self.safe.get_contract()
     self.accounts: Set[Account] = set()
     self.default_sender: Optional[Account] = None
     self.executed_transactions: List[str] = []
     self._safe_cli_info: Optional[SafeCliInfo] = None  # Cache for SafeCliInfo
Example #22
0
    def test_change_master_copy(self):
        safe_operator = self.setup_operator()
        safe = Safe(safe_operator.address, self.ethereum_client)
        current_master_copy = safe.retrieve_master_copy_address()
        with self.assertRaises(SameMasterCopyException):
            safe_operator.change_master_copy(current_master_copy)

        random_address = Account.create().address
        with self.assertRaises(InvalidMasterCopyException):
            safe_operator.change_master_copy(random_address)

        self.assertTrue(
            safe_operator.change_master_copy(self.safe_old_contract_address))
        self.assertEqual(safe_operator.safe_cli_info.master_copy,
                         self.safe_old_contract_address)
        self.assertEqual(safe.retrieve_master_copy_address(),
                         self.safe_old_contract_address)
Example #23
0
    def test_remove_owner(self):
        safe_address = self.deploy_test_safe(
            owners=[self.ethereum_test_account.address]).safe_address
        safe_operator = SafeOperator(safe_address, self.ethereum_node_url)
        random_address = Account.create().address
        with self.assertRaises(NonExistingOwnerException):
            safe_operator.remove_owner(random_address)

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

        self.assertTrue(safe_operator.remove_owner(new_owner))
        self.assertNotIn(new_owner, safe_operator.accounts)
        self.assertNotIn(new_owner, safe.retrieve_owners())
Example #24
0
    def test_change_fallback_handler(self):
        safe_operator = self.setup_operator()
        safe = Safe(safe_operator.address, self.ethereum_client)
        current_fallback_handler = safe.retrieve_fallback_handler()
        with self.assertRaises(SameFallbackHandlerException):
            safe_operator.change_fallback_handler(current_fallback_handler)

        new_fallback_handler = Account.create().address
        self.assertTrue(
            safe_operator.change_fallback_handler(new_fallback_handler))
        self.assertEqual(safe_operator.safe_cli_info.fallback_handler,
                         new_fallback_handler)
        self.assertEqual(safe.retrieve_fallback_handler(),
                         new_fallback_handler)

        safe_operator.change_master_copy(self.safe_old_contract_address)
        with self.assertRaises(FallbackHandlerNotSupportedException):
            safe_operator.change_fallback_handler(Account.create().address)
 def get_safe_info_from_blockchain(
         self, safe_address: ChecksumAddress) -> SafeInfo:
     """
     :param safe_address:
     :return: SafeInfo from blockchain
     """
     try:
         safe = Safe(safe_address, self.ethereum_client)
         safe_info = safe.retrieve_all_info()
         # Return same master copy information than the db method
         return replace(
             safe_info,
             version=SafeMasterCopy.objects.get_version_for_address(
                 safe_info.master_copy),
         )
     except IOError as exc:
         raise NodeConnectionException from exc
     except CannotRetrieveSafeInfoException as exc:
         raise CannotGetSafeInfoFromBlockchain(safe_address) from exc
 def retrieve_safe_info(self, address: str) -> SafeInfo:
     safe = Safe(address, self.ethereum_client)
     if not self.ethereum_client.is_contract(address):
         raise SafeNotDeployed('Safe with address=%s not deployed' %
                               address)
     nonce = safe.retrieve_nonce()
     threshold = safe.retrieve_threshold()
     owners = safe.retrieve_owners()
     master_copy = safe.retrieve_master_copy_address()
     version = safe.retrieve_version()
     fallback_handler = safe.retrieve_fallback_handler()
     return SafeInfo(address, nonce, threshold, owners, master_copy,
                     version, fallback_handler)
Example #27
0
    def test_add_owner(self):
        safe_address = self.deploy_test_safe(
            owners=[self.ethereum_test_account.address]).safe_address
        safe_operator = SafeOperator(safe_address, self.ethereum_node_url)
        with self.assertRaises(ExistingOwnerException):
            safe_operator.add_owner(self.ethereum_test_account.address)

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

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

        safe_operator.accounts.add(self.ethereum_test_account)
        safe = Safe(safe_address, self.ethereum_client)
        self.assertTrue(safe_operator.add_owner(new_owner))
        self.assertIn(self.ethereum_test_account, safe_operator.accounts)
        self.assertIn(new_owner, safe.retrieve_owners())
 def save(self, **kwargs):
     safe_address = self.context["safe_address"]
     ethereum_client = EthereumClientProvider()
     safe = Safe(safe_address, ethereum_client)
     exc = None
     # Retry thrice to get an estimation
     for _ in range(3):
         try:
             safe_tx_gas = safe.estimate_tx_gas(
                 self.validated_data["to"],
                 self.validated_data["value"],
                 self.validated_data["data"],
                 self.validated_data["operation"],
             )
             return {"safe_tx_gas": safe_tx_gas}
         except (IOError, ValueError) as _exc:
             exc = _exc
     raise NodeConnectionException(
         f"Node connection error when estimating gas for Safe {safe_address}"
     ) from exc
    def test_change_guard(self):
        safe_operator = self.setup_operator(version="1.1.1")
        with self.assertRaises(GuardNotSupportedException):
            safe_operator.change_guard(Account.create().address)

        safe_operator = self.setup_operator(version="1.3.0")
        safe = Safe(safe_operator.address, self.ethereum_client)
        current_guard = safe.retrieve_guard()
        with self.assertRaises(SameGuardException):
            safe_operator.change_guard(current_guard)

        new_guard = Account.create().address
        with self.assertRaises(InvalidGuardException):  # Contract does not exist
            self.assertTrue(safe_operator.change_guard(new_guard))

        with mock.patch.object(
            EthereumClient, "is_contract", autospec=True, return_value=True
        ):
            self.assertTrue(safe_operator.change_guard(new_guard))
        self.assertEqual(safe_operator.safe_cli_info.guard, new_guard)
        self.assertEqual(safe.retrieve_guard(), new_guard)
    def estimate_tx_for_all_tokens(self, safe_address: str, to: str, value: int, data: bytes,
                                   operation: int) -> TransactionEstimationWithNonceAndGasTokens:
        """
        :return: TransactionEstimation with costs using ether and every gas token supported by the service,
        with the last used nonce of the Safe
        :raises: InvalidGasToken: If Gas Token is not valid
        """
        safe = Safe(safe_address, self.ethereum_client)
        last_used_nonce = self.get_last_used_nonce(safe_address)
        safe_tx_gas = safe.estimate_tx_gas(to, value, data, operation)

        safe_version = safe.retrieve_version()
        if Version(safe_version) >= Version('1.0.0'):
            safe_tx_operational_gas = 0
        else:
            safe_tx_operational_gas = safe.estimate_tx_operational_gas(len(data) if data else 0)

        # Calculate `base_gas` for ether and calculate for tokens using the ether token price
        ether_safe_tx_base_gas = safe.estimate_tx_base_gas(to, value, data, operation, NULL_ADDRESS, safe_tx_gas)
        base_gas_price = self._get_configured_gas_price()
        gas_price = self._estimate_tx_gas_price(base_gas_price, NULL_ADDRESS)
        gas_token_estimations = [TransactionGasTokenEstimation(ether_safe_tx_base_gas, gas_price, NULL_ADDRESS)]
        token_gas_difference = 50000  # 50K gas more expensive than ether
        for token in Token.objects.gas_tokens():
            try:
                gas_price = self._estimate_tx_gas_price(base_gas_price, token.address)
                gas_token_estimations.append(
                    TransactionGasTokenEstimation(ether_safe_tx_base_gas + token_gas_difference,
                                                  gas_price, token.address)
                )
            except CannotGetTokenPriceFromApi:
                logger.error('Cannot get price for token=%s', token.address)

        return TransactionEstimationWithNonceAndGasTokens(last_used_nonce, safe_tx_gas, safe_tx_operational_gas,
                                                          gas_token_estimations)