def __init__(self, gas_station: GasStation, ethereum_client: EthereumClient, redis: Redis, safe_valid_contract_addresses: Set[str], proxy_factory_address: str, tx_sender_private_key: str): self.gas_station = gas_station self.ethereum_client = ethereum_client self.redis = redis self.safe_valid_contract_addresses = safe_valid_contract_addresses self.proxy_factory = ProxyFactory(proxy_factory_address, self.ethereum_client) self.tx_sender_account = Account.from_key(tx_sender_private_key)
def deploy_create2_safe_tx(self, safe_address: str) -> SafeCreation2: """ Deploys safe if SafeCreation2 exists. :param safe_address: :return: tx_hash """ safe_creation2: SafeCreation2 = SafeCreation2.objects.get( safe=safe_address) if safe_creation2.tx_hash: logger.info('Safe=%s has already been deployed with tx-hash=%s', safe_address, safe_creation2.tx_hash) return safe_creation2 if safe_creation2.payment_token and safe_creation2.payment_token != NULL_ADDRESS: safe_balance = self.ethereum_client.erc20.get_balance( safe_address, safe_creation2.payment_token) else: safe_balance = self.ethereum_client.get_balance(safe_address) if safe_balance < safe_creation2.payment: message = 'Balance=%d for safe=%s with payment-token=%s. Not found ' \ 'required=%d' % (safe_balance, safe_address, safe_creation2.payment_token, safe_creation2.payment) logger.info(message) raise NotEnoughFundingForCreation(message) logger.info( 'Found %d balance for safe=%s with payment-token=%s. Required=%d', safe_balance, safe_address, safe_creation2.payment_token, safe_creation2.payment) setup_data = HexBytes(safe_creation2.setup_data.tobytes()) with EthereumNonceLock(self.redis, self.ethereum_client, self.funder_account.address, timeout=60 * 2) as tx_nonce: proxy_factory = ProxyFactory(safe_creation2.proxy_factory, self.ethereum_client) ethereum_tx_sent = proxy_factory.deploy_proxy_contract_with_nonce( self.funder_account, safe_creation2.master_copy, setup_data, safe_creation2.salt_nonce, gas=safe_creation2.gas_estimated, gas_price=safe_creation2.gas_price_estimated, nonce=tx_nonce) EthereumTx.objects.create_from_tx(ethereum_tx_sent.tx, ethereum_tx_sent.tx_hash) safe_creation2.tx_hash = ethereum_tx_sent.tx_hash safe_creation2.save() logger.info('Deployed safe=%s with tx-hash=%s', safe_address, ethereum_tx_sent.tx_hash.hex()) return safe_creation2
def deploy_again_create2_safe_tx(self, safe_address: str) -> SafeCreation2: """ Try to deploy Safe again with a higher gas price :param safe_address: :return: tx_hash """ safe_creation2: SafeCreation2 = SafeCreation2.objects.get( safe=safe_address) if not safe_creation2.tx_hash: message = f"Safe={safe_address} deploy transaction does not exist" logger.info(message) raise DeployTransactionDoesNotExist(message) if safe_creation2.block_number is not None: message = ( f"Safe={safe_address} has already been deployed with tx-hash={safe_creation2.tx_hash} " f"on block-number={safe_creation2.block_number}") logger.info(message) raise SafeAlreadyExistsException(message) ethereum_tx: EthereumTx = EthereumTx.objects.get( tx_hash=safe_creation2.tx_hash) assert ethereum_tx, "Ethereum tx cannot be missing" self._check_safe_balance(safe_creation2) setup_data = HexBytes(safe_creation2.setup_data.tobytes()) proxy_factory = ProxyFactory(safe_creation2.proxy_factory, self.ethereum_client) # Increase gas price a little gas_price = math.ceil( max(self.gas_station.get_gas_prices().fast, ethereum_tx.gas_price) * 1.1) ethereum_tx_sent = proxy_factory.deploy_proxy_contract_with_nonce( self.funder_account, safe_creation2.master_copy, setup_data, safe_creation2.salt_nonce, gas=safe_creation2.gas_estimated + 50000, # Just in case gas_price=gas_price, nonce=ethereum_tx.nonce, ) # Replace old transaction EthereumTx.objects.create_from_tx_dict(ethereum_tx_sent.tx, ethereum_tx_sent.tx_hash) safe_creation2.tx_hash = ethereum_tx_sent.tx_hash.hex() safe_creation2.save(update_fields=["tx_hash"]) logger.info( "Send again transaction to deploy Safe=%s with tx-hash=%s", safe_address, safe_creation2.tx_hash, ) return safe_creation2
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 handle(self, *args, **options): ethereum_client = EthereumClientProvider() proxy_factory_address = settings.SAFE_PROXY_FACTORY_ADDRESS deployer_key = options['deployer_key'] deployer_account = Account.privateKeyToAccount( deployer_key) if deployer_key else self.DEFAULT_ACCOUNT self.stdout.write( self.style.SUCCESS( 'Checking if proxy factory was already deployed on %s' % proxy_factory_address)) if ethereum_client.is_contract(proxy_factory_address): self.stdout.write( self.style.NOTICE('Proxy factory was already deployed on %s' % proxy_factory_address)) else: self.stdout.write( self.style.SUCCESS( 'Deploying proxy factory using deployer account, ' 'proxy factory %s not found' % proxy_factory_address)) proxy_factory_address = ProxyFactory.deploy_proxy_factory_contract( ethereum_client, deployer_account=deployer_account).contract_address self.stdout.write( self.style.SUCCESS('Proxy factory has been deployed on %s' % proxy_factory_address))
def deploy_create2_safe_tx(self, safe_address: str) -> SafeCreation2: """ Deploys safe if SafeCreation2 exists. :param safe_address: :return: tx_hash """ safe_creation2: SafeCreation2 = SafeCreation2.objects.get( safe=safe_address) if safe_creation2.tx_hash: logger.info( "Safe=%s has already been deployed with tx-hash=%s", safe_address, safe_creation2.tx_hash, ) return safe_creation2 self._check_safe_balance(safe_creation2) setup_data = HexBytes(safe_creation2.setup_data.tobytes()) proxy_factory = ProxyFactory(safe_creation2.proxy_factory, self.ethereum_client) with EthereumNonceLock( self.redis, self.ethereum_client, self.funder_account.address, lock_timeout=60 * 2, ) as tx_nonce: ethereum_tx_sent = proxy_factory.deploy_proxy_contract_with_nonce( self.funder_account, safe_creation2.master_copy, setup_data, safe_creation2.salt_nonce, gas=safe_creation2.gas_estimated + 50000, # Just in case gas_price=safe_creation2.gas_price_estimated, nonce=tx_nonce, ) EthereumTx.objects.create_from_tx_dict(ethereum_tx_sent.tx, ethereum_tx_sent.tx_hash) safe_creation2.tx_hash = ethereum_tx_sent.tx_hash safe_creation2.save(update_fields=["tx_hash"]) logger.info( "Send transaction to deploy Safe=%s with tx-hash=%s", safe_address, ethereum_tx_sent.tx_hash.hex(), ) return safe_creation2
def __init__(self, gas_station: GasStation, ethereum_client: EthereumClient, redis: Redis, safe_contract_address: str, safe_old_contract_address: str, proxy_factory_address: str, safe_funder_private_key: str, safe_fixed_creation_cost: int, safe_auto_fund: bool, safe_auto_approve_token: bool): self.gas_station = gas_station self.ethereum_client = ethereum_client self.redis = redis self.safe_contract_address = safe_contract_address self.safe_old_contract_address = safe_old_contract_address self.proxy_factory = ProxyFactory(proxy_factory_address, self.ethereum_client) self.funder_account = Account.privateKeyToAccount( safe_funder_private_key) self.safe_fixed_creation_cost = safe_fixed_creation_cost self.safe_auto_fund = safe_auto_fund self.safe_auto_approve_token = safe_auto_approve_token
def __init__(self, gas_station: GasStation, ethereum_client: EthereumClient, redis: Redis, safe_contract_address: str, proxy_factory_address: str, default_callback_handler: str, safe_funder_private_key: str, safe_fixed_creation_cost: int): self.gas_station = gas_station self.ethereum_client = ethereum_client self.redis = redis self.safe_contract_address = safe_contract_address self.proxy_factory = ProxyFactory(proxy_factory_address, self.ethereum_client) self.default_callback_handler = default_callback_handler self.funder_account = Account.from_key(safe_funder_private_key) self.safe_fixed_creation_cost = safe_fixed_creation_cost
class TransactionService: def __init__(self, gas_station: GasStation, ethereum_client: EthereumClient, redis: Redis, safe_valid_contract_addresses: Set[str], proxy_factory_address: str, tx_sender_private_key: str): self.gas_station = gas_station self.ethereum_client = ethereum_client self.redis = redis self.safe_valid_contract_addresses = safe_valid_contract_addresses self.proxy_factory = ProxyFactory(proxy_factory_address, self.ethereum_client) self.tx_sender_account = Account.from_key(tx_sender_private_key) def _check_refund_receiver(self, refund_receiver: str) -> bool: """ Support tx.origin or relay tx sender as refund receiver. This would prevent that anybody can front-run our service :param refund_receiver: Payment refund receiver as Ethereum checksummed address :return: True if refund_receiver is ok, False otherwise """ return refund_receiver in (NULL_ADDRESS, self.tx_sender_account.address) @staticmethod def _is_valid_gas_token(address: Optional[str]) -> float: """ :param address: Token address :return: bool if gas token, false otherwise """ address = address or NULL_ADDRESS if address == NULL_ADDRESS: return True try: Token.objects.get(address=address, gas=True) return True except Token.DoesNotExist: logger.warning('Cannot retrieve gas token from db: Gas token %s not valid', address) return False def _check_safe_gas_price(self, gas_token: Optional[str], safe_gas_price: int) -> bool: """ Check that `safe_gas_price` is not too low, so that the relay gets a full refund for the tx. Gas_price must be always > 0, if not refunding would be disabled If a `gas_token` is used we need to calculate the `gas_price` in Eth Gas price must be at least >= _minimum_gas_price_ > 0 :param gas_token: Address of token is used, `NULL_ADDRESS` or `None` if it's ETH :return: :exception GasPriceTooLow :exception InvalidGasToken """ if safe_gas_price < 1: raise RefundMustBeEnabled('Tx internal gas price cannot be 0 or less, it was %d' % safe_gas_price) minimum_accepted_gas_price = self._get_minimum_gas_price() estimated_gas_price = self._estimate_tx_gas_price(minimum_accepted_gas_price, gas_token) if safe_gas_price < estimated_gas_price: raise GasPriceTooLow('Required gas-price>=%d with gas-token=%s' % (estimated_gas_price, gas_token)) return True def _estimate_tx_gas_price(self, base_gas_price: int, gas_token: Optional[str] = None) -> int: if gas_token and gas_token != NULL_ADDRESS: try: gas_token_model = Token.objects.get(address=gas_token, gas=True) estimated_gas_price = gas_token_model.calculate_gas_price(base_gas_price) except Token.DoesNotExist: raise InvalidGasToken('Gas token %s not found' % gas_token) else: estimated_gas_price = base_gas_price # FIXME Remove 2 / 3, workaround to prevent frontrunning return int(estimated_gas_price * 2 / 3) def _get_configured_gas_price(self) -> int: """ :return: Gas price for txs """ return self.gas_station.get_gas_prices().fast def _get_minimum_gas_price(self) -> int: """ :return: Minimum gas price accepted for txs set by the user """ return self.gas_station.get_gas_prices().standard 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 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: 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 create_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: List[Dict[str, int]]) -> SafeMultisigTx: """ :return: Database model of SafeMultisigTx :raises: SafeMultisigTxExists: If Safe Multisig Tx with nonce already exists :raises: InvalidGasToken: If Gas Token is not valid :raises: TransactionServiceException: If Safe Tx is not valid (not sorted owners, bad signature, bad nonce...) """ safe_contract, _ = SafeContract.objects.get_or_create(address=safe_address, defaults={'master_copy': NULL_ADDRESS}) created = timezone.now() if SafeMultisigTx.objects.not_failed().filter(safe=safe_contract, nonce=safe_nonce).exists(): raise SafeMultisigTxExists(f'Tx with safe-nonce={safe_nonce} for safe={safe_address} already exists in DB') signature_pairs = [(s['v'], s['r'], s['s']) for s in signatures] signatures_packed = signatures_to_bytes(signature_pairs) try: tx_hash, safe_tx_hash, tx = self._send_multisig_tx( safe_address, to, value, data, operation, safe_tx_gas, base_gas, gas_price, gas_token, refund_receiver, safe_nonce, signatures_packed ) except SafeServiceException as exc: raise TransactionServiceException(str(exc)) from exc ethereum_tx = EthereumTx.objects.create_from_tx_dict(tx, tx_hash) try: return SafeMultisigTx.objects.create( created=created, safe=safe_contract, ethereum_tx=ethereum_tx, to=to, value=value, data=data, operation=operation, safe_tx_gas=safe_tx_gas, data_gas=base_gas, gas_price=gas_price, gas_token=None if gas_token == NULL_ADDRESS else gas_token, refund_receiver=refund_receiver, nonce=safe_nonce, signatures=signatures_packed, safe_tx_hash=safe_tx_hash, ) except IntegrityError as exc: raise SafeMultisigTxExists(f'Tx with safe_tx_hash={safe_tx_hash.hex()} already exists in DB') 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') 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.recommended_gas() safe_tx.call(tx_sender_address=tx_sender_address, tx_gas=tx_gas, block_identifier=block_identifier) with EthereumNonceLock(self.redis, self.ethereum_client, self.tx_sender_account.address, lock_timeout=60 * 2) as tx_nonce: logger.info('Safe=%s safe-nonce=%d `call()` was successful', 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) 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
f'Sender {account.address} - Balance: {ether_account_balance}Ξ') if not ethereum_client.w3.eth.getCode(safe_contract_address) \ or not ethereum_client.w3.eth.getCode(proxy_factory_address): print_formatted_text('Network not supported') sys.exit(1) salt_nonce = secrets.SystemRandom().randint(0, 2**256 - 1) # TODO Add support for CPK print_formatted_text( f'Creating new Safe with owners={owners} threshold={threshold} and sat-nonce={salt_nonce}' ) gas_price = 0 safe_creation_tx = Safe.build_safe_create2_tx( ethereum_client, safe_contract_address, proxy_factory_address, salt_nonce, owners, threshold, gas_price, fallback_handler=callback_handler_address, payment_token=None) proxy_factory = ProxyFactory(proxy_factory_address, ethereum_client) ethereum_tx_sent = proxy_factory.deploy_proxy_contract_with_nonce( account, safe_contract_address, safe_creation_tx.safe_setup_data, safe_creation_tx.salt_nonce) print_formatted_text( f'Tx with tx-hash={ethereum_tx_sent.tx_hash.hex()} ' f'will create safe={ethereum_tx_sent.contract_address}')
class TransactionService: def __init__(self, gas_station: GasStation, ethereum_client: EthereumClient, redis: Redis, safe_valid_contract_addresses: Set[str], proxy_factory_address: str, tx_sender_private_key: str): self.gas_station = gas_station self.ethereum_client = ethereum_client self.redis = redis self.safe_valid_contract_addresses = safe_valid_contract_addresses self.proxy_factory = ProxyFactory(proxy_factory_address, self.ethereum_client) self.tx_sender_account = Account.privateKeyToAccount( tx_sender_private_key) @staticmethod def _check_refund_receiver(refund_receiver: str) -> bool: """ We only support tx.origin as refund receiver right now In the future we can also accept transactions where it is set to our service account to receive the payments. This would prevent that anybody can front-run our service """ return refund_receiver == NULL_ADDRESS @staticmethod def _is_valid_gas_token(address: Optional[str]) -> float: """ :param address: Token address :return: bool if gas token, false otherwise """ address = address or NULL_ADDRESS if address == NULL_ADDRESS: return True try: Token.objects.get(address=address, gas=True) return True except Token.DoesNotExist: logger.warning( 'Cannot retrieve gas token from db: Gas token %s not valid', address) return False def _check_safe_gas_price(self, gas_token: Optional[str], safe_gas_price: int) -> bool: """ Check that `safe_gas_price` is not too low, so that the relay gets a full refund for the tx. Gas_price must be always > 0, if not refunding would be disabled If a `gas_token` is used we need to calculate the `gas_price` in Eth Gas price must be at least >= _current standard gas price_ > 0 :param gas_token: Address of token is used, `NULL_ADDRESS` or `None` if it's ETH :return: :exception GasPriceTooLow :exception InvalidGasToken """ if safe_gas_price < 1: raise RefundMustBeEnabled( 'Tx internal gas price cannot be 0 or less, it was %d' % safe_gas_price) minimum_accepted_gas_price = self._get_minimum_gas_price() if gas_token and gas_token != NULL_ADDRESS: try: gas_token_model = Token.objects.get(address=gas_token, gas=True) estimated_gas_price = gas_token_model.calculate_gas_price( minimum_accepted_gas_price) if safe_gas_price < estimated_gas_price: raise GasPriceTooLow( 'Required gas-price>=%d to use gas-token' % estimated_gas_price) # We use gas station tx gas price. We cannot use internal tx's because is calculated # based on the gas token except Token.DoesNotExist: logger.warning( 'Cannot retrieve gas token from db: Gas token %s not valid', gas_token) raise InvalidGasToken('Gas token %s not valid' % gas_token) else: if safe_gas_price < minimum_accepted_gas_price: raise GasPriceTooLow('Required gas-price>=%d' % minimum_accepted_gas_price) return True def _estimate_tx_gas_price(self, gas_token: Optional[str] = None): gas_price_fast = self._get_configured_gas_price() if gas_token and gas_token != NULL_ADDRESS: try: gas_token_model = Token.objects.get(address=gas_token, gas=True) return gas_token_model.calculate_gas_price(gas_price_fast) except Token.DoesNotExist: raise InvalidGasToken('Gas token %s not found' % gas_token) else: return gas_price_fast def _get_configured_gas_price(self) -> int: """ :return: Gas price for txs """ return self.gas_station.get_gas_prices().standard def _get_minimum_gas_price(self) -> int: """ :return: Minimum gas price accepted for txs set by the user """ return self.gas_station.get_gas_prices().safe_low def estimate_tx( self, safe_address: str, to: str, value: int, data: str, operation: int, gas_token: Optional[str]) -> TransactionEstimationWithNonce: """ :return: TransactionEstimation with costs and last used nonce of 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 = 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_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(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) 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 create_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, nonce: int, signatures: List[Dict[str, int]]) -> SafeMultisigTx: """ :return: Database model of SafeMultisigTx :raises: SafeMultisigTxExists: If Safe Multisig Tx with nonce already exists :raises: InvalidGasToken: If Gas Token is not valid :raises: TransactionServiceException: If Safe Tx is not valid (not sorted owners, bad signature, bad nonce...) """ safe_contract = SafeContract.objects.get(address=safe_address) created = timezone.now() if SafeMultisigTx.objects.filter(safe=safe_contract, nonce=nonce).exists(): raise SafeMultisigTxExists( f'Tx with nonce={nonce} for safe={safe_address} already exists in DB' ) signature_pairs = [(s['v'], s['r'], s['s']) for s in signatures] signatures_packed = signatures_to_bytes(signature_pairs) try: tx_hash, safe_tx_hash, tx = self._send_multisig_tx( safe_address, to, value, data, operation, safe_tx_gas, base_gas, gas_price, gas_token, refund_receiver, nonce, signatures_packed) except SafeServiceException as exc: raise TransactionServiceException(str(exc)) from exc ethereum_tx = EthereumTx.objects.create_from_tx(tx, tx_hash) # Fix race conditions for tx being created at the same time try: return SafeMultisigTx.objects.create( created=created, safe=safe_contract, ethereum_tx=ethereum_tx, to=to, value=value, data=data, operation=operation, safe_tx_gas=safe_tx_gas, data_gas=base_gas, gas_price=gas_price, gas_token=None if gas_token == NULL_ADDRESS else gas_token, refund_receiver=refund_receiver, nonce=nonce, signatures=signatures_packed, safe_tx_hash=safe_tx_hash, ) except IntegrityError as exc: raise SafeMultisigTxExists( f'Tx with nonce={nonce} for safe={safe_address} already exists in DB' ) 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, 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)) # 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.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.hex(), safe_tx.signers)) safe_tx.call(tx_sender_address=tx_sender_address, block_identifier=block_identifier) with EthereumNonceLock(self.redis, self.ethereum_client, self.tx_sender_account.address, 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) return tx_hash, safe_tx.tx_hash, tx def get_pending_multisig_transactions( self, older_than: int) -> List[SafeMultisigTx]: """ Get multisig txs that have not been mined after X seconds :param older_than: Time in seconds for a tx to be considered pending, if 0 all will be returned """ return SafeMultisigTx.objects.filter( Q(ethereum_tx__block=None) | Q(ethereum_tx=None)).filter( created__lte=timezone.now() - timedelta(seconds=older_than), )
def main(*args, **kwargs): parser = setup_argument_parser() print_formatted_text( pyfiglet.figlet_format("Gnosis Safe Creator")) # Print fancy text args = parser.parse_args() node_url: URI = args.node_url account: LocalAccount = Account.from_key(args.private_key) owners: List[str] = args.owners if args.owners else [account.address] threshold: int = args.threshold salt_nonce: int = args.salt_nonce to = NULL_ADDRESS data = b"" payment_token = NULL_ADDRESS payment = 0 payment_receiver = NULL_ADDRESS if len(owners) < threshold: print_formatted_text( "Threshold cannot be bigger than the number of unique owners") sys.exit(1) safe_contract_address = args.safe_contract or ( LAST_SAFE_L2_CONTRACT if args.l2 else LAST_SAFE_CONTRACT) proxy_factory_address = args.proxy_factory fallback_handler = args.callback_handler ethereum_client = EthereumClient(node_url) ethereum_network = ethereum_client.get_network() if not ethereum_client.is_contract(safe_contract_address): print_formatted_text( f"Safe contract address {safe_contract_address} " f"does not exist on network {ethereum_network.name}") sys.exit(1) elif not ethereum_client.is_contract(proxy_factory_address): print_formatted_text( f"Proxy contract address {proxy_factory_address} " f"does not exist on network {ethereum_network.name}") sys.exit(1) elif fallback_handler != NULL_ADDRESS and not ethereum_client.is_contract( fallback_handler): print_formatted_text( f"Fallback handler address {fallback_handler} " f"does not exist on network {ethereum_network.name}") sys.exit(1) account_balance: int = ethereum_client.get_balance(account.address) if not account_balance: print_formatted_text( "Client does not have any funds. Let's try anyway in case it's a network without gas costs" ) else: ether_account_balance = round( ethereum_client.w3.fromWei(account_balance, "ether"), 6) print_formatted_text( f"Network {ethereum_client.get_network().name} - Sender {account.address} - " f"Balance: {ether_account_balance}Ξ") if not ethereum_client.w3.eth.getCode( safe_contract_address) or not ethereum_client.w3.eth.getCode( proxy_factory_address): print_formatted_text("Network not supported") sys.exit(1) print_formatted_text( f"Creating new Safe with owners={owners} threshold={threshold} salt-nonce={salt_nonce}" ) print_formatted_text( f"Proxy factory={proxy_factory_address} safe-master-copy={safe_contract_address} and " f"fallback-handler={fallback_handler}") if yes_or_no_question("Do you want to continue?"): safe_contract = get_safe_V1_3_0_contract(ethereum_client.w3, safe_contract_address) safe_creation_tx_data = HexBytes( safe_contract.functions.setup( owners, threshold, to, data, fallback_handler, payment_token, payment, payment_receiver, ).buildTransaction({ "gas": 1, "gasPrice": 1 })["data"]) proxy_factory = ProxyFactory(proxy_factory_address, ethereum_client) ethereum_tx_sent = proxy_factory.deploy_proxy_contract_with_nonce( account, safe_contract_address, safe_creation_tx_data, salt_nonce) print_formatted_text( f"Tx with tx-hash={ethereum_tx_sent.tx_hash.hex()} " f"will create safe={ethereum_tx_sent.contract_address}") print_formatted_text(f"Tx paramters={ethereum_tx_sent.tx}")
class SafeCreationService: def __init__(self, gas_station: GasStation, ethereum_client: EthereumClient, redis: Redis, safe_contract_address: str, safe_old_contract_address: str, proxy_factory_address: str, safe_funder_private_key: str, safe_fixed_creation_cost: int, safe_auto_fund: bool, safe_auto_approve_token: bool): self.gas_station = gas_station self.ethereum_client = ethereum_client self.redis = redis self.safe_contract_address = safe_contract_address self.safe_old_contract_address = safe_old_contract_address self.proxy_factory = ProxyFactory(proxy_factory_address, self.ethereum_client) self.funder_account = Account.privateKeyToAccount( safe_funder_private_key) self.safe_fixed_creation_cost = safe_fixed_creation_cost self.safe_auto_fund = safe_auto_fund self.safe_auto_approve_token = safe_auto_approve_token def _get_token_eth_value_or_raise(self, address: str) -> float: """ :param address: Token address :return: Current eth value of the token :raises: InvalidPaymentToken, CannotGetTokenPriceFromApi """ address = address or NULL_ADDRESS if address == NULL_ADDRESS: return 1.0 try: token = Token.objects.get(address=address, gas=True) except Token.DoesNotExist: if self.safe_auto_approve_token: # Add the token for development purposes. token = Token.objects.create(address=address, name="Cash", symbol="cash", decimals=2, fixed_eth_conversion=0.006, gas=True) else: raise return token.get_eth_value() def _get_configured_gas_price(self) -> int: """ :return: Gas price for txs """ return self.gas_station.get_gas_prices().fast def create_safe_tx(self, s: int, owners: List[str], threshold: int, payment_token: Optional[str]) -> SafeCreation: """ Prepare creation tx for a new safe using classic CREATE method. Deprecated, it's recommended to use `create2_safe_tx` :param s: Random s value for ecdsa signature :param owners: Owners of the new Safe :param threshold: Minimum number of users required to operate the Safe :param payment_token: Address of the payment token, if ether is not used :rtype: SafeCreation :raises: InvalidPaymentToken """ payment_token = payment_token or NULL_ADDRESS payment_token_eth_value = self._get_token_eth_value_or_raise( payment_token) gas_price: int = self._get_configured_gas_price() current_block_number = self.ethereum_client.current_block_number logger.debug('Building safe creation tx with gas price %d' % gas_price) safe_creation_tx = Safe.build_safe_creation_tx( self.ethereum_client, self.safe_old_contract_address, s, owners, threshold, gas_price, payment_token, self.funder_account.address, payment_token_eth_value=payment_token_eth_value, fixed_creation_cost=self.safe_fixed_creation_cost) safe_contract = SafeContract.objects.create( address=safe_creation_tx.safe_address, master_copy=safe_creation_tx.master_copy) # Enable tx and erc20 tracing SafeTxStatus.objects.create(safe=safe_contract, initial_block_number=current_block_number, tx_block_number=current_block_number, erc_20_block_number=current_block_number) return SafeCreation.objects.create( deployer=safe_creation_tx.deployer_address, safe=safe_contract, master_copy=safe_creation_tx.master_copy, funder=safe_creation_tx.funder, owners=owners, threshold=threshold, payment=safe_creation_tx.payment, tx_hash=safe_creation_tx.tx_hash.hex(), gas=safe_creation_tx.gas, gas_price=safe_creation_tx.gas_price, payment_token=None if safe_creation_tx.payment_token == NULL_ADDRESS else safe_creation_tx.payment_token, value=safe_creation_tx.tx_pyethereum.value, v=safe_creation_tx.v, r=safe_creation_tx.r, s=safe_creation_tx.s, data=safe_creation_tx.tx_pyethereum.data, signed_tx=safe_creation_tx.tx_raw) def create2_safe_tx(self, salt_nonce: int, owners: Iterable[str], threshold: int, payment_token: Optional[str], setup_data: Optional[str], to: Optional[str], callback: Optional[str]) -> SafeCreation2: """ Prepare creation tx for a new safe using CREATE2 method :param salt_nonce: Random value for solidity `create2` salt :param owners: Owners of the new Safe :param threshold: Minimum number of users required to operate the Safe :param payment_token: Address of the payment token, otherwise `ether` is used :param setup_data: Data used for safe creation delegate call. :rtype: SafeCreation2 :raises: InvalidPaymentToken """ callback = callback or NULL_ADDRESS payment_token = payment_token or NULL_ADDRESS payment_token_eth_value = self._get_token_eth_value_or_raise( payment_token) gas_price: int = self._get_configured_gas_price() current_block_number = self.ethereum_client.current_block_number logger.debug('Building safe create2 tx with gas price %d', gas_price) safe_creation_tx = Safe.build_safe_create2_tx( self.ethereum_client, self.safe_contract_address, self.proxy_factory.address, salt_nonce, owners, threshold, gas_price, payment_token, payment_token_eth_value=payment_token_eth_value, fixed_creation_cost=self.safe_fixed_creation_cost, setup_data=HexBytes(setup_data if setup_data else '0x'), to=to, callback=callback) safe_contract, created = SafeContract.objects.get_or_create( address=safe_creation_tx.safe_address, defaults={'master_copy': safe_creation_tx.master_copy_address}) if not created: raise SafeAlreadyExistsException( f'Safe={safe_contract.address} cannot be created, already exists' ) # Enable tx and erc20 tracing SafeTxStatus.objects.create(safe=safe_contract, initial_block_number=current_block_number, tx_block_number=current_block_number, erc_20_block_number=current_block_number) return SafeCreation2.objects.create( safe=safe_contract, master_copy=safe_creation_tx.master_copy_address, proxy_factory=safe_creation_tx.proxy_factory_address, salt_nonce=salt_nonce, owners=owners, threshold=threshold, to=to, # Contract address for optional delegate call # data # Data payload for optional delegate call payment_token=None if safe_creation_tx.payment_token == NULL_ADDRESS else safe_creation_tx.payment_token, payment=safe_creation_tx.payment, payment_receiver=safe_creation_tx.payment_receiver, setup_data=safe_creation_tx.safe_setup_data, gas_estimated=safe_creation_tx.gas, gas_price_estimated=safe_creation_tx.gas_price, callback=callback, ) def deploy_create2_safe_tx(self, safe_address: str) -> SafeCreation2: """ Deploys safe if SafeCreation2 exists. :param safe_address: :return: tx_hash """ safe_creation2 = SafeCreation2.objects.get(safe=safe_address) if safe_creation2.tx_hash: logger.info('Safe=%s has already been deployed with tx-hash=%s', safe_address, safe_creation2.tx_hash) return safe_creation2 if safe_creation2.payment_token and safe_creation2.payment_token != NULL_ADDRESS: safe_balance = self.ethereum_client.erc20.get_balance( safe_address, safe_creation2.payment_token) else: safe_balance = self.ethereum_client.get_balance(safe_address) if safe_balance < safe_creation2.payment: message = 'Balance=%d for safe=%s with payment-token=%s. Not found ' \ 'required=%d\n' % (safe_balance, safe_address, safe_creation2.payment_token, safe_creation2.payment) # Be sure we are actually using an erc20. if self.safe_auto_fund and safe_creation2.payment_token and safe_creation2.payment_token != NULL_ADDRESS: # Send funds from deployers address to the contract. # NOTE: THIS IS FOR DEVELOPMENT PURPOSES ONLY (self.safe_auto_fund should be False on production) amount_to_send = 1000000000000000000000 funder_balance = self.ethereum_client.erc20.get_balance( self.funder_account.address, safe_creation2.payment_token) if amount_to_send < funder_balance: message = message + 'Sending %d from account %s to %s.' % ( amount_to_send, self.funder_account.address, safe_address, ) self.ethereum_client.erc20.send_tokens( safe_address, amount_to_send, safe_creation2.payment_token, self.funder_account.privateKey) else: message = message + 'Cannot seed wallet with funds. Please faucet %s' % ( self.funder_account.address) logger.info(message) raise NotEnoughFundingForCreation(message) logger.info( 'Found %d balance for safe=%s with payment-token=%s. Required=%d', safe_balance, safe_address, safe_creation2.payment_token, safe_creation2.payment) setup_data = HexBytes(safe_creation2.setup_data.tobytes()) logger.info(setup_data) with EthereumNonceLock(self.redis, self.ethereum_client, self.funder_account.address, timeout=60 * 2) as tx_nonce: logger.info( 'Calling deploy_proxy_contract_with_callback with: funder=%s address=%s setup_data=%s salt_nonce=%s callback=%s', self.funder_account, self.safe_contract_address, setup_data, safe_creation2.salt_nonce, safe_creation2.callback) ethereum_tx_sent = self.proxy_factory.deploy_proxy_contract_with_callback( self.funder_account, self.safe_contract_address, setup_data, safe_creation2.salt_nonce, safe_creation2.gas_estimated, safe_creation2.gas_price_estimated, nonce=tx_nonce, callback=safe_creation2.callback) EthereumTx.objects.create_from_tx(ethereum_tx_sent.tx, ethereum_tx_sent.tx_hash) safe_creation2.tx_hash = ethereum_tx_sent.tx_hash safe_creation2.save() logger.info('Deployed safe=%s with tx-hash=%s', safe_address, ethereum_tx_sent.tx_hash.hex()) return safe_creation2 def estimate_safe_creation( 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( self.ethereum_client, self.safe_old_contract_address, number_owners, gas_price, payment_token, payment_token_eth_value=payment_token_eth_value, fixed_creation_cost=fixed_creation_cost) 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_token_eth_value=payment_token_eth_value, fixed_creation_cost=fixed_creation_cost) def estimate_safe_creation_for_all_tokens( self, number_owners: int) -> List[SafeCreationEstimate]: # Estimate for eth, then calculate for the rest of the tokens ether_creation_estimate = self.estimate_safe_creation2( number_owners, NULL_ADDRESS) safe_creation_estimates = [ether_creation_estimate] token_gas_difference = 50000 # 50K gas more expensive than ether for token in Token.objects.gas_tokens(): try: safe_creation_estimates.append( SafeCreationEstimate( gas=ether_creation_estimate.gas + token_gas_difference, gas_price=ether_creation_estimate.gas_price, payment=token.calculate_payment( ether_creation_estimate.payment), payment_token=token.address, )) except CannotGetTokenPriceFromApi: logger.error('Cannot get price for token=%s', token.address) return safe_creation_estimates def retrieve_safe_info(self, address: str) -> SafeInfo: safe = Safe(address, self.ethereum_client) if not self.ethereum_client.is_contract(address): raise SafeNotDeployed('Safe with address=%s not deployed' % address) nonce = safe.retrieve_nonce() threshold = safe.retrieve_threshold() owners = safe.retrieve_owners() master_copy = safe.retrieve_master_copy_address() version = safe.retrieve_version() return SafeInfo(address, nonce, threshold, owners, master_copy, version)