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 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 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)
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 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 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 _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 _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
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