def test_deploy_proxy_contract_with_nonce(self): salt_nonce = generate_salt_nonce() owners = [Account.create().address for _ in range(2)] threshold = 2 payment_token = None safe_create2_tx = Safe.build_safe_create2_tx( self.ethereum_client, self.safe_contract_address, self.proxy_factory_contract_address, salt_nonce, owners, threshold, self.gas_price, payment_token) # Send ether for safe deploying costs self.send_tx( { 'to': safe_create2_tx.safe_address, 'value': safe_create2_tx.payment }, self.ethereum_test_account) proxy_factory = ProxyFactory(self.proxy_factory_contract_address, self.ethereum_client) ethereum_tx_sent = proxy_factory.deploy_proxy_contract_with_nonce( self.ethereum_test_account, safe_create2_tx.master_copy_address, safe_create2_tx.safe_setup_data, salt_nonce, safe_create2_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(ethereum_tx_sent.contract_address, safe_create2_tx.safe_address) self.assertEqual(set(safe.retrieve_owners()), set(owners)) self.assertEqual(safe.retrieve_master_copy_address(), safe_create2_tx.master_copy_address)
def test_change_master_copy(self): safe_operator = self.setup_operator() safe = Safe(safe_operator.address, self.ethereum_client) current_master_copy = safe.retrieve_master_copy_address() with self.assertRaises(SameMasterCopyException): safe_operator.change_master_copy(current_master_copy) random_address = Account.create().address with self.assertRaises(InvalidMasterCopyException): safe_operator.change_master_copy(random_address) self.assertTrue( safe_operator.change_master_copy(self.safe_old_contract_address)) self.assertEqual(safe_operator.safe_cli_info.master_copy, self.safe_old_contract_address) self.assertEqual(safe.retrieve_master_copy_address(), self.safe_old_contract_address)
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)
def test_safe_creation(self): salt_nonce = generate_salt_nonce() owners = [Account.create().address for _ in range(2)] data = { 'saltNonce': salt_nonce, 'owners': owners, 'threshold': len(owners) } response = self.client.post(reverse('v3:safe-creation'), data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) response_json = response.json() safe_address = response_json['safe'] self.assertTrue(check_checksum(safe_address)) self.assertTrue(check_checksum(response_json['paymentReceiver'])) self.assertEqual(response_json['paymentToken'], NULL_ADDRESS) self.assertEqual(int(response_json['payment']), int(response_json['gasEstimated']) * int(response_json['gasPriceEstimated'])) self.assertGreater(int(response_json['gasEstimated']), 0) self.assertGreater(int(response_json['gasPriceEstimated']), 0) self.assertGreater(len(response_json['setupData']), 2) self.assertEqual(response_json['masterCopy'], settings.SAFE_CONTRACT_ADDRESS) self.assertTrue(SafeContract.objects.filter(address=safe_address)) self.assertTrue(SafeCreation2.objects.filter(owners__contains=[owners[0]])) safe_creation = SafeCreation2.objects.get(safe=safe_address) self.assertEqual(safe_creation.payment_token, None) # Payment includes deployment gas + gas to send eth to the deployer self.assertEqual(safe_creation.payment, safe_creation.wei_estimated_deploy_cost()) # Deploy the Safe to check it self.send_ether(safe_address, int(response_json['payment'])) safe_creation2 = SafeCreationServiceProvider().deploy_create2_safe_tx(safe_address) self.ethereum_client.get_transaction_receipt(safe_creation2.tx_hash, timeout=20) safe = Safe(safe_address, self.ethereum_client) self.assertEqual(safe.retrieve_master_copy_address(), response_json['masterCopy']) self.assertEqual(safe.retrieve_owners(), owners) # Test exception when same Safe is created response = self.client.post(reverse('v3:safe-creation'), data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) self.assertIn('SafeAlreadyExistsException', response.json()['exception']) data = { 'salt_nonce': -1, 'owners': owners, 'threshold': 2 } response = self.client.post(reverse('v3:safe-creation'), data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
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 ) 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 _send_multisig_tx(self, safe_address: str, to: str, value: int, data: bytes, operation: int, safe_tx_gas: int, base_gas: int, gas_price: int, gas_token: str, refund_receiver: str, safe_nonce: int, signatures: bytes, block_identifier='latest') -> Tuple[bytes, bytes, Dict[str, Any]]: """ This function calls the `send_multisig_tx` of the Safe, but has some limitations to prevent abusing the relay :return: Tuple(tx_hash, safe_tx_hash, tx) :raises: InvalidMultisigTx: If user tx cannot go through the Safe """ safe = Safe(safe_address, self.ethereum_client) data = data or b'' gas_token = gas_token or NULL_ADDRESS refund_receiver = refund_receiver or NULL_ADDRESS to = to or NULL_ADDRESS # Make sure refund receiver is set to 0x0 so that the contract refunds the gas costs to tx.origin if not self._check_refund_receiver(refund_receiver): raise InvalidRefundReceiver(refund_receiver) self._check_safe_gas_price(gas_token, gas_price) # Make sure proxy contract is ours if not self.proxy_factory.check_proxy_code(safe_address): raise InvalidProxyContract(safe_address) # Make sure master copy is valid safe_master_copy_address = safe.retrieve_master_copy_address() if safe_master_copy_address not in self.safe_valid_contract_addresses: raise InvalidMasterCopyAddress(safe_master_copy_address) # Check enough funds to pay for the gas if not safe.check_funds_for_tx_gas(safe_tx_gas, base_gas, gas_price, gas_token): raise NotEnoughFundsForMultisigTx threshold = safe.retrieve_threshold() number_signatures = len(signatures) // 65 # One signature = 65 bytes if number_signatures < threshold: raise SignaturesNotFound('Need at least %d signatures' % threshold) safe_tx_gas_estimation = safe.estimate_tx_gas(to, value, data, operation) safe_base_gas_estimation = safe.estimate_tx_base_gas(to, value, data, operation, gas_token, safe_tx_gas_estimation) if safe_tx_gas < safe_tx_gas_estimation or base_gas < safe_base_gas_estimation: raise InvalidGasEstimation("Gas should be at least equal to safe-tx-gas=%d and base-gas=%d. Current is " "safe-tx-gas=%d and base-gas=%d" % (safe_tx_gas_estimation, safe_base_gas_estimation, safe_tx_gas, base_gas)) # We use fast tx gas price, if not txs could be stuck tx_gas_price = self._get_configured_gas_price() tx_sender_private_key = self.tx_sender_account.key tx_sender_address = Account.from_key(tx_sender_private_key).address safe_tx = safe.build_multisig_tx( to, value, data, operation, safe_tx_gas, base_gas, gas_price, gas_token, refund_receiver, signatures, safe_nonce=safe_nonce, safe_version=safe.retrieve_version() ) owners = safe.retrieve_owners() signers = safe_tx.signers if set(signers) - set(owners): # All the signers must be owners raise InvalidOwners('Signers=%s are not valid owners of the safe. Owners=%s', safe_tx.signers, owners) if signers != safe_tx.sorted_signers: raise SignaturesNotSorted('Safe-tx-hash=%s - Signatures are not sorted by owner: %s' % (safe_tx.safe_tx_hash.hex(), safe_tx.signers)) if banned_signers := BannedSigner.objects.filter(address__in=signers): raise SignerIsBanned(f'Signers {list(banned_signers)} are banned')
def _send_multisig_tx(self, safe_address: str, to: str, value: int, data: bytes, operation: int, safe_tx_gas: int, base_gas: int, gas_price: int, gas_token: str, refund_receiver: str, safe_nonce: int, signatures: bytes, tx_gas=None, block_identifier='latest') -> Tuple[bytes, bytes, Dict[str, Any]]: """ This function calls the `send_multisig_tx` of the Safe, but has some limitations to prevent abusing the relay :return: Tuple(tx_hash, safe_tx_hash, tx) :raises: InvalidMultisigTx: If user tx cannot go through the Safe """ safe = Safe(safe_address, self.ethereum_client) data = data or b'' gas_token = gas_token or NULL_ADDRESS refund_receiver = refund_receiver or NULL_ADDRESS to = to or NULL_ADDRESS # Make sure refund receiver is set to 0x0 so that the contract refunds the gas costs to tx.origin if not self._check_refund_receiver(refund_receiver): raise InvalidRefundReceiver(refund_receiver) self._check_safe_gas_price(gas_token, gas_price) # Make sure proxy contract is ours if not self.proxy_factory.check_proxy_code(safe_address): raise InvalidProxyContract(safe_address) # Make sure master copy is valid safe_master_copy_address = safe.retrieve_master_copy_address() if safe_master_copy_address not in self.safe_valid_contract_addresses: raise InvalidMasterCopyAddress(safe_master_copy_address) # Check enough funds to pay for the gas if not safe.check_funds_for_tx_gas(safe_tx_gas, base_gas, gas_price, gas_token): raise NotEnoughFundsForMultisigTx threshold = safe.retrieve_threshold() number_signatures = len(signatures) // 65 # One signature = 65 bytes if number_signatures < threshold: raise SignaturesNotFound('Need at least %d signatures' % threshold) safe_tx_gas_estimation = safe.estimate_tx_gas(to, value, data, operation) safe_base_gas_estimation = safe.estimate_tx_base_gas(to, value, data, operation, gas_token, safe_tx_gas_estimation) if safe_tx_gas < safe_tx_gas_estimation or base_gas < safe_base_gas_estimation: raise InvalidGasEstimation("Gas should be at least equal to safe-tx-gas=%d and data-gas=%d. Current is " "safe-tx-gas=%d and data-gas=%d" % (safe_tx_gas_estimation, safe_base_gas_estimation, safe_tx_gas, base_gas)) # Use user provided gasPrice for TX if more than our stardard gas price standard_gas = self._get_configured_gas_price() if gas_price > standard_gas : tx_gas_price = gas_price else: tx_gas_price = standard_gas tx_sender_private_key = self.tx_sender_account.privateKey tx_sender_address = Account.privateKeyToAccount(tx_sender_private_key).address safe_tx = safe.build_multisig_tx( to, value, data, operation, safe_tx_gas, base_gas, gas_price, gas_token, refund_receiver, signatures, safe_nonce=safe_nonce, safe_version=safe.retrieve_version() ) if safe_tx.signers != safe_tx.sorted_signers: raise SignaturesNotSorted('Safe-tx-hash=%s - Signatures are not sorted by owner: %s' % (safe_tx.safe_tx_hash, safe_tx.signers)) safe_tx.call(tx_sender_address=tx_sender_address, block_identifier=block_identifier) logger.info('_send_multisig_tx about to execute tx for safe=%s and nonce=%s', safe_address, safe_nonce) with EthereumNonceLock(self.redis, self.ethereum_client, self.tx_sender_account.address, timeout=60 * 2) as tx_nonce: logger.info('_send_multisig_tx executing tx for safe=%s and nonce=%s', safe_address, safe_nonce) tx_hash, tx = safe_tx.execute(tx_sender_private_key, tx_gas=tx_gas, tx_gas_price=tx_gas_price, tx_nonce=tx_nonce, block_identifier=block_identifier) return tx_hash, safe_tx.tx_hash, tx
def _send_multisig_tx(self, safe_address: str, to: str, value: int, data: bytes, operation: int, safe_tx_gas: int, base_gas: int, gas_price: int, gas_token: str, refund_receiver: str, safe_nonce: int, signatures: bytes, tx_gas: Optional[int] = None, block_identifier='latest') -> Tuple[bytes, bytes, Dict[str, Any]]: """ This function calls the `send_multisig_tx` of the Safe, but has some limitations to prevent abusing the relay :return: Tuple(tx_hash, safe_tx_hash, tx) :raises: InvalidMultisigTx: If user tx cannot go through the Safe """ safe = Safe(safe_address, self.ethereum_client) data = data or b'' gas_token = gas_token or NULL_ADDRESS refund_receiver = refund_receiver or NULL_ADDRESS to = to or NULL_ADDRESS # Make sure refund receiver is set to 0x0 so that the contract refunds the gas costs to tx.origin if not self._check_refund_receiver(refund_receiver): raise InvalidRefundReceiver(refund_receiver) self._check_safe_gas_price(gas_token, gas_price) # Make sure proxy contract is ours if not self.proxy_factory.check_proxy_code(safe_address): raise InvalidProxyContract(safe_address) # Make sure master copy is valid safe_master_copy_address = safe.retrieve_master_copy_address() if safe_master_copy_address not in self.safe_valid_contract_addresses: raise InvalidMasterCopyAddress(safe_master_copy_address) # Check enough funds to pay for the gas if not safe.check_funds_for_tx_gas(safe_tx_gas, base_gas, gas_price, gas_token): raise NotEnoughFundsForMultisigTx threshold = safe.retrieve_threshold() number_signatures = len(signatures) // 65 # One signature = 65 bytes if number_signatures < threshold: raise SignaturesNotFound('Need at least %d signatures' % threshold) safe_tx_gas_estimation = safe.estimate_tx_gas(to, value, data, operation) safe_base_gas_estimation = safe.estimate_tx_base_gas(to, value, data, operation, gas_token, safe_tx_gas_estimation) if safe_tx_gas < safe_tx_gas_estimation or base_gas < safe_base_gas_estimation: raise InvalidGasEstimation("Gas should be at least equal to safe-tx-gas=%d and base-gas=%d. Current is " "safe-tx-gas=%d and base-gas=%d" % (safe_tx_gas_estimation, safe_base_gas_estimation, safe_tx_gas, base_gas)) # We use fast tx gas price, if not txs could be stuck tx_gas_price = self._get_configured_gas_price() tx_sender_private_key = self.tx_sender_account.key tx_sender_address = Account.from_key(tx_sender_private_key).address safe_tx = safe.build_multisig_tx( to, value, data, operation, safe_tx_gas, base_gas, gas_price, gas_token, refund_receiver, signatures, safe_nonce=safe_nonce, safe_version=safe.retrieve_version() ) if safe_tx.signers != safe_tx.sorted_signers: raise SignaturesNotSorted('Safe-tx-hash=%s - Signatures are not sorted by owner: %s' % (safe_tx.safe_tx_hash.hex(), safe_tx.signers)) logger.info('Safe=%s safe-nonce=%d Check `call()` before sending transaction', safe_address, safe_nonce) # Set `gasLimit` for `call()`. It will use the same that it will be used later for execution tx_gas = safe_tx.base_gas + safe_tx.safe_tx_gas + 25000 safe_tx.call(tx_gas=tx_gas, tx_sender_address=tx_sender_address, block_identifier=block_identifier) logger.info('Safe=%s safe-nonce=%d `call()` was successful', safe_address, safe_nonce) with EthereumNonceLock(self.redis, self.ethereum_client, self.tx_sender_account.address, lock_timeout=60 * 2) as tx_nonce: tx_hash, tx = safe_tx.execute(tx_sender_private_key, tx_gas=tx_gas, tx_gas_price=tx_gas_price, tx_nonce=tx_nonce, block_identifier=block_identifier) logger.info('Safe=%s, Sent transaction with nonce=%d tx-hash=%s for safe-tx-hash=%s safe-nonce=%d', safe_address, tx_nonce, tx_hash.hex(), safe_tx.safe_tx_hash.hex(), safe_tx.safe_nonce) return tx_hash, safe_tx.safe_tx_hash, tx