def __init__(self, w3: Web3, master_copy_address: str, proxy_factory_address: str): """ Init builder for safe creation using create2 :param w3: Web3 instance :param master_copy_address: `Gnosis Safe` master copy address :param proxy_factory_address: `Gnosis Proxy Factory` address """ assert Web3.isChecksumAddress(master_copy_address) assert Web3.isChecksumAddress(proxy_factory_address) self.w3 = w3 self.master_copy_address = master_copy_address self.proxy_factory_address = proxy_factory_address self.safe_version = get_safe_contract( w3, master_copy_address).functions.VERSION().call() if self.safe_version == '1.1.1': self.master_copy_contract = get_safe_contract( w3, master_copy_address) elif self.safe_version == '1.0.0': self.master_copy_contract = get_safe_V1_0_0_contract( w3, master_copy_address) else: raise ValueError('Safe version must be 1.1.1 or 1.0.0') self.proxy_factory_contract = get_proxy_factory_contract( w3, proxy_factory_address)
def setUpTestData(cls): super().setUpTestData() for key, value in contract_addresses.items(): if callable(value): contract_addresses[key] = value(cls.ethereum_client, cls.ethereum_test_account).contract_address settings.SAFE_CONTRACT_ADDRESS = contract_addresses['safe'] settings.SAFE_MULTISEND_ADDRESS = contract_addresses['multi_send'] settings.SAFE_V1_0_0_CONTRACT_ADDRESS = contract_addresses['safe_V1_0_0'] settings.SAFE_V0_0_1_CONTRACT_ADDRESS = contract_addresses['safe_V0_0_1'] settings.SAFE_PROXY_FACTORY_ADDRESS = contract_addresses['proxy_factory'] settings.SAFE_PROXY_FACTORY_V1_0_0_ADDRESS = contract_addresses['proxy_factory_V1_0_0'] settings.SAFE_VALID_CONTRACT_ADDRESSES = {settings.SAFE_CONTRACT_ADDRESS, settings.SAFE_V1_0_0_CONTRACT_ADDRESS, settings.SAFE_V0_0_1_CONTRACT_ADDRESS, } cls.safe_contract_address = contract_addresses['safe'] cls.safe_contract = get_safe_contract(cls.w3, cls.safe_contract_address) cls.safe_contract_V1_0_0_address = contract_addresses['safe_V1_0_0'] cls.safe_contract_V1_0_0 = get_safe_V1_0_0_contract(cls.w3, cls.safe_contract_V1_0_0_address) cls.safe_contract_V0_0_1_address = contract_addresses['safe_V0_0_1'] cls.safe_contract_V0_0_1 = get_safe_V1_0_0_contract(cls.w3, cls.safe_contract_V0_0_1_address) cls.proxy_factory_contract_address = contract_addresses['proxy_factory'] cls.proxy_factory_contract = get_proxy_factory_contract(cls.w3, cls.proxy_factory_contract_address) cls.proxy_factory = ProxyFactory(cls.proxy_factory_contract_address, cls.ethereum_client) cls.multi_send_contract = get_multi_send_contract(cls.w3, contract_addresses['multi_send']) cls.multi_send = MultiSend(cls.multi_send_contract.address, cls.ethereum_client)
def test_contract_signature(self): safe_account = self.ethereum_test_account safe = self.deploy_test_safe(owners=[safe_account.address], initial_funding_wei=Web3.toWei(0.01, 'ether')) safe_contract = get_safe_contract(self.ethereum_client.w3, safe.safe_address) safe_tx_hash = Web3.keccak(text='test') signature_r = HexBytes(safe.safe_address.replace('0x', '').rjust(64, '0')) signature_s = HexBytes('0' * 62 + '41') # Position of end of signature signature_v = HexBytes('00') contract_signature = HexBytes('0' * 64) signature = signature_r + signature_s + signature_v + contract_signature safe_signature = SafeContractSignature(signature, safe_tx_hash, self.ethereum_client) self.assertFalse(safe_signature.ok) # Approve the hash tx = safe_contract.functions.approveHash( safe_tx_hash ).buildTransaction({'from': safe_account.address}) self.ethereum_client.send_unsigned_transaction(tx, private_key=safe_account.key) safe_signature = SafeContractSignature(signature, safe_tx_hash, self.ethereum_client) self.assertFalse(safe_signature.ok) # Test with an owner signature safe_tx_hash_2 = Web3.keccak(text='test2') safe_tx_hash_2_message_hash = safe_contract.functions.getMessageHash(safe_tx_hash_2).call() safe_signature = SafeContractSignature(signature, safe_tx_hash_2, self.ethereum_client) self.assertFalse(safe_signature.ok) contract_signature = encode_single('bytes', safe_account.signHash(safe_tx_hash_2_message_hash)['signature']) signature = signature_r + signature_s + signature_v + contract_signature safe_signature = SafeContractSignature(signature, safe_tx_hash_2, self.ethereum_client) self.assertTrue(safe_signature.ok)
def __init__(self): # This safe_tx_failure events allow us to detect a failed safe transaction self.safe_tx_failure_events = [get_safe_V1_0_0_contract(Web3()).events.ExecutionFailed(), get_safe_contract(Web3()).events.ExecutionFailure()] self.safe_tx_failure_events_topics = {event_abi_to_log_topic(event.abi) for event in self.safe_tx_failure_events} self.safe_status_cache: Dict[str, SafeStatus] = {}
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.toWei(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.fromWei(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.getBalance(safe_creation_tx.safe_address), safe_balance) logger.info("Send %d gwei to deployer %s", w3.fromWei(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.getBalance(funder_account.address) # This tx will create the Safe Proxy and return ether to the funder tx_hash = w3.eth.sendRawTransaction(safe_creation_tx.tx_raw) tx_receipt = w3.eth.waitForTransactionReceipt(tx_hash) self.assertEqual(tx_receipt.contractAddress, safe_creation_tx.safe_address) self.assertEqual(w3.eth.getBalance(funder_account.address), funder_balance + safe_creation_tx.payment) logger.info("Deployer account has still %d gwei left (will be lost)", w3.fromWei(w3.eth.getBalance(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 deploy_test_safe( self, number_owners: int = 3, threshold: Optional[int] = None, owners: Optional[List[str]] = None, initial_funding_wei: int = 0, fallback_handler: Optional[str] = None) -> SafeCreate2Tx: owners = owners if owners else [ Account.create().address for _ in range(number_owners) ] if not threshold: threshold = len(owners) - 1 if len(owners) > 1 else 1 safe_creation_tx = self.build_test_safe( threshold=threshold, owners=owners, fallback_handler=fallback_handler) funder_account = self.ethereum_test_account ethereum_tx_sent = self.proxy_factory.deploy_proxy_contract_with_nonce( funder_account, self.safe_contract_address, safe_creation_tx.safe_setup_data, safe_creation_tx.salt_nonce) safe_address = ethereum_tx_sent.contract_address if initial_funding_wei: self.send_ether(safe_address, initial_funding_wei) safe_instance = get_safe_contract(self.w3, safe_address) self.assertEqual(safe_instance.functions.getThreshold().call(), threshold) self.assertEqual(safe_instance.functions.getOwners().call(), owners) self.assertEqual(safe_address, safe_creation_tx.safe_address) return safe_creation_tx
def __init__(self): #TODO Refactor this using inheritance self.dummy_w3 = Web3() exchanges = [ get_uniswap_exchange_contract(self.dummy_w3), self.dummy_w3.eth.contract(abi=gnosis_protocol_abi) ] sight_contracts = [ self.dummy_w3.eth.contract(abi=abi) for abi in (conditional_token_abi, market_maker_abi, market_maker_factory_abi) ] erc_contracts = [ get_erc721_contract(self.dummy_w3), get_erc20_contract(self.dummy_w3) ] safe_contracts = [ get_safe_V0_0_1_contract(self.dummy_w3), get_safe_V1_0_0_contract(self.dummy_w3), get_safe_contract(self.dummy_w3) ] # Order is important. If signature is the same (e.g. renaming of `baseGas`) last elements in the list # will take preference self.supported_contracts = exchanges + sight_contracts + erc_contracts + safe_contracts # Web3 generates possible selectors every time. We cache that and use a dict to do a fast check # Store selectors with abi self.supported_fn_selectors: Dict[bytes, ContractFunction] = {} for supported_contract in self.supported_contracts: self.supported_fn_selectors.update( self._generate_selectors_with_abis_from_contract( supported_contract))
def deploy_master_contract( ethereum_client: EthereumClient, deployer_account: LocalAccount) -> EthereumTxSent: """ Deploy master contract. 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_contract(ethereum_client.w3) constructor_tx = safe_contract.constructor().buildTransaction() tx_hash = ethereum_client.send_unsigned_transaction( constructor_tx, private_key=deployer_account.key) tx_receipt = ethereum_client.get_transaction_receipt(tx_hash, timeout=60) assert tx_receipt assert tx_receipt['status'] ethereum_tx_sent = EthereumTxSent(tx_hash, constructor_tx, tx_receipt['contractAddress']) logger.info("Deployed and initialized Safe Master Contract=%s by %s", ethereum_tx_sent.contract_address, deployer_account.address) return ethereum_tx_sent
def create(ethereum_client: EthereumClient, deployer_account: LocalAccount, master_copy_address: str, owners: List[str], threshold: int, fallback_handler: str = NULL_ADDRESS, proxy_factory_address: Optional[str] = None, payment_token: str = NULL_ADDRESS, payment: int = 0, payment_receiver: str = 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) """ assert owners, 'At least one owner must be set' assert threshold >= len(owners), 'Threshold=%d must be >= %d' % ( threshold, len(owners)) initializer = get_safe_contract( ethereum_client.w3, NULL_ADDRESS ).functions.setup( owners, threshold, NULL_ADDRESS, # Contract address for optional delegate call b'', # Data payload for optional delegate call fallback_handler, # Handler for fallback calls to this contract, payment_token, payment, payment_receiver).buildTransaction({ 'gas': 1, 'gasPrice': 1 })['data'] if proxy_factory_address: proxy_factory = ProxyFactory(proxy_factory_address, ethereum_client) return proxy_factory.deploy_proxy_contract(deployer_account, master_copy_address, initializer=initializer) proxy_contract = get_delegate_constructor_proxy_contract( ethereum_client.w3) tx = proxy_contract.constructor(master_copy_address, initializer).buildTransaction( {'from': deployer_account.address}) tx['gas'] = tx['gas'] * 100000 tx_hash = ethereum_client.send_unsigned_transaction( tx, private_key=deployer_account.key) tx_receipt = ethereum_client.get_transaction_receipt(tx_hash, timeout=60) assert tx_receipt.status contract_address = tx_receipt.contractAddress return EthereumTxSent(tx_hash, tx, contract_address)
def __init__(self): self.dummy_w3 = Web3() self.safe_contracts = [get_safe_V0_0_1_contract(self.dummy_w3), get_safe_V1_0_0_contract(self.dummy_w3), get_safe_contract(self.dummy_w3)] # Order is important. If signature is the same (e.g. renaming of `baseGas`) last elements in the list # will take preference self.supported_contracts = self.safe_contracts
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.fromWei(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.fromWei(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_create2_tx_builder_with_payment_receiver(self): w3 = self.w3 salt_nonce = generate_salt_nonce() payment_receiver = Account.create().address funder_account = self.ethereum_test_account owners = [Account.create().address for _ in range(4)] threshold = len(owners) - 1 gas_price = self.gas_price safe_creation_tx = SafeCreate2TxBuilder( w3=w3, master_copy_address=self.safe_contract_address, proxy_factory_address=self.proxy_factory_contract_address).build( owners=owners, threshold=threshold, salt_nonce=salt_nonce, gas_price=gas_price, payment_receiver=payment_receiver) self.assertEqual(safe_creation_tx.payment, safe_creation_tx.payment_ether) self.send_tx( { 'to': safe_creation_tx.safe_address, 'value': safe_creation_tx.payment, }, funder_account) ethereum_tx_sent = self.proxy_factory.deploy_proxy_contract_with_nonce( funder_account, self.safe_contract_address, safe_creation_tx.safe_setup_data, salt_nonce, gas=safe_creation_tx.gas, gas_price=safe_creation_tx.gas_price) tx_receipt = w3.eth.wait_for_transaction_receipt( ethereum_tx_sent.tx_hash) self.assertEqual(tx_receipt.status, 1) logs = self.proxy_factory_contract.events.ProxyCreation( ).processReceipt(tx_receipt) log = logs[0] self.assertIsNone(tx_receipt.contractAddress) self.assertEqual(log['event'], 'ProxyCreation') proxy_address = log['args']['proxy'] self.assertEqual(proxy_address, safe_creation_tx.safe_address) self.assertEqual(ethereum_tx_sent.contract_address, safe_creation_tx.safe_address) deployed_safe_proxy_contract = get_safe_contract(w3, proxy_address) self.assertEqual( deployed_safe_proxy_contract.functions.getThreshold().call(), threshold) self.assertEqual( deployed_safe_proxy_contract.functions.getOwners().call(), owners) self.assertEqual(self.ethereum_client.get_balance(proxy_address), 0) self.assertEqual(self.ethereum_client.get_balance(payment_receiver), safe_creation_tx.payment)
def w3_tx(self): """ :return: Web3 contract tx prepared for `call`, `transact` or `buildTransaction` """ safe_contract = get_safe_contract(self.w3, address=self.safe_address) return safe_contract.functions.execTransaction( self.to, self.value, self.data, self.operation, self.safe_tx_gas, self.base_gas, self.gas_price, self.gas_token, self.refund_receiver, self.signatures)
def get_supported_abis(self) -> List[ABI]: safe_abis = [get_safe_V0_0_1_contract(self.dummy_w3).abi, get_safe_V1_0_0_contract(self.dummy_w3).abi, get_safe_V1_3_0_contract(self.dummy_w3).abi, get_safe_contract(self.dummy_w3).abi] # Order is important. If signature is the same (e.g. renaming of `baseGas`) last elements in the list # will take preference return safe_abis
def _check_signature(self) -> bool: contract_signature_len = int.from_bytes( self.signature[self.s:self.s + 32], 'big') contract_signature = self.signature[self.s + 32:self.s + 32 + contract_signature_len] safe_contract = get_safe_contract(self.ethereum_client.w3, self.owner) return safe_contract.functions.isValidSignature( self.safe_tx_hash, contract_signature).call() == self.EIP1271_MAGIC_VALUE
def __init__(self, network, address, rpc_endpoint_url, safe_relay_url): """Initializes a Safe. """ self.network = network self.address = address self.w3 = web3.Web3(web3.HTTPProvider(rpc_endpoint_url)) # https://web3py.readthedocs.io/en/stable/middleware.html#geth-style-proof-of-authority self.w3.middleware_stack.inject(geth_poa_middleware, layer=0) self.contract = get_safe_contract(w3=self.w3, address=self.address) self.safe_relay = Relay(safe_relay_url)
def is_valid(self, ethereum_client: EthereumClient, *args) -> bool: safe_contract = get_safe_contract(ethereum_client.w3, self.owner) try: for block_identifier in ('pending', 'latest'): return safe_contract.functions.isValidSignature( self.safe_tx_hash, self.contract_signature).call( block_identifier=block_identifier ) == self.EIP1271_MAGIC_VALUE except BadFunctionCallOutput as e: # Error using `pending` block identifier pass raise e # This should never happen
def is_valid(self, ethereum_client: EthereumClient, safe_address: str) -> bool: safe_contract = get_safe_contract(ethereum_client.w3, safe_address) try: for block_identifier in ('pending', 'latest'): return safe_contract.functions.approvedHashes( self.owner, self.safe_tx_hash).call( block_identifier=block_identifier) == 1 except BadFunctionCallOutput as e: # Error using `pending` block identifier pass raise e # This should never happen
def test_send_previously_approved_tx(self): number_owners = 4 accounts = [self.create_account(initial_ether=0.01) for _ in range(number_owners)] accounts.sort(key=lambda x: x.address.lower()) owners = [account.address for account in accounts] safe_creation = self.deploy_test_safe(threshold=2, owners=owners, initial_funding_wei=self.w3.toWei(0.01, 'ether')) safe_address = safe_creation.safe_address safe = Safe(safe_address, self.ethereum_client) safe_instance = get_safe_contract(self.w3, safe_address) to, _ = get_eth_address_with_key() value = self.w3.toWei(0.001, 'ether') data = b'' operation = 0 safe_tx_gas = 500000 data_gas = 500000 gas_price = 1 gas_token = NULL_ADDRESS refund_receiver = NULL_ADDRESS nonce = safe.retrieve_nonce() self.assertEqual(nonce, 0) safe_tx_hash = safe.build_multisig_tx(to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, safe_nonce=nonce).safe_tx_hash safe_tx_contract_hash = safe_instance.functions.getTransactionHash(to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, nonce).call() self.assertEqual(safe_tx_hash, safe_tx_contract_hash) approve_hash_fn = safe_instance.functions.approveHash(safe_tx_hash) for account in accounts[:2]: self.send_tx(approve_hash_fn.buildTransaction({'from': account.address}), account) for owner in (owners[0], owners[1]): is_approved = safe.retrieve_is_hash_approved(owner, safe_tx_hash) self.assertTrue(is_approved) # Prepare signatures. v must be 1 for previously signed and r the owner signatures = (1, int(owners[0], 16), 0), (1, int(owners[1], 16), 0) signature_bytes = signatures_to_bytes(signatures) safe.send_multisig_tx(to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, signature_bytes, self.ethereum_test_account.key) balance = self.w3.eth.get_balance(to) self.assertEqual(value, balance)
def __init__(self, ethereum_client: EthereumClient): # This safe_tx_failure events allow us to detect a failed safe transaction self.ethereum_client = ethereum_client dummy_w3 = Web3() self.safe_tx_failure_events = [ get_safe_V1_0_0_contract(dummy_w3).events.ExecutionFailed(), get_safe_contract(dummy_w3).events.ExecutionFailure() ] self.safe_tx_module_failure_events = [ get_safe_contract(dummy_w3).events.ExecutionFromModuleFailure() ] self.safe_tx_failure_events_topics = { event_abi_to_log_topic(event.abi) for event in self.safe_tx_failure_events } self.safe_tx_module_failure_topics = { event_abi_to_log_topic(event.abi) for event in self.safe_tx_module_failure_events } self.safe_status_cache: Dict[str, SafeStatus] = {}
def __init__(self): self.dummy_w3 = Web3() # Order is important. If signature is the same (e.g. renaming of `baseGas`) last elements in the list # will take preference self.supported_contracts = [get_safe_V0_0_1_contract(self.dummy_w3), get_safe_V1_0_0_contract(self.dummy_w3), get_safe_contract(self.dummy_w3)] # Web3 generates possible selectors every time. We cache that and use a dict to do a fast check # Store selectors with abi self.supported_fn_selectors: Dict[bytes, ContractFunction] = {} for supported_contract in self.supported_contracts: self.supported_fn_selectors.update(self._generate_selectors_with_abis_from_contract(supported_contract))
def is_version_updated(self) -> bool: """ :return: True if Safe Master Copy is updated, False otherwise """ if self._safe_cli_info.master_copy == LAST_SAFE_CONTRACT: return True else: # Check versions, maybe safe-cli addresses were not updated safe_contract = get_safe_contract(self.ethereum_client.w3, LAST_SAFE_CONTRACT) try: safe_contract_version = safe_contract.functions.VERSION().call() except BadFunctionCallOutput: # Safe master copy is not deployed or errored, maybe custom network return True # We cannot say you are not updated ¯\_(ツ)_/¯ return semantic_version.parse(self.safe_cli_info.version) >= semantic_version.parse(safe_contract_version)
def is_valid(self, ethereum_client: EthereumClient, *args) -> bool: safe_contract = get_safe_contract(ethereum_client.w3, self.owner) for block_identifier in ('pending', 'latest'): try: return safe_contract.functions.isValidSignature( self.safe_tx_hash, self.contract_signature).call( block_identifier=block_identifier ) == self.EIP1271_MAGIC_VALUE except (BadFunctionCallOutput, DecodingError): # Error using `pending` block identifier or contract does not exist logger.warning( 'Cannot check EIP1271 signature from contract %s', self.owner) return False
def is_valid(self, ethereum_client: EthereumClient, safe_address: str) -> bool: safe_contract = get_safe_contract(ethereum_client.w3, safe_address) exception: Exception for block_identifier in ("pending", "latest"): try: return ( safe_contract.functions.approvedHashes( self.owner, self.safe_tx_hash ).call(block_identifier=block_identifier) == 1 ) except BadFunctionCallOutput as e: # Error using `pending` block identifier exception = e raise exception # This should never happen
def test_create_multisig_tx(self): w3 = self.w3 # The balance we will send to the safe safe_balance = w3.toWei(0.02, 'ether') # Create Safe funder_account = self.ethereum_test_account funder = funder_account.address accounts = [self.create_account(), self.create_account()] # Signatures must be sorted! accounts.sort(key=lambda account: account.address.lower()) owners = [x.address for x in accounts] threshold = len(accounts) safe_creation = self.deploy_test_safe(owners=owners, threshold=threshold) my_safe_address = safe_creation.safe_address my_safe_contract = get_safe_contract(w3, my_safe_address) SafeContractFactory(address=my_safe_address) to = funder value = safe_balance // 4 data = HexBytes('') operation = 0 safe_tx_gas = 100000 data_gas = 300000 gas_price = self.transaction_service._get_minimum_gas_price() gas_token = NULL_ADDRESS refund_receiver = NULL_ADDRESS safe = Safe(my_safe_address, self.ethereum_client) nonce = safe.retrieve_nonce() safe_tx = safe.build_multisig_tx(to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, safe_nonce=nonce).safe_tx_hash # Just to make sure we are not miscalculating tx_hash contract_multisig_tx_hash = my_safe_contract.functions.getTransactionHash( to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, nonce).call() self.assertEqual(safe_tx, contract_multisig_tx_hash) signatures = [account.signHash(safe_tx) for account in accounts] # Check owners are the same contract_owners = my_safe_contract.functions.getOwners().call() self.assertEqual(set(contract_owners), set(owners)) invalid_proxy = self.deploy_example_erc20(1, Account.create().address) with self.assertRaises(InvalidProxyContract): SafeContractFactory(address=invalid_proxy.address) self.transaction_service.create_multisig_tx( invalid_proxy.address, to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, nonce, signatures, ) # Use invalid master copy random_master_copy = Account.create().address proxy_create_tx = get_paying_proxy_contract(self.w3).constructor( random_master_copy, b'', NULL_ADDRESS, NULL_ADDRESS, 0).buildTransaction({'from': self.ethereum_test_account.address}) tx_hash = self.ethereum_client.send_unsigned_transaction( proxy_create_tx, private_key=self.ethereum_test_account.key) tx_receipt = self.ethereum_client.get_transaction_receipt(tx_hash, timeout=60) proxy_address = tx_receipt.contractAddress with self.assertRaises(InvalidMasterCopyAddress): SafeContractFactory(address=proxy_address) self.transaction_service.create_multisig_tx( proxy_address, to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, nonce, signatures, ) with self.assertRaises(NotEnoughFundsForMultisigTx): self.transaction_service.create_multisig_tx( my_safe_address, to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, nonce, signatures, ) # Send something to the safe self.send_tx({ 'to': my_safe_address, 'value': safe_balance }, funder_account) bad_refund_receiver = get_eth_address_with_key()[0] with self.assertRaises(InvalidRefundReceiver): self.transaction_service.create_multisig_tx( my_safe_address, to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, bad_refund_receiver, nonce, signatures, ) invalid_gas_price = 0 with self.assertRaises(RefundMustBeEnabled): self.transaction_service.create_multisig_tx( my_safe_address, to, value, data, operation, safe_tx_gas, data_gas, invalid_gas_price, gas_token, refund_receiver, nonce, signatures, ) with self.assertRaises(GasPriceTooLow): self.transaction_service.create_multisig_tx( my_safe_address, to, value, data, operation, safe_tx_gas, data_gas, self.transaction_service._estimate_tx_gas_price( self.transaction_service._get_minimum_gas_price(), gas_token) - 1, gas_token, refund_receiver, nonce, signatures) with self.assertRaises(InvalidGasToken): invalid_gas_token = Account.create().address self.transaction_service.create_multisig_tx( my_safe_address, to, value, data, operation, safe_tx_gas, data_gas, gas_price, invalid_gas_token, refund_receiver, nonce, reversed(signatures)) with self.assertRaises(SignaturesNotSorted): self.transaction_service.create_multisig_tx( my_safe_address, to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, nonce, reversed(signatures)) with self.assertRaises(SignerIsBanned): for account in accounts: BannedSignerFactory(address=account.address) self.transaction_service.create_multisig_tx( my_safe_address, to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, nonce, signatures) BannedSigner.objects.all().delete() self.assertEqual(BannedSigner.objects.count(), 0) sender = self.transaction_service.tx_sender_account.address sender_balance = w3.eth.getBalance(sender) safe_balance = w3.eth.getBalance(my_safe_address) safe_multisig_tx = self.transaction_service.create_multisig_tx( my_safe_address, to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, nonce, signatures, ) with self.assertRaises(SafeMultisigTxExists): self.transaction_service.create_multisig_tx( my_safe_address, to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, nonce, signatures, ) tx_receipt = w3.eth.waitForTransactionReceipt( safe_multisig_tx.ethereum_tx.tx_hash) self.assertTrue(tx_receipt['status']) self.assertEqual(w3.toChecksumAddress(tx_receipt['from']), sender) self.assertEqual(w3.toChecksumAddress(tx_receipt['to']), my_safe_address) self.assertGreater(safe_multisig_tx.ethereum_tx.gas_price, gas_price) # We used minimum gas price sender_new_balance = w3.eth.getBalance(sender) gas_used = tx_receipt['gasUsed'] tx_fees = gas_used * safe_multisig_tx.ethereum_tx.gas_price estimated_refund = ( safe_multisig_tx.data_gas + safe_multisig_tx.safe_tx_gas) * safe_multisig_tx.gas_price real_refund = safe_balance - w3.eth.getBalance(my_safe_address) - value # Real refund can be less if not all the `safe_tx_gas` is used self.assertGreaterEqual(estimated_refund, real_refund) self.assertEqual(sender_new_balance, sender_balance - tx_fees + real_refund) self.assertEqual(safe.retrieve_nonce(), 1) # Send again the tx and check that works nonce += 1 value = 0 safe_tx = safe.build_multisig_tx(to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, safe_nonce=nonce) # Use invalid signatures with self.assertRaises(InvalidOwners): signatures = [ Account.create().signHash(safe_tx.safe_tx_hash) for _ in range(len(accounts)) ] self.transaction_service.create_multisig_tx( my_safe_address, to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, nonce, signatures, ) signatures = [ account.signHash(safe_tx.safe_tx_hash) for account in accounts ] safe_multisig_tx = self.transaction_service.create_multisig_tx( my_safe_address, to, value, data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, nonce, signatures, ) tx_receipt = w3.eth.waitForTransactionReceipt( safe_multisig_tx.ethereum_tx.tx_hash) self.assertTrue(tx_receipt['status'])
def test_safe_create2_tx_builder_v_1_0_0(self): w3 = self.w3 tx_hash = get_safe_V1_0_0_contract(self.w3).constructor().transact( {'from': self.ethereum_test_account.address}) tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) master_copy = tx_receipt['contractAddress'] salt_nonce = generate_salt_nonce() funder_account = self.ethereum_test_account owners = [Account.create().address for _ in range(4)] threshold = len(owners) - 1 gas_price = self.gas_price safe_creation_tx = SafeCreate2TxBuilder( w3=w3, master_copy_address=master_copy, proxy_factory_address=self.proxy_factory_contract_address).build( owners=owners, threshold=threshold, salt_nonce=salt_nonce, gas_price=gas_price) self.assertEqual(safe_creation_tx.payment, safe_creation_tx.payment_ether) self.send_tx( { 'to': safe_creation_tx.safe_address, 'value': safe_creation_tx.payment, }, funder_account) funder_balance = self.ethereum_client.get_balance( funder_account.address) ethereum_tx_sent = self.proxy_factory.deploy_proxy_contract_with_nonce( funder_account, master_copy, safe_creation_tx.safe_setup_data, salt_nonce, safe_creation_tx.gas, safe_creation_tx.gas_price) tx_receipt = w3.eth.wait_for_transaction_receipt( ethereum_tx_sent.tx_hash) self.assertEqual(tx_receipt.status, 1) # Funder balance must be bigger after a Safe deployment, as Safe deployment is a little overpriced self.assertGreater( self.ethereum_client.get_balance(funder_account.address), funder_balance) logs = self.proxy_factory_contract.events.ProxyCreation( ).processReceipt(tx_receipt) log = logs[0] self.assertIsNone(tx_receipt.contractAddress) self.assertEqual(log['event'], 'ProxyCreation') proxy_address = log['args']['proxy'] self.assertEqual(proxy_address, safe_creation_tx.safe_address) self.assertEqual(ethereum_tx_sent.contract_address, safe_creation_tx.safe_address) deployed_safe_proxy_contract = get_safe_contract(w3, proxy_address) self.assertEqual( deployed_safe_proxy_contract.functions.VERSION().call(), '1.0.0') self.assertEqual( deployed_safe_proxy_contract.functions.getThreshold().call(), threshold) self.assertEqual( deployed_safe_proxy_contract.functions.getOwners().call(), owners) self.assertEqual(self.ethereum_client.get_balance(proxy_address), 0)
def test_safe_create2_tx_builder_with_token_payment(self): w3 = self.w3 salt_nonce = generate_salt_nonce() erc20_deployer = Account.create() funder_account = self.ethereum_test_account owners = [Account.create().address for _ in range(4)] 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) # Send something to the erc20 deployer self.send_tx( { 'to': erc20_deployer.address, 'value': w3.toWei(1, 'ether') }, funder_account) safe_creation_tx = SafeCreate2TxBuilder( w3=w3, master_copy_address=self.safe_contract_address, proxy_factory_address=self.proxy_factory_contract_address).build( owners=owners, threshold=threshold, salt_nonce=salt_nonce, gas_price=gas_price, payment_token=erc20_contract.address) self.assertEqual(safe_creation_tx.payment_token, erc20_contract.address) self.assertGreater(safe_creation_tx.payment, 0) self.assertEqual(safe_creation_tx.payment_ether, safe_creation_tx.gas * safe_creation_tx.gas_price) self.send_tx( erc20_contract.functions.transfer( safe_creation_tx.safe_address, safe_creation_tx.payment).buildTransaction( {'from': erc20_deployer.address}), erc20_deployer) self.assertEqual( erc20_contract.functions.balanceOf( safe_creation_tx.safe_address).call(), safe_creation_tx.payment) ethereum_tx_sent = self.proxy_factory.deploy_proxy_contract_with_nonce( funder_account, self.safe_contract_address, safe_creation_tx.safe_setup_data, salt_nonce, gas=safe_creation_tx.gas, gas_price=safe_creation_tx.gas_price) tx_receipt = w3.eth.wait_for_transaction_receipt( ethereum_tx_sent.tx_hash) self.assertEqual(tx_receipt.status, 1) logs = self.proxy_factory_contract.events.ProxyCreation( ).processReceipt(tx_receipt) log = logs[0] self.assertIsNone(tx_receipt.contractAddress) self.assertEqual(log['event'], 'ProxyCreation') proxy_address = log['args']['proxy'] self.assertEqual(proxy_address, safe_creation_tx.safe_address) self.assertEqual(ethereum_tx_sent.contract_address, safe_creation_tx.safe_address) deployed_safe_proxy_contract = get_safe_contract(w3, proxy_address) self.assertEqual( deployed_safe_proxy_contract.functions.getThreshold().call(), threshold) self.assertEqual( deployed_safe_proxy_contract.functions.getOwners().call(), owners) self.assertEqual(self.ethereum_client.get_balance(proxy_address), 0)
def test_send_multisig_tx(self): # Create Safe w3 = self.w3 funder_account = self.ethereum_test_account funder = funder_account.address owners_with_keys = [ get_eth_address_with_key(), get_eth_address_with_key() ] # Signatures must be sorted! owners_with_keys.sort(key=lambda x: x[0].lower()) owners = [x[0] for x in owners_with_keys] keys = [x[1] for x in owners_with_keys] threshold = len(owners_with_keys) safe = self.deploy_test_safe(threshold=threshold, owners=owners) my_safe_address = safe.address # The balance we will send to the safe safe_balance = w3.toWei(0.02, "ether") # Send something to the owner[0], who will be sending the tx owner0_balance = safe_balance self.send_tx({ "to": owners[0], "value": owner0_balance }, funder_account) my_safe_contract = get_safe_contract(w3, my_safe_address) safe = Safe(my_safe_address, self.ethereum_client) to = funder value = safe_balance // 2 data = HexBytes("") operation = 0 safe_tx_gas = 100000 base_gas = 300000 gas_price = 1 gas_token = NULL_ADDRESS refund_receiver = NULL_ADDRESS nonce = None safe_multisig_tx = safe.build_multisig_tx( to=to, value=value, data=data, operation=operation, safe_tx_gas=safe_tx_gas, base_gas=base_gas, gas_price=gas_price, gas_token=gas_token, refund_receiver=refund_receiver, safe_nonce=nonce, ) safe_multisig_tx_hash = safe_multisig_tx.safe_tx_hash nonce = safe.retrieve_nonce() self.assertEqual( safe.build_multisig_tx( to=to, value=value, data=data, operation=operation, safe_tx_gas=safe_tx_gas, base_gas=base_gas, gas_price=gas_price, gas_token=gas_token, refund_receiver=refund_receiver, safe_nonce=nonce, ).safe_tx_hash, safe_multisig_tx_hash, ) # Just to make sure we are not miscalculating tx_hash contract_multisig_tx_hash = my_safe_contract.functions.getTransactionHash( to, value, data, operation, safe_tx_gas, base_gas, gas_price, gas_token, refund_receiver, nonce, ).call() self.assertEqual(safe_multisig_tx_hash, contract_multisig_tx_hash) for private_key in keys: safe_multisig_tx.sign(private_key) signatures = safe_multisig_tx.signatures self.assertEqual(set(safe_multisig_tx.signers), set(owners)) # Check owners are the same contract_owners = my_safe_contract.functions.getOwners().call() self.assertEqual(set(contract_owners), set(owners)) self.assertEqual(w3.eth.get_balance(owners[0]), owner0_balance) # with self.assertRaises(CouldNotPayGasWithEther): # Ganache v7 does not raise CouldNotPayGasWithEther anymore with self.assertRaises(InvalidInternalTx): safe.send_multisig_tx( to, value, data, operation, safe_tx_gas, base_gas, gas_price, gas_token, refund_receiver, signatures, tx_sender_private_key=keys[0], tx_gas_price=self.gas_price, ) # Send something to the safe self.send_tx({ "to": my_safe_address, "value": safe_balance }, funder_account) ethereum_tx_sent = safe.send_multisig_tx( to, value, data, operation, safe_tx_gas, base_gas, gas_price, gas_token, refund_receiver, signatures, tx_sender_private_key=keys[0], tx_gas_price=self.gas_price, ) tx_receipt = w3.eth.wait_for_transaction_receipt( ethereum_tx_sent.tx_hash) self.assertTrue(tx_receipt["status"]) owner0_new_balance = w3.eth.get_balance(owners[0]) gas_used = tx_receipt["gasUsed"] gas_cost = gas_used * self.gas_price estimated_payment = (base_gas + gas_used) * gas_price real_payment = owner0_new_balance - (owner0_balance - gas_cost) # Estimated payment will be bigger, because it uses all the tx gas. Real payment only uses gas left # in the point of calculation of the payment, so it will be slightly lower self.assertTrue(estimated_payment > real_payment > 0) self.assertTrue(owner0_new_balance > owner0_balance - ethereum_tx_sent.tx["gas"] * self.gas_price) self.assertEqual(safe.retrieve_nonce(), 1)
def get_contract(self): return get_safe_contract(self.w3, address=self.address)
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.toWei(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).buildTransaction({'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.fromWei(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.getBalance(funder) # This tx will create the Safe Proxy and return tokens to the funder tx_hash = w3.eth.sendRawTransaction(safe_creation_tx.tx_raw) tx_receipt = w3.eth.waitForTransactionReceipt(tx_hash) self.assertEqual(tx_receipt.contractAddress, safe_address) self.assertEqual(w3.eth.getBalance(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.fromWei(w3.eth.getBalance(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)