diff --git a/gnosis/safe/safe.py b/gnosis/safe/safe.py index adfb06eb9..aa76c8169 100644 --- a/gnosis/safe/safe.py +++ b/gnosis/safe/safe.py @@ -1,5 +1,6 @@ import dataclasses import math +from abc import ABCMeta, abstractmethod from enum import Enum from functools import cached_property from logging import getLogger @@ -33,7 +34,6 @@ fast_bytes_to_checksum_address, fast_is_checksum_address, fast_keccak, - get_eth_address_with_key, ) from gnosis.safe.proxy_factory import ProxyFactory @@ -43,8 +43,7 @@ CannotRetrieveSafeInfoException, InvalidPaymentToken, ) -from .safe_create2_tx import SafeCreate2Tx, SafeCreate2TxBuilder -from .safe_creation_tx import InvalidERC20Token, SafeCreationTx +from .safe_create2_tx import InvalidERC20Token, SafeCreate2Tx, SafeCreate2TxBuilder from .safe_tx import SafeTx logger = getLogger(__name__) @@ -76,7 +75,7 @@ class SafeInfo: version: str -class SafeBase(ContractCommon): +class SafeBase(ContractCommon, metaclass=ABCMeta): """ Collection of methods and utilies to handle a Safe """ @@ -95,9 +94,7 @@ class SafeBase(ContractCommon): "60b3cbf8b4a223d68d641b3b6ddf9a298e7f33710cf3d3a9d1146b5a6150fbca" ) - def __init__( - self, address: ChecksumAddress, ethereum_client: EthereumClient, version: str - ): + def __init__(self, address: ChecksumAddress, ethereum_client: EthereumClient): """ :param address: Safe address :param ethereum_client: Initialized ethereum client @@ -106,11 +103,22 @@ def __init__( self.ethereum_client = ethereum_client self.w3 = self.ethereum_client.w3 self.address = address - self.version = version def __str__(self): return f"Safe={self.address}" + @property + @abstractmethod + def version(self) -> str: + """ + :return: String with semantic version + """ + raise NotImplementedError + + @cached_property + def chain_id(self) -> int: + return self.ethereum_client.get_chain_id() + def retrieve_version( self, block_identifier: Optional[BlockIdentifier] = "latest" ) -> str: @@ -374,7 +382,7 @@ def estimate_tx_gas_by_trying( self, to: str, value: int, data: Union[bytes, str], operation: int ): """ - Try to get an estimation with Safe's `requiredTxGas`. If estimation if successful, try to set a gas limit and + Try to get an estimation with Safe's `requiredTxGas`. If estimation is successful, try to set a gas limit and estimate again. If gas estimation is ok, same gas estimation should be returned, if it's less than required estimation will not be completed, so estimation was not accurate and gas limit needs to be increased. @@ -459,28 +467,11 @@ def estimate_tx_gas(self, to: str, value: int, data: bytes, operation: int) -> i + WEB3_ESTIMATION_OFFSET ) - def estimate_tx_operational_gas(self, data_bytes_length: int) -> int: - """ - DEPRECATED. `estimate_tx_base_gas` already includes this. - Estimates the gas for the verification of the signatures and other safe related tasks - before and after executing a transaction. - Calculation will be the sum of: - - - Base cost of 15000 gas - - 100 of gas per word of `data_bytes` - - Validate the signatures 5000 * threshold (ecrecover for ecdsa ~= 4K gas) - - :param data_bytes_length: Length of the data (in bytes, so `len(HexBytes('0x12'))` would be `1` - :return: gas costs per signature * threshold of Safe - """ - threshold = self.retrieve_threshold() - return 15000 + data_bytes_length // 32 * 100 + 5000 * threshold - def get_message_hash(self, message: Union[str, Hash32]) -> Hash32: """ Return hash of a message that can be signed by owners. - :param message: Message that should be hashed + :param message: Message that should be hashed. A ``Hash32`` must be provided for EIP191 or EIP712 messages :return: Message hash """ @@ -661,7 +652,7 @@ def retrieve_is_hash_approved( def retrieve_is_message_signed( self, - message_hash: bytes, + message_hash: Hash32, block_identifier: Optional[BlockIdentifier] = "latest", ) -> bool: return self.contract.functions.signedMessages(message_hash).call( @@ -707,7 +698,6 @@ def build_multisig_tx( refund_receiver: str = NULL_ADDRESS, signatures: bytes = b"", safe_nonce: Optional[int] = None, - safe_version: Optional[str] = None, ) -> SafeTx: """ Allows to execute a Safe transaction confirmed by required number of owners and then pays the account @@ -722,7 +712,7 @@ def build_multisig_tx( (e.g. base transaction fee, signature check, payment of the refund) :param gas_price: Gas price that should be used for the payment calculation :param gas_token: Token address (or `0x000..000` if ETH) that is used for the payment - :param refund_receiver: Address of receiver of gas payment (or `0x000..000` if tx.origin). + :param refund_receiver: Address of receiver of gas payment (or `0x000..000` if tx.origin). :param signatures: Packed signature data ({bytes32 r}{bytes32 s}{uint8 v}) :param safe_nonce: Nonce of the safe (to calculate hash) :param safe_version: Safe version (to calculate hash) @@ -731,7 +721,6 @@ def build_multisig_tx( if safe_nonce is None: safe_nonce = self.retrieve_nonce() - safe_version = safe_version or self.retrieve_version() return SafeTx( self.ethereum_client, self.address, @@ -746,7 +735,8 @@ def build_multisig_tx( refund_receiver, signatures=signatures, safe_nonce=safe_nonce, - safe_version=safe_version, + safe_version=self.version, + chain_id=self.chain_id, ) def send_multisig_tx( @@ -815,53 +805,58 @@ def send_multisig_tx( return EthereumTxSent(tx_hash, tx, None) -class SafeV130(SafeBase): - @cached_property - def contract(self) -> Contract: - return get_safe_V1_3_0_contract(self.w3, address=self.address) - - @classmethod - def deploy_master_contract( - cls, ethereum_client: EthereumClient, deployer_account: LocalAccount - ) -> EthereumTxSent: - """ - Deploy master contract v1.3.0. Takes deployer_account (if unlocked in the node) or the deployer private key - Safe with version > v1.1.1 doesn't need to be initialized as it already has a constructor - - :param ethereum_client: - :param deployer_account: Ethereum account - :return: deployed contract address - """ - - return cls._deploy_master_contract( - ethereum_client, deployer_account, get_safe_V1_3_0_contract - ) - +class SafeV001(SafeBase): + @property + def version(self): + return "0.0.1" -class SafeV111(SafeBase): @cached_property def contract(self) -> Contract: - return get_safe_V1_1_1_contract(self.w3, address=self.address) + return get_safe_V0_0_1_contract(self.w3, address=self.address) - @classmethod + @staticmethod def deploy_master_contract( - cls, ethereum_client: EthereumClient, deployer_account: LocalAccount + ethereum_client: EthereumClient, deployer_account: LocalAccount ) -> EthereumTxSent: """ - Deploy master contract v1.1.1. Takes deployer_account (if unlocked in the node) or the deployer private key - Safe with version > v1.1.1 doesn't need to be initialized as it already has a constructor + Deploy master contract. Takes deployer_account (if unlocked in the node) or the deployer private key :param ethereum_client: :param deployer_account: Ethereum account :return: deployed contract address """ - return cls._deploy_master_contract( - ethereum_client, deployer_account, get_safe_V1_1_1_contract + safe_contract = get_safe_V0_0_1_contract(ethereum_client.w3) + constructor_data = safe_contract.constructor().build_transaction({"gas": 0})[ + "data" + ] + initializer_data = safe_contract.functions.setup( + # We use 2 owners that nobody controls for the master copy + [ + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003", + ], + 2, # Threshold. Maximum security + NULL_ADDRESS, # Address for optional DELEGATE CALL + b"", # Data for optional DELEGATE CALL + ).build_transaction({"to": NULL_ADDRESS})["data"] + + ethereum_tx_sent = ethereum_client.deploy_and_initialize_contract( + deployer_account, constructor_data, HexBytes(initializer_data) + ) + logger.info( + "Deployed and initialized Old Safe Master Contract=%s by %s", + ethereum_tx_sent.contract_address, + deployer_account.address, ) + return ethereum_tx_sent class SafeV100(SafeBase): + @property + def version(self): + return "1.0.0" + @cached_property def contract(self) -> Contract: return get_safe_V1_0_0_contract(self.w3, address=self.address) @@ -950,92 +945,113 @@ def retrieve_all_info( raise CannotRetrieveSafeInfoException(self.address) from e -class SafeV001(SafeBase): +class SafeV111(SafeBase): + @property + def version(self): + return "1.1.1" + @cached_property def contract(self) -> Contract: - return get_safe_V0_0_1_contract(self.w3, address=self.address) + return get_safe_V1_1_1_contract(self.w3, address=self.address) - @staticmethod + @classmethod def deploy_master_contract( - ethereum_client: EthereumClient, deployer_account: LocalAccount + cls, ethereum_client: EthereumClient, deployer_account: LocalAccount ) -> EthereumTxSent: """ - Deploy master contract. Takes deployer_account (if unlocked in the node) or the deployer private key + Deploy master contract v1.1.1. Takes deployer_account (if unlocked in the node) or the deployer private key + Safe with version > v1.1.1 doesn't need to be initialized as it already has a constructor :param ethereum_client: :param deployer_account: Ethereum account :return: deployed contract address """ - safe_contract = get_safe_V0_0_1_contract(ethereum_client.w3) - constructor_data = safe_contract.constructor().build_transaction({"gas": 0})[ - "data" - ] - initializer_data = safe_contract.functions.setup( - # We use 2 owners that nobody controls for the master copy - [ - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003", - ], - 2, # Threshold. Maximum security - NULL_ADDRESS, # Address for optional DELEGATE CALL - b"", # Data for optional DELEGATE CALL - ).build_transaction({"to": NULL_ADDRESS})["data"] - - ethereum_tx_sent = ethereum_client.deploy_and_initialize_contract( - deployer_account, constructor_data, HexBytes(initializer_data) + return cls._deploy_master_contract( + ethereum_client, deployer_account, get_safe_V1_1_1_contract ) - logger.info( - "Deployed and initialized Old Safe Master Contract=%s by %s", - ethereum_tx_sent.contract_address, - deployer_account.address, + + +class SafeV130(SafeBase): + @property + def version(self): + return "1.3.0" + + @cached_property + def contract(self) -> Contract: + return get_safe_V1_3_0_contract(self.w3, address=self.address) + + @classmethod + def deploy_master_contract( + cls, ethereum_client: EthereumClient, deployer_account: LocalAccount + ) -> EthereumTxSent: + """ + Deploy master contract v1.3.0. Takes deployer_account (if unlocked in the node) or the deployer private key + Safe with version > v1.1.1 doesn't need to be initialized as it already has a constructor + + :param ethereum_client: + :param deployer_account: Ethereum account + :return: deployed contract address + """ + + return cls._deploy_master_contract( + ethereum_client, deployer_account, get_safe_V1_3_0_contract ) - return ethereum_tx_sent class Safe: versions = { - "1.3.0": SafeV130, - "1.1.1": SafeV111, - "1.0.0": SafeV100, "0.0.1": SafeV001, + "1.0.0": SafeV100, + "1.1.1": SafeV111, + "1.3.0": SafeV130, } def __new__(cls, address: ChecksumAddress, ethereum_client: EthereumClient): assert fast_is_checksum_address(address), "%s is not a valid address" % address + version: Optional[str] try: contract = get_safe_contract(ethereum_client.w3, address=address) version = contract.functions.VERSION().call(block_identifier="latest") - safe_version = cls.versions.get(version, SafeV130) - except (Web3Exception, ValueError) as e: - # Done to pass the tests, but should instanciate a not deployed safe? - safe_version = SafeV130 - version = "1.3.0" + except (Web3Exception, ValueError): + version = None # Cannot detect the version + safe_version = cls.versions.get(version, SafeV130) instance = super().__new__(safe_version) - instance.__init__(address, ethereum_client, version) + instance.__init__(address, ethereum_client) return instance @staticmethod def create( ethereum_client: EthereumClient, deployer_account: LocalAccount, - master_copy_address: str, - owners: List[str], + master_copy_address: ChecksumAddress, + owners: List[ChecksumAddress], threshold: int, - fallback_handler: str = NULL_ADDRESS, - proxy_factory_address: Optional[str] = None, - payment_token: str = NULL_ADDRESS, + fallback_handler: Optional[ChecksumAddress] = NULL_ADDRESS, + proxy_factory_address: Optional[ChecksumAddress] = None, + payment_token: Optional[ChecksumAddress] = NULL_ADDRESS, payment: int = 0, - payment_receiver: str = NULL_ADDRESS, + payment_receiver: Optional[ChecksumAddress] = NULL_ADDRESS, ) -> EthereumTxSent: """ Deploy new Safe proxy pointing to the specified `master_copy` address and configured with the provided `owners` and `threshold`. By default, payment for the deployer of the tx will be `0`. If `proxy_factory_address` is set deployment will be done using the proxy factory instead of calling the `constructor` of a new `DelegatedProxy` - Using `proxy_factory_address` is recommended, as it takes less gas. - (Testing with `Ganache` and 1 owner 261534 without proxy vs 229022 with Proxy) + Using `proxy_factory_address` is recommended + + :param ethereum_client: + :param deployer_account: + :param master_copy_address: + :param owners: + :param threshold: + :param fallback_handler: + :param proxy_factory_address: + :param payment_token: + :param payment: + :param payment_receiver: + :return: """ assert owners, "At least one owner must be set" @@ -1079,12 +1095,12 @@ def create( contract_address = tx_receipt["contractAddress"] return EthereumTxSent(tx_hash, tx, contract_address) - @classmethod + @staticmethod def deploy_compatibility_fallback_handler( - cls, ethereum_client: EthereumClient, deployer_account: LocalAccount + ethereum_client: EthereumClient, deployer_account: LocalAccount ) -> EthereumTxSent: """ - Deploy Compatibility Fallback handler v1.3.0 + Deploy Last compatibility Fallback handler (v1.3.0) :param ethereum_client: :param deployer_account: Ethereum account @@ -1113,39 +1129,6 @@ def deploy_compatibility_fallback_handler( ) return ethereum_tx_sent - @staticmethod - def estimate_safe_creation( - ethereum_client: EthereumClient, - old_master_copy_address: str, - number_owners: int, - gas_price: int, - payment_token: Optional[str], - payment_receiver: str = NULL_ADDRESS, - payment_token_eth_value: float = 1.0, - fixed_creation_cost: Optional[int] = None, - ) -> SafeCreationEstimate: - s = 15 - owners = [get_eth_address_with_key()[0] for _ in range(number_owners)] - threshold = number_owners - safe_creation_tx = SafeCreationTx( - w3=ethereum_client.w3, - owners=owners, - threshold=threshold, - signature_s=s, - master_copy=old_master_copy_address, - gas_price=gas_price, - funder=payment_receiver, - payment_token=payment_token, - payment_token_eth_value=payment_token_eth_value, - fixed_creation_cost=fixed_creation_cost, - ) - return SafeCreationEstimate( - safe_creation_tx.gas, - safe_creation_tx.gas_price, - safe_creation_tx.payment, - safe_creation_tx.payment_token, - ) - @staticmethod def estimate_safe_creation_2( ethereum_client: EthereumClient, @@ -1188,40 +1171,6 @@ def estimate_safe_creation_2( safe_creation_tx.payment_token, ) - @staticmethod - def build_safe_creation_tx( - ethereum_client: EthereumClient, - master_copy_old_address: str, - s: int, - owners: List[str], - threshold: int, - gas_price: int, - payment_token: Optional[str], - payment_receiver: str, - payment_token_eth_value: float = 1.0, - fixed_creation_cost: Optional[int] = None, - ) -> SafeCreationTx: - try: - safe_creation_tx = SafeCreationTx( - w3=ethereum_client.w3, - owners=owners, - threshold=threshold, - signature_s=s, - master_copy=master_copy_old_address, - gas_price=gas_price, - funder=payment_receiver, - payment_token=payment_token, - payment_token_eth_value=payment_token_eth_value, - fixed_creation_cost=fixed_creation_cost, - ) - except InvalidERC20Token as exc: - raise InvalidPaymentToken( - "Invalid payment token %s" % payment_token - ) from exc - - assert safe_creation_tx.tx_pyethereum.nonce == 0 - return safe_creation_tx - @staticmethod def build_safe_create2_tx( ethereum_client: EthereumClient, diff --git a/gnosis/safe/safe_creation_tx.py b/gnosis/safe/safe_creation_tx.py deleted file mode 100644 index 5ee768d9c..000000000 --- a/gnosis/safe/safe_creation_tx.py +++ /dev/null @@ -1,337 +0,0 @@ -import math -import os -from logging import getLogger -from typing import Any, Dict, List, Optional, Tuple - -import rlp -from eth.vm.forks.frontier.transactions import FrontierTransaction -from eth_keys.exceptions import BadSignature -from hexbytes import HexBytes -from web3 import Web3 -from web3.contract import ContractConstructor -from web3.exceptions import Web3Exception - -from gnosis.eth.constants import GAS_CALL_DATA_BYTE, NULL_ADDRESS, SECPK1_N -from gnosis.eth.contracts import ( - get_erc20_contract, - get_paying_proxy_contract, - get_safe_V0_0_1_contract, -) -from gnosis.eth.utils import ( - fast_is_checksum_address, - fast_to_checksum_address, - mk_contract_address, -) - -logger = getLogger(__name__) - - -class InvalidERC20Token(Exception): - pass - - -class SafeCreationTx: - def __init__( - self, - w3: Web3, - owners: List[str], - threshold: int, - signature_s: int, - master_copy: str, - gas_price: int, - funder: Optional[str], - payment_token: Optional[str] = None, - payment_token_eth_value: float = 1.0, - fixed_creation_cost: Optional[int] = None, - ): - """ - Prepare Safe creation - :param w3: Web3 instance - :param owners: Owners of the Safe - :param threshold: Minimum number of users required to operate the Safe - :param signature_s: Random s value for ecdsa signature - :param master_copy: Safe master copy address - :param gas_price: Gas Price - :param funder: Address to refund when the Safe is created. Address(0) if no need to refund - :param payment_token: Payment token instead of paying the funder with ether. If None Ether will be used - :param payment_token_eth_value: Value of payment token per 1 Ether - :param fixed_creation_cost: Fixed creation cost of Safe (Wei) - """ - - assert 0 < threshold <= len(owners) - funder = funder or NULL_ADDRESS - payment_token = payment_token or NULL_ADDRESS - assert fast_is_checksum_address(master_copy) - assert fast_is_checksum_address(funder) - assert fast_is_checksum_address(payment_token) - - self.w3 = w3 - self.owners = owners - self.threshold = threshold - self.s = signature_s - self.master_copy = master_copy - self.gas_price = gas_price - self.funder = funder - self.payment_token = payment_token - self.payment_token_eth_value = payment_token_eth_value - self.fixed_creation_cost = fixed_creation_cost - - # Get bytes for `setup(address[] calldata _owners, uint256 _threshold, address to, bytes calldata data)` - # This initializer will be passed to the proxy and will be called right after proxy is deployed - safe_setup_data: bytes = self._get_initial_setup_safe_data(owners, threshold) - - # Calculate gas based on experience of previous deployments of the safe - calculated_gas: int = self._calculate_gas( - owners, safe_setup_data, payment_token - ) - # Estimate gas using web3 - estimated_gas: int = self._estimate_gas( - master_copy, safe_setup_data, funder, payment_token - ) - self.gas = max(calculated_gas, estimated_gas) - - # Payment will be safe deploy cost + transfer fees for sending ether to the deployer - self.payment = self._calculate_refund_payment( - self.gas, gas_price, fixed_creation_cost, payment_token_eth_value - ) - - self.tx_dict: Dict[str, Any] = self._build_proxy_contract_creation_tx( - master_copy=master_copy, - initializer=safe_setup_data, - funder=funder, - payment_token=payment_token, - payment=self.payment, - gas=self.gas, - gas_price=gas_price, - ) - - self.tx_pyethereum: FrontierTransaction = ( - self._build_contract_creation_tx_with_valid_signature(self.tx_dict, self.s) - ) - self.tx_raw = rlp.encode(self.tx_pyethereum) - self.tx_hash = self.tx_pyethereum.hash - self.deployer_address = fast_to_checksum_address(self.tx_pyethereum.sender) - self.safe_address = mk_contract_address(self.tx_pyethereum.sender, 0) - - self.v = self.tx_pyethereum.v - self.r = self.tx_pyethereum.r - self.safe_setup_data = safe_setup_data - - assert mk_contract_address(self.deployer_address, nonce=0) == self.safe_address - - @property - def payment_ether(self): - return self.gas * self.gas_price - - @staticmethod - def find_valid_random_signature(s: int) -> Tuple[int, int]: - """ - Find v and r valid values for a given s - :param s: random value - :return: v, r - """ - for _ in range(10000): - r = int(os.urandom(31).hex(), 16) - v = (r % 2) + 27 - if r < SECPK1_N: - tx = FrontierTransaction(0, 1, 21000, b"", 0, b"", v=v, r=r, s=s) - try: - tx.sender - return v, r - except (BadSignature, ValueError): - logger.debug("Cannot find signature with v=%d r=%d s=%d", v, r, s) - - raise ValueError("Valid signature not found with s=%d", s) - - @staticmethod - def _calculate_gas( - owners: List[str], safe_setup_data: bytes, payment_token: str - ) -> int: - """ - Calculate gas manually, based on tests of previosly deployed safes - :param owners: Safe owners - :param safe_setup_data: Data for proxy setup - :param payment_token: If payment token, we will need more gas to transfer and maybe storage if first time - :return: total gas needed for deployment - """ - # TODO Do gas calculation estimating the call instead this magic - - base_gas = 60580 # Transaction standard gas - - # If we already have the token, we don't have to pay for storage, so it will be just 5K instead of 20K. - # The other 1K is for overhead of making the call - if payment_token != NULL_ADDRESS: - payment_token_gas = 55000 - else: - payment_token_gas = 0 - - data_gas = GAS_CALL_DATA_BYTE * len(safe_setup_data) # Data gas - gas_per_owner = 18020 # Magic number calculated by testing and averaging owners - return ( - base_gas - + data_gas - + payment_token_gas - + 270000 - + len(owners) * gas_per_owner - ) - - @staticmethod - def _calculate_refund_payment( - gas: int, - gas_price: int, - fixed_creation_cost: Optional[int], - payment_token_eth_value: float, - ) -> int: - if fixed_creation_cost is None: - # Payment will be safe deploy cost + transfer fees for sending ether to the deployer - base_payment: int = (gas + 23000) * gas_price - # Calculate payment for tokens using the conversion (if used) - return math.ceil(base_payment / payment_token_eth_value) - else: - return fixed_creation_cost - - def _build_proxy_contract_creation_constructor( - self, - master_copy: str, - initializer: bytes, - funder: str, - payment_token: str, - payment: int, - ) -> ContractConstructor: - """ - :param master_copy: Master Copy of Gnosis Safe already deployed - :param initializer: Data initializer to send to GnosisSafe setup method - :param funder: Address that should get the payment (if payment set) - :param payment_token: Address if a token is used. If not set, 0x0 will be ether - :param payment: Payment - :return: Transaction dictionary - """ - if not funder or funder == NULL_ADDRESS: - funder = NULL_ADDRESS - payment = 0 - - return get_paying_proxy_contract(self.w3).constructor( - master_copy, initializer, funder, payment_token, payment - ) - - def _build_proxy_contract_creation_tx( - self, - master_copy: str, - initializer: bytes, - funder: str, - payment_token: str, - payment: int, - gas: int, - gas_price: int, - nonce: int = 0, - ): - """ - :param master_copy: Master Copy of Gnosis Safe already deployed - :param initializer: Data initializer to send to GnosisSafe setup method - :param funder: Address that should get the payment (if payment set) - :param payment_token: Address if a token is used. If not set, 0x0 will be ether - :param payment: Payment - :return: Transaction dictionary - """ - return self._build_proxy_contract_creation_constructor( - master_copy, initializer, funder, payment_token, payment - ).build_transaction( - { - "gas": gas, - "gasPrice": gas_price, - "nonce": nonce, - } - ) - - def _build_contract_creation_tx_with_valid_signature( - self, tx_dict: Dict[str, Any], s: int - ) -> FrontierTransaction: - """ - Use pyethereum `Transaction` to generate valid tx using a random signature - :param tx_dict: Web3 tx dictionary - :param s: Signature s value - :return: PyEthereum creation tx for the proxy contract - """ - zero_address = HexBytes("0x" + "0" * 40) - f_address = HexBytes("0x" + "f" * 40) - nonce = tx_dict["nonce"] - gas_price = tx_dict["gasPrice"] - gas = tx_dict["gas"] - to = tx_dict.get("to", b"") # Contract creation should always have `to` empty - value = tx_dict["value"] - data = tx_dict["data"] - for _ in range(100): - try: - v, r = self.find_valid_random_signature(s) - contract_creation_tx = FrontierTransaction( - nonce, gas_price, gas, to, value, HexBytes(data), v=v, r=r, s=s - ) - sender_address = contract_creation_tx.sender - contract_address: bytes = HexBytes( - mk_contract_address(sender_address, nonce) - ) - if sender_address in (zero_address, f_address) or contract_address in ( - zero_address, - f_address, - ): - raise ValueError("Invalid transaction") - return contract_creation_tx - except BadSignature: - pass - raise ValueError("Valid signature not found with s=%d", s) - - def _estimate_gas( - self, master_copy: str, initializer: bytes, funder: str, payment_token: str - ) -> int: - """ - Gas estimation done using web3 and calling the node - Payment cannot be estimated, as no ether is in the address. So we add some gas later. - :param master_copy: Master Copy of Gnosis Safe already deployed - :param initializer: Data initializer to send to GnosisSafe setup method - :param funder: Address that should get the payment (if payment set) - :param payment_token: Address if a token is used. If not set, 0x0 will be ether - :return: Total gas estimation - """ - - # Estimate the contract deployment. We cannot estimate the refunding, as the safe address has not any fund - gas: int = self._build_proxy_contract_creation_constructor( - master_copy, initializer, funder, payment_token, 0 - ).estimate_gas() - - # We estimate the refund as a new tx - if payment_token == NULL_ADDRESS: - # Same cost to send 1 ether than 1000 - gas += self.w3.eth.estimate_gas({"to": funder, "value": 1}) - else: - # Top should be around 52000 when storage is needed (funder no previous owner of token), - # we use value 1 as we are simulating an internal call, and in that calls you don't pay for the data. - # If it was a new tx sending 5000 tokens would be more expensive than sending 1 because of data costs - try: - gas += ( - get_erc20_contract(self.w3, payment_token) - .functions.transfer(funder, 1) - .estimate_gas({"from": payment_token}) - ) - except Web3Exception as exc: - if "transfer amount exceeds balance" in str(exc): - return 70000 - raise InvalidERC20Token from exc - - return gas - - def _get_initial_setup_safe_data(self, owners: List[str], threshold: int) -> bytes: - return ( - get_safe_V0_0_1_contract(self.w3, self.master_copy) - .functions.setup( - owners, - threshold, - NULL_ADDRESS, # Contract address for optional delegate call - b"", # Data payload for optional delegate call - ) - .build_transaction( - { - "gas": 1, - "gasPrice": 1, - } - )["data"] - ) diff --git a/gnosis/safe/safe_tx.py b/gnosis/safe/safe_tx.py index 31a229c21..9420bc5fe 100644 --- a/gnosis/safe/safe_tx.py +++ b/gnosis/safe/safe_tx.py @@ -53,7 +53,7 @@ def __init__( refund_receiver: Optional[str], signatures: Optional[bytes] = None, safe_nonce: Optional[int] = None, - safe_version: str = None, + safe_version: Optional[str] = None, chain_id: Optional[int] = None, ): """ diff --git a/gnosis/safe/tests/test_proxy_factory/test_proxy_factory.py b/gnosis/safe/tests/test_proxy_factory/test_proxy_factory.py index 78e10cc6e..360d5048f 100644 --- a/gnosis/safe/tests/test_proxy_factory/test_proxy_factory.py +++ b/gnosis/safe/tests/test_proxy_factory/test_proxy_factory.py @@ -98,47 +98,6 @@ def test_check_proxy_code_mainnet(self): with self.subTest(safe=safe): self.assertTrue(proxy_factory.check_proxy_code(safe)) - def test_deploy_proxy_contract(self): - s = 15 - owners = [Account.create().address for _ in range(2)] - threshold = 2 - payment_token = None - safe_creation_tx = Safe.build_safe_creation_tx( - self.ethereum_client, - self.safe_contract_V0_0_1_address, - s, - owners, - threshold, - self.gas_price, - payment_token, - payment_receiver=self.ethereum_test_account.address, - ) - # Send ether for safe deploying costs - self.send_tx( - {"to": safe_creation_tx.safe_address, "value": safe_creation_tx.payment}, - self.ethereum_test_account, - ) - - proxy_factory = ProxyFactory( - self.proxy_factory_contract_address, self.ethereum_client, version="1.1.1" - ) - ethereum_tx_sent = proxy_factory.deploy_proxy_contract( - self.ethereum_test_account, - safe_creation_tx.master_copy, - safe_creation_tx.safe_setup_data, - safe_creation_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( - safe.retrieve_master_copy_address(), safe_creation_tx.master_copy - ) - self.assertEqual(set(safe.retrieve_owners()), set(owners)) - def test_deploy_proxy_contract_with_nonce(self): salt_nonce = generate_salt_nonce() owners = [Account.create().address for _ in range(2)] diff --git a/gnosis/safe/tests/test_safe.py b/gnosis/safe/tests/test_safe.py index d60eaa866..552fac0f6 100644 --- a/gnosis/safe/tests/test_safe.py +++ b/gnosis/safe/tests/test_safe.py @@ -63,22 +63,6 @@ def test_check_funds_for_tx_gas(self): safe.check_funds_for_tx_gas(safe_tx_gas, base_gas, gas_price, NULL_ADDRESS) ) - def test_estimate_safe_creation(self): - number_owners = 4 - gas_price = self.gas_price - payment_token = NULL_ADDRESS - safe_creation_estimate = Safe.estimate_safe_creation( - self.ethereum_client, - self.safe_contract_V0_0_1_address, - number_owners, - gas_price, - payment_token, - ) - self.assertGreater(safe_creation_estimate.gas_price, 0) - self.assertGreater(safe_creation_estimate.gas, 0) - self.assertGreater(safe_creation_estimate.payment, 0) - self.assertEqual(safe_creation_estimate.payment_token, payment_token) - def test_estimate_safe_creation_2(self): number_owners = 4 gas_price = self.gas_price @@ -549,12 +533,6 @@ def test_estimate_tx_gas_with_web3(self): ).build_transaction({"gas": 0})["data"] safe.estimate_tx_gas_with_web3(deployed_erc20.address, value, transfer_data) - def test_estimate_tx_operational_gas(self): - for threshold in range(2, 5): - safe = self.deploy_test_safe(threshold=threshold, number_owners=6) - tx_signature_gas_estimation = safe.estimate_tx_operational_gas(0) - self.assertGreaterEqual(tx_signature_gas_estimation, 20000) - def test_retrieve_code(self): self.assertEqual( Safe(NULL_ADDRESS, self.ethereum_client).retrieve_code(), HexBytes("0x") diff --git a/gnosis/safe/tests/test_safe_create2_tx.py b/gnosis/safe/tests/test_safe_create2_tx.py index fed84d6a8..fe04e4dc4 100644 --- a/gnosis/safe/tests/test_safe_create2_tx.py +++ b/gnosis/safe/tests/test_safe_create2_tx.py @@ -15,7 +15,7 @@ LOG_TITLE_WIDTH = 100 -class TestSafeCreationTx(SafeTestCaseMixin, TestCase): +class TestSafeCreation2Tx(SafeTestCaseMixin, TestCase): def test_safe_create2_tx_builder(self): w3 = self.w3 diff --git a/gnosis/safe/tests/test_safe_creation_tx.py b/gnosis/safe/tests/test_safe_creation_tx.py deleted file mode 100644 index 1316c388e..000000000 --- a/gnosis/safe/tests/test_safe_creation_tx.py +++ /dev/null @@ -1,480 +0,0 @@ -import logging - -from django.test import TestCase - -from eth_account import Account - -from gnosis.eth.constants import NULL_ADDRESS -from gnosis.eth.contracts import get_safe_contract -from gnosis.eth.utils import get_eth_address_with_key - -from ..safe_creation_tx import SafeCreationTx -from .safe_test_case import SafeTestCaseMixin -from .utils import generate_valid_s - -logger = logging.getLogger(__name__) - -LOG_TITLE_WIDTH = 100 - - -class TestSafeCreationTx(SafeTestCaseMixin, TestCase): - def test_safe_creation_tx_builder(self): - logger.info( - "Test Safe Proxy creation without payment".center(LOG_TITLE_WIDTH, "-") - ) - w3 = self.w3 - - s = generate_valid_s() - - funder_account = self.ethereum_test_account - owners = [get_eth_address_with_key()[0] for _ in range(4)] - threshold = len(owners) - 1 - gas_price = self.gas_price - - safe_creation_tx = SafeCreationTx( - w3=w3, - owners=owners, - threshold=threshold, - signature_s=s, - master_copy=self.safe_contract_V0_0_1_address, - gas_price=gas_price, - funder=NULL_ADDRESS, - ) - - logger.info( - "Send %d gwei to deployer %s", - w3.from_wei(safe_creation_tx.payment_ether, "gwei"), - safe_creation_tx.deployer_address, - ) - - self.send_tx( - { - "to": safe_creation_tx.deployer_address, - "value": safe_creation_tx.payment_ether, - }, - funder_account, - ) - - logger.info( - "Create proxy contract with address %s", safe_creation_tx.safe_address - ) - - tx_hash = w3.eth.send_raw_transaction(safe_creation_tx.tx_raw) - tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - self.assertEqual(tx_receipt.contractAddress, safe_creation_tx.safe_address) - - deployed_safe_proxy_contract = get_safe_contract(w3, tx_receipt.contractAddress) - - logger.info( - "Deployer account has still %d gwei left (will be lost)", - w3.from_wei(w3.eth.get_balance(safe_creation_tx.deployer_address), "gwei"), - ) - - self.assertEqual( - deployed_safe_proxy_contract.functions.getThreshold().call(), threshold - ) - self.assertEqual( - deployed_safe_proxy_contract.functions.getOwners().call(), owners - ) - - def test_safe_creation_tx_builder_with_not_enough_funds(self): - w3 = self.w3 - s = generate_valid_s() - funder_account = self.ethereum_test_account - owners = [get_eth_address_with_key()[0] for _ in range(4)] - threshold = len(owners) - 1 - gas_price = self.gas_price - - safe_creation_tx = SafeCreationTx( - w3=w3, - owners=owners, - threshold=threshold, - signature_s=s, - master_copy=self.safe_contract_V0_0_1_address, - gas_price=gas_price, - funder=NULL_ADDRESS, - ) - - logger.info( - "Send %d gwei to deployer %s", - w3.from_wei(safe_creation_tx.payment_ether - 1, "gwei"), - safe_creation_tx.deployer_address, - ) - self.send_tx( - { - "to": safe_creation_tx.deployer_address, - "value": safe_creation_tx.payment_ether - 1, - }, - funder_account, - ) - - with self.assertRaisesMessage(ValueError, "insufficient funds for gas"): - w3.eth.send_raw_transaction(safe_creation_tx.tx_raw) - - def test_safe_creation_tx_builder_with_payment(self): - logger.info( - "Test Safe Proxy creation With Payment".center(LOG_TITLE_WIDTH, "-") - ) - w3 = self.w3 - - s = generate_valid_s() - - funder_account = self.ethereum_test_account - owners = [get_eth_address_with_key()[0] for _ in range(2)] - threshold = len(owners) - 1 - gas_price = self.gas_price - - safe_creation_tx = SafeCreationTx( - w3=w3, - owners=owners, - threshold=threshold, - signature_s=s, - master_copy=self.safe_contract_V0_0_1_address, - gas_price=gas_price, - funder=funder_account.address, - ) - - user_external_account = Account.create() - # Send some ether to that account - safe_balance = w3.to_wei(0.01, "ether") - self.send_tx( - {"to": user_external_account.address, "value": safe_balance * 2}, - funder_account, - ) - - logger.info( - "Send %d ether to safe %s", - w3.from_wei(safe_balance, "ether"), - safe_creation_tx.safe_address, - ) - self.send_tx( - {"to": safe_creation_tx.safe_address, "value": safe_balance}, - user_external_account, - ) - self.assertEqual( - w3.eth.get_balance(safe_creation_tx.safe_address), safe_balance - ) - - logger.info( - "Send %d gwei to deployer %s", - w3.from_wei(safe_creation_tx.payment_ether, "gwei"), - safe_creation_tx.deployer_address, - ) - self.send_tx( - { - "to": safe_creation_tx.deployer_address, - "value": safe_creation_tx.payment_ether, - }, - funder_account, - ) - - logger.info( - "Create proxy contract with address %s", safe_creation_tx.safe_address - ) - - funder_balance = w3.eth.get_balance(funder_account.address) - - # This tx will create the Safe Proxy and return ether to the funder - tx_hash = w3.eth.send_raw_transaction(safe_creation_tx.tx_raw) - tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - self.assertEqual(tx_receipt.contractAddress, safe_creation_tx.safe_address) - - self.assertEqual( - w3.eth.get_balance(funder_account.address), - funder_balance + safe_creation_tx.payment, - ) - - logger.info( - "Deployer account has still %d gwei left (will be lost)", - w3.from_wei(w3.eth.get_balance(safe_creation_tx.deployer_address), "gwei"), - ) - - deployed_safe_proxy_contract = get_safe_contract(w3, tx_receipt.contractAddress) - - self.assertEqual( - deployed_safe_proxy_contract.functions.getThreshold().call(), threshold - ) - self.assertEqual( - deployed_safe_proxy_contract.functions.getOwners().call(), owners - ) - - def test_safe_creation_tx_builder_with_token_payment(self): - logger.info( - "Test Safe Proxy creation With Gas Payment".center(LOG_TITLE_WIDTH, "-") - ) - w3 = self.w3 - - s = generate_valid_s() - - erc20_deployer = Account.create() - funder_account = self.ethereum_test_account - - # Send something to the erc20 deployer - self.send_tx( - {"to": erc20_deployer.address, "value": w3.to_wei(1, "ether")}, - funder_account, - ) - - funder = funder_account.address - owners = [get_eth_address_with_key()[0] for _ in range(2)] - threshold = len(owners) - 1 - gas_price = self.gas_price - token_amount = int(1e18) - erc20_contract = self.deploy_example_erc20(token_amount, erc20_deployer.address) - self.assertEqual( - erc20_contract.functions.balanceOf(erc20_deployer.address).call(), - token_amount, - ) - - safe_creation_tx = SafeCreationTx( - w3=w3, - owners=owners, - threshold=threshold, - signature_s=s, - master_copy=self.safe_contract_V0_0_1_address, - gas_price=gas_price, - payment_token=erc20_contract.address, - funder=funder, - ) - - # In this test we will pretend that ether value = token value, so we send tokens as ether payment - payment = safe_creation_tx.payment - deployer_address = safe_creation_tx.deployer_address - safe_address = safe_creation_tx.safe_address - logger.info("Send %d tokens to safe %s", payment, safe_address) - self.send_tx( - erc20_contract.functions.transfer(safe_address, payment).build_transaction( - {"from": erc20_deployer.address} - ), - erc20_deployer, - ) - self.assertEqual( - erc20_contract.functions.balanceOf(safe_address).call(), payment - ) - - logger.info( - "Send %d ether to deployer %s", - w3.from_wei(payment, "ether"), - deployer_address, - ) - self.send_tx( - { - "to": safe_creation_tx.deployer_address, - "value": safe_creation_tx.payment, - }, - funder_account, - ) - - logger.info( - "Create proxy contract with address %s", safe_creation_tx.safe_address - ) - funder_balance = w3.eth.get_balance(funder) - - # This tx will create the Safe Proxy and return tokens to the funder - tx_hash = w3.eth.send_raw_transaction(safe_creation_tx.tx_raw) - tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - self.assertEqual(tx_receipt.contractAddress, safe_address) - self.assertEqual(w3.eth.get_balance(funder), funder_balance) - self.assertEqual(erc20_contract.functions.balanceOf(funder).call(), payment) - self.assertEqual(erc20_contract.functions.balanceOf(safe_address).call(), 0) - - logger.info( - "Deployer account has still %d gwei left (will be lost)", - w3.from_wei(w3.eth.get_balance(safe_creation_tx.deployer_address), "gwei"), - ) - - deployed_safe_proxy_contract = get_safe_contract(w3, tx_receipt.contractAddress) - - self.assertEqual( - deployed_safe_proxy_contract.functions.getThreshold().call(), threshold - ) - self.assertEqual( - deployed_safe_proxy_contract.functions.getOwners().call(), owners - ) - - # Payment should be <= when payment_token_eth_value is 1.0 - # Funder will already have tokens so no storage need to be paid) - safe_creation_tx_2 = SafeCreationTx( - w3=w3, - owners=owners, - threshold=threshold, - signature_s=s, - master_copy=self.safe_contract_V0_0_1_address, - gas_price=gas_price, - payment_token=erc20_contract.address, - payment_token_eth_value=1.0, - funder=funder, - ) - self.assertLessEqual(safe_creation_tx_2.payment, safe_creation_tx.payment) - - # Now payment should be equal when payment_token_eth_value is 1.0 - safe_creation_tx_3 = SafeCreationTx( - w3=w3, - owners=owners, - threshold=threshold, - signature_s=s, - master_copy=self.safe_contract_V0_0_1_address, - gas_price=gas_price, - payment_token=erc20_contract.address, - payment_token_eth_value=1.0, - funder=funder, - ) - self.assertEqual(safe_creation_tx_3.payment, safe_creation_tx_2.payment) - - # Check that payment is less when payment_token_eth_value is set(token value > ether) - safe_creation_tx_4 = SafeCreationTx( - w3=w3, - owners=owners, - threshold=threshold, - signature_s=s, - master_copy=self.safe_contract_V0_0_1_address, - gas_price=gas_price, - payment_token=erc20_contract.address, - payment_token_eth_value=1.1, - funder=funder, - ) - self.assertLess(safe_creation_tx_4.payment, safe_creation_tx.payment) - - # Check that payment is more when payment_token_eth_value is set(token value < ether) - safe_creation_tx_5 = SafeCreationTx( - w3=w3, - owners=owners, - threshold=threshold, - signature_s=s, - master_copy=self.safe_contract_V0_0_1_address, - gas_price=gas_price, - payment_token=erc20_contract.address, - payment_token_eth_value=0.1, - funder=funder, - ) - self.assertGreater(safe_creation_tx_5.payment, safe_creation_tx.payment) - - def test_safe_creation_tx_builder_with_fixed_cost(self): - logger.info( - "Test Safe Proxy creation With Fixed Cost".center(LOG_TITLE_WIDTH, "-") - ) - w3 = self.w3 - - s = generate_valid_s() - - funder_account = self.ethereum_test_account - owners = [get_eth_address_with_key()[0] for _ in range(2)] - threshold = len(owners) - 1 - gas_price = self.gas_price - fixed_creation_cost = 123 # Wei - - safe_creation_tx = SafeCreationTx( - w3=w3, - owners=owners, - threshold=threshold, - signature_s=s, - master_copy=self.safe_contract_V0_0_1_address, - gas_price=gas_price, - payment_token=None, - funder=funder_account.address, - fixed_creation_cost=fixed_creation_cost, - ) - - self.assertEqual(safe_creation_tx.payment, fixed_creation_cost) - self.assertEqual( - safe_creation_tx.payment_ether, - safe_creation_tx.gas * safe_creation_tx.gas_price, - ) - - deployer_address = safe_creation_tx.deployer_address - safe_address = safe_creation_tx.safe_address - safe_balance = w3.to_wei(0.01, "ether") - logger.info( - "Send %d ether to safe %s", w3.from_wei(safe_balance, "ether"), safe_address - ) - self.send_tx({"to": safe_address, "value": safe_balance}, funder_account) - self.assertEqual(w3.eth.get_balance(safe_address), safe_balance) - - logger.info( - "Send %d ether to deployer %s", - w3.from_wei(safe_creation_tx.payment_ether, "ether"), - deployer_address, - ) - self.send_tx( - {"to": deployer_address, "value": safe_creation_tx.payment_ether}, - funder_account, - ) - - logger.info( - "Create proxy contract with address %s", safe_creation_tx.safe_address - ) - - funder_balance = w3.eth.get_balance(funder_account.address) - - # This tx will create the Safe Proxy and return tokens to the funder - tx_hash = w3.eth.send_raw_transaction(safe_creation_tx.tx_raw) - tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - self.assertEqual(tx_receipt.contractAddress, safe_address) - self.assertEqual( - w3.eth.get_balance(safe_address), safe_balance - fixed_creation_cost - ) - self.assertLess( - w3.eth.get_balance(deployer_address), safe_creation_tx.payment_ether - ) - self.assertEqual( - w3.eth.get_balance(funder_account.address), - funder_balance + safe_creation_tx.payment, - ) - - logger.info( - "Deployer account has still %d gwei left (will be lost)", - w3.from_wei(w3.eth.get_balance(safe_creation_tx.deployer_address), "gwei"), - ) - - deployed_safe_proxy_contract = get_safe_contract(w3, safe_address) - - self.assertEqual( - deployed_safe_proxy_contract.functions.getThreshold().call(), threshold - ) - self.assertEqual( - deployed_safe_proxy_contract.functions.getOwners().call(), owners - ) - - def test_safe_gas_with_multiple_owners(self): - logger.info( - "Test Safe Proxy creation gas with multiple owners".center( - LOG_TITLE_WIDTH, "-" - ) - ) - w3 = self.w3 - funder_account = self.ethereum_test_account - number_of_accounts = 10 - for i in range(2, number_of_accounts): - s = generate_valid_s() - owners = [get_eth_address_with_key()[0] for _ in range(i + 1)] - threshold = len(owners) - gas_price = w3.to_wei(15, "gwei") - - safe_creation_tx = SafeCreationTx( - w3=w3, - owners=owners, - threshold=threshold, - signature_s=s, - master_copy=self.safe_contract_V0_0_1_address, - gas_price=gas_price, - funder=None, - ) - - self.send_tx( - { - "to": safe_creation_tx.deployer_address, - "value": safe_creation_tx.payment, - }, - funder_account, - ) - tx_hash = w3.eth.send_raw_transaction(safe_creation_tx.tx_raw) - tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - self.assertEqual(tx_receipt.contractAddress, safe_creation_tx.safe_address) - - logger.info( - "Number of owners: %d - Gas estimated %d - Gas Used %d - Difference %d", - len(owners), - safe_creation_tx.gas, - tx_receipt.gasUsed, - safe_creation_tx.gas - tx_receipt.gasUsed, - ) diff --git a/gnosis/safe/tests/utils.py b/gnosis/safe/tests/utils.py index 2c5489181..cd623872a 100644 --- a/gnosis/safe/tests/utils.py +++ b/gnosis/safe/tests/utils.py @@ -2,12 +2,7 @@ import random from logging import getLogger -from web3 import Web3 - -from gnosis.eth.tests.utils import send_tx - -from ...eth.constants import SECPK1_N -from ..safe_creation_tx import SafeCreationTx +from gnosis.eth.constants import SECPK1_N logger = getLogger(__name__) @@ -21,57 +16,3 @@ def generate_valid_s() -> int: s = int(os.urandom(30).hex(), 16) if s <= (SECPK1_N // 2): return s - - -def deploy_safe( - w3: Web3, - safe_creation_tx: SafeCreationTx, - funder: str, - initial_funding_wei: int = 0, - funder_account=None, -) -> str: - if funder_account: - send_tx( - w3, - { - "to": safe_creation_tx.deployer_address, - "value": safe_creation_tx.payment, - }, - funder_account, - ) - - send_tx( - w3, - { - "to": safe_creation_tx.safe_address, - "value": safe_creation_tx.payment + initial_funding_wei, - }, - funder_account, - ) - else: - w3.eth.wait_for_transaction_receipt( - w3.eth.send_transaction( - { - "from": funder, - "to": safe_creation_tx.deployer_address, - "value": safe_creation_tx.payment, - } - ) - ) - - w3.eth.wait_for_transaction_receipt( - w3.eth.send_transaction( - { - "from": funder, - "to": safe_creation_tx.safe_address, - "value": safe_creation_tx.payment + initial_funding_wei, - } - ) - ) - - tx_hash = w3.eth.send_raw_transaction(bytes(safe_creation_tx.tx_raw)) - tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - assert tx_receipt.contractAddress == safe_creation_tx.safe_address - assert tx_receipt.status - - return safe_creation_tx.safe_address