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)
    def estimate_tx(self, safe_address: str, to: str, value: int, data: bytes, operation: int,
                    gas_token: Optional[str]) -> TransactionEstimationWithNonce:
        """
        :return: TransactionEstimation with costs using the provided gas token and last used nonce of the Safe
        :raises: InvalidGasToken: If Gas Token is not valid
        """
        if not self._is_valid_gas_token(gas_token):
            raise InvalidGasToken(gas_token)

        last_used_nonce = self.get_last_used_nonce(safe_address)
        safe = Safe(safe_address, self.ethereum_client)
        safe_tx_gas = safe.estimate_tx_gas(to, value, data, operation)
        safe_tx_base_gas = safe.estimate_tx_base_gas(to, value, data, operation, gas_token, safe_tx_gas)

        # For Safe contracts v1.0.0 operational gas is not used (`base_gas` has all the related costs already)
        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)

        # Can throw RelayServiceException
        gas_price = self._estimate_tx_gas_price(self._get_configured_gas_price(), gas_token)
        return TransactionEstimationWithNonce(safe_tx_gas, safe_tx_base_gas, safe_tx_base_gas, safe_tx_operational_gas,
                                              gas_price, gas_token or NULL_ADDRESS, last_used_nonce,
                                              self.tx_sender_account.address)
Ejemplo n.º 3
0
    def estimate_tx_for_all_tokens(self, safe_address: str, to: str, value: int, data: str,
                                   operation: int) -> TransactionEstimationWithNonceAndGasTokens:
        last_used_nonce = SafeMultisigTx.objects.get_last_nonce_for_safe(safe_address)
        safe = Safe(safe_address, self.ethereum_client)
        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)
        gas_price = self._estimate_tx_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(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)
    def _send_multisig_tx(self,
                          safe_address: str,
                          to: str,
                          value: int,
                          data: bytes,
                          operation: int,
                          safe_tx_gas: int,
                          base_gas: int,
                          gas_price: int,
                          gas_token: str,
                          refund_receiver: str,
                          safe_nonce: int,
                          signatures: bytes,
                          block_identifier='latest') -> Tuple[bytes, bytes, Dict[str, Any]]:
        """
        This function calls the `send_multisig_tx` of the Safe, but has some limitations to prevent abusing
        the relay
        :return: Tuple(tx_hash, safe_tx_hash, tx)
        :raises: InvalidMultisigTx: If user tx cannot go through the Safe
        """

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

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

        self._check_safe_gas_price(gas_token, gas_price)

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

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

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

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

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

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

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

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

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

        if banned_signers := BannedSigner.objects.filter(address__in=signers):
            raise SignerIsBanned(f'Signers {list(banned_signers)} are banned')
Ejemplo n.º 5
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
Ejemplo n.º 6
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