def test_decode_execute_transaction(self): owners = [Account.create() for _ in range(2)] owner_addresses = [owner.address for owner in owners] threshold = 1 safe_creation = self.deploy_test_safe(owners=owner_addresses, threshold=threshold, initial_funding_wei=self.w3.toWei(0.1, 'ether')) safe_address = safe_creation.safe_address to = Account().create().address value = self.w3.toWei(0.01, 'ether') safe_tx_gas = 200000 data_gas = 100000 safe_tx = SafeTx(self.ethereum_client, safe_address, to, value, b'', 0, safe_tx_gas, data_gas, self.gas_price, None, None, safe_nonce=0) safe_tx.sign(owners[0].privateKey) self.assertEqual(safe_tx.call(tx_sender_address=self.ethereum_test_account.address), 1) tx_hash, _ = safe_tx.execute(tx_sender_private_key=self.ethereum_test_account.privateKey) self.ethereum_client.get_transaction_receipt(tx_hash, timeout=60) self.assertEqual(self.ethereum_client.get_balance(to), value) tx_decoder = TxDecoder() function_name, arguments = tx_decoder.decode_transaction(safe_tx.tx['data']) self.assertEqual(function_name, 'execTransaction') self.assertIn('baseGas', arguments)
def send_safe_tx(self, safe_address: str, safe_version: str, accounts: List[Account], payment_token: Optional[str] = None, wait_for_receipt: bool = True) -> bytes: safe_balance = self.w3.eth.getBalance(safe_address) tx = { 'to': self.main_account.address, 'value': safe_balance, 'data': None, 'operation': 0, # CALL 'gasToken': payment_token, } if payment_token: tx['gasToken'] = payment_token # We used payment * 2 to fund the safe, now we return ether to the main account r = requests.post(self.get_estimate_url(safe_address), json=tx) assert r.ok, "Estimate not working %s" % r.content self.stdout.write(self.style.SUCCESS('Estimation=%s for tx=%s' % (r.json(), tx))) # estimate_gas = r.json()['safeTxGas'] + r.json()['dataGas'] + r.json()['operationalGas'] # fees = r.json()['gasPrice'] * estimate_gas if payment_token: # We can transfer the full amount as we are paying fees with a token tx['value'] = safe_balance else: estimate_gas = r.json()['safeTxGas'] + r.json()['dataGas'] + r.json()['operationalGas'] fees = r.json()['gasPrice'] * estimate_gas tx['value'] = safe_balance - fees tx['dataGas'] = r.json()['dataGas'] + r.json()['operationalGas'] tx['gasPrice'] = r.json()['gasPrice'] tx['safeTxGas'] = r.json()['safeTxGas'] tx['nonce'] = 0 if r.json()['lastUsedNonce'] is None else r.json()['lastUsedNonce'] + 1 tx['refundReceiver'] = None # Sign the tx safe_tx_hash = SafeTx(None, safe_address, tx['to'], tx['value'], tx['data'], tx['operation'], tx['safeTxGas'], tx['dataGas'], tx['gasPrice'], tx['gasToken'], tx['refundReceiver'], safe_nonce=tx['nonce'], safe_version=safe_version).safe_tx_hash signatures = [account.signHash(safe_tx_hash) for account in accounts[:2]] curated_signatures = [{'r': signature['r'], 's': signature['s'], 'v': signature['v']} for signature in signatures] tx['signatures'] = curated_signatures self.stdout.write(self.style.SUCCESS('Sending multisig tx to return some funds to the main owner %s' % tx)) r = requests.post(self.get_tx_url(safe_address), json=tx) assert r.ok, "Error sending tx %s" % r.content multisig_tx_hash = r.json()['txHash'] self.stdout.write(self.style.SUCCESS('Tx with tx-hash=%s was successful' % multisig_tx_hash)) if wait_for_receipt: self.w3.eth.waitForTransactionReceipt(multisig_tx_hash, timeout=500) return multisig_tx_hash
def test_decode_old_execute_transaction(self): safe_address = Account.create().address to = Account().create().address value = self.w3.toWei(0.01, 'ether') safe_tx_gas = 200000 data_gas = 100000 safe_tx = SafeTx(self.ethereum_client, safe_address, to, value, b'', 0, safe_tx_gas, data_gas, self.gas_price, None, None, safe_nonce=0, safe_version='0.0.1') tx_decoder = TxDecoder() data = safe_tx.w3_tx.buildTransaction()['data'] function_name, arguments = tx_decoder.decode_transaction(data) self.assertEqual(function_name, 'execTransaction') # self.assertIn('dataGas', arguments) self.assertIn('baseGas', arguments) # Signature of the tx is the same
def get_safe_tx(self, ethereum_client: EthereumClient) -> SafeTx: return SafeTx( ethereum_client, self.safe_id, self.to, self.value, self.data.tobytes() if self.data else b"", self.operation, self.safe_tx_gas, self.data_gas, self.gas_price, self.gas_token, self.refund_receiver, signatures=self.signatures.tobytes() if self.signatures else b"", safe_nonce=self.nonce, )
def get_safe_transaction( self, safe_tx_hash: bytes ) -> Tuple[SafeTx, Optional[HexBytes]]: """ :param safe_tx_hash: :return: SafeTx and `tx-hash` if transaction was executed """ safe_tx_hash = HexBytes(safe_tx_hash).hex() response = self._get_request(f"/api/v1/multisig-transactions/{safe_tx_hash}/") if not response.ok: raise BaseAPIException( f"Cannot get transaction with safe-tx-hash={safe_tx_hash}: {response.content}" ) else: result = response.json() # TODO return tx-hash if executed signatures = self.parse_signatures(result) return ( SafeTx( self.ethereum_client, result["safe"], result["to"], int(result["value"]), HexBytes(result["data"]) if result["data"] else b"", int(result["operation"]), int(result["safeTxGas"]), int(result["baseGas"]), int(result["gasPrice"]), result["gasToken"], result["refundReceiver"], signatures=signatures if signatures else b"", safe_nonce=int(result["nonce"]), ), HexBytes(result["transactionHash"]) if result["transactionHash"] else None, )
def test_safe_multisig_tx_post_gas_token(self): # Create Safe ------------------------------------------------ w3 = self.ethereum_client.w3 safe_balance = w3.toWei(0.01, 'ether') owner_account = self.create_account() owner = owner_account.address threshold = 1 safe_creation = self.deploy_test_safe(owners=[owner], threshold=threshold, initial_funding_wei=safe_balance) my_safe_address = safe_creation.safe_address self.assertEqual(self.w3.eth.getBalance(my_safe_address), safe_balance) SafeContractFactory(address=my_safe_address) # Get tokens for the safe safe_token_balance = int(1e18) erc20_contract = self.deploy_example_erc20(safe_token_balance, my_safe_address) # Safe prepared -------------------------------------------- to = Account.create().address value = safe_balance tx_data = None operation = SafeOperation.CALL.value refund_receiver = None nonce = 0 gas_token = erc20_contract.address data = { "to": to, "value": value, "data": tx_data, "operation": operation, "gasToken": gas_token } # Get estimation for gas. Token does not exist response = self.client.post(reverse('v1:safe-multisig-tx-estimate', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) self.assertEqual('InvalidGasToken: %s' % gas_token, response.json()['exception']) # Create token token_model = TokenFactory(address=gas_token) response = self.client.post(reverse('v1:safe-multisig-tx-estimate', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) estimation_json = response.json() safe_tx_gas = estimation_json['safeTxGas'] + estimation_json[ 'operationalGas'] data_gas = estimation_json['dataGas'] gas_price = estimation_json['gasPrice'] gas_token = estimation_json['gasToken'] multisig_tx_hash = SafeTx(None, my_safe_address, to, value, tx_data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, safe_nonce=nonce).safe_tx_hash signatures = [ w3.eth.account.signHash(multisig_tx_hash, private_key) for private_key in [owner_account.key] ] signatures_json = [{ 'v': s['v'], 'r': s['r'], 's': s['s'] } for s in signatures] data = { "to": to, "value": value, "data": tx_data, "operation": operation, "safe_tx_gas": safe_tx_gas, "data_gas": data_gas, "gas_price": gas_price, "gas_token": gas_token, "nonce": nonce, "signatures": signatures_json } response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) tx_hash = response.json()['transactionHash'][2:] # Remove leading 0x safe_multisig_tx = SafeMultisigTx.objects.get( ethereum_tx__tx_hash=tx_hash) self.assertEqual(safe_multisig_tx.to, to) self.assertEqual(safe_multisig_tx.value, value) self.assertEqual(safe_multisig_tx.data, tx_data) self.assertEqual(safe_multisig_tx.operation, operation) self.assertEqual(safe_multisig_tx.safe_tx_gas, safe_tx_gas) self.assertEqual(safe_multisig_tx.data_gas, data_gas) self.assertEqual(safe_multisig_tx.gas_price, gas_price) self.assertEqual(safe_multisig_tx.gas_token, gas_token) self.assertEqual(safe_multisig_tx.nonce, nonce)
def test_safe_multisig_tx_post(self): # Create Safe ------------------------------------------------ w3 = self.ethereum_client.w3 safe_balance = w3.toWei(0.01, 'ether') 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, initial_funding_wei=safe_balance) my_safe_address = safe_creation.safe_address SafeContractFactory(address=my_safe_address) self.assertEqual(self.ethereum_client.get_balance(my_safe_address), safe_balance) # Safe prepared -------------------------------------------- to = Account.create().address value = safe_balance // 2 tx_data = None operation = SafeOperation.CALL.value data = { "to": to, "value": value, "data": tx_data, "operation": operation, } # Get estimation for gas response = self.client.post(reverse('v1:safe-multisig-tx-estimate', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) estimation_json = response.json() safe_tx_gas = estimation_json['safeTxGas'] + estimation_json[ 'operationalGas'] data_gas = estimation_json['dataGas'] gas_price = estimation_json['gasPrice'] gas_token = estimation_json['gasToken'] refund_receiver = None nonce = 0 multisig_tx_hash = SafeTx(None, my_safe_address, to, value, tx_data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, safe_nonce=nonce).safe_tx_hash signatures = [ account.signHash(multisig_tx_hash) for account in accounts ] signatures_json = [{ 'v': s['v'], 'r': s['r'], 's': s['s'] } for s in signatures] data = { "to": to, "value": value, "data": tx_data, "operation": operation, "safe_tx_gas": safe_tx_gas, "data_gas": data_gas, "gas_price": gas_price, "gas_token": gas_token, "nonce": nonce, "signatures": signatures_json } response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) tx_hash = response.json()['transactionHash'][2:] # Remove leading 0x safe_multisig_tx = SafeMultisigTx.objects.get( ethereum_tx__tx_hash=tx_hash) self.assertEqual(safe_multisig_tx.to, to) self.assertEqual(safe_multisig_tx.value, value) self.assertEqual(safe_multisig_tx.data, tx_data) self.assertEqual(safe_multisig_tx.operation, operation) self.assertEqual(safe_multisig_tx.safe_tx_gas, safe_tx_gas) self.assertEqual(safe_multisig_tx.data_gas, data_gas) self.assertEqual(safe_multisig_tx.gas_price, gas_price) self.assertEqual(safe_multisig_tx.gas_token, None) self.assertEqual(safe_multisig_tx.nonce, nonce) signature_pairs = [(s['v'], s['r'], s['s']) for s in signatures] signatures_packed = signatures_to_bytes(signature_pairs) self.assertEqual(bytes(safe_multisig_tx.signatures), signatures_packed) # Send the same tx again response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) self.assertTrue('exists' in response.data['exception']) # Send with a Safe not created via the service safe_creation = self.deploy_test_safe(owners=owners, threshold=threshold, initial_funding_wei=safe_balance) my_safe_address = safe_creation.safe_address multisig_tx_hash = SafeTx(None, my_safe_address, to, value, tx_data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, safe_nonce=nonce).safe_tx_hash signatures = [ account.signHash(multisig_tx_hash) for account in accounts ] signatures_json = [{ 'v': s['v'], 'r': s['r'], 's': s['s'] } for s in signatures] data['signatures'] = signatures_json with self.assertRaises(SafeContract.DoesNotExist): SafeContract.objects.get(address=my_safe_address) response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertTrue( SafeContract.objects.filter(address=my_safe_address).exists()) self.assertEqual( SafeMultisigTx.objects.filter(safe_id=my_safe_address).count(), 1) # Send the same tx again response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) self.assertTrue('exists' in response.data['exception']) self.assertEqual( SafeMultisigTx.objects.filter(safe_id=my_safe_address).count(), 1) # Send tx with not existing Safe my_safe_address = Account.create().address response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) self.assertTrue('InvalidProxyContract' in response.data['exception'])
def __process_decoded_transaction(self, internal_tx_decoded: InternalTxDecoded) -> bool: """ Decode internal tx and creates needed models :param internal_tx_decoded: InternalTxDecoded to process. It will be set as `processed` :return: True if tx could be processed, False otherwise """ function_name = internal_tx_decoded.function_name arguments = internal_tx_decoded.arguments internal_tx = internal_tx_decoded.internal_tx contract_address = internal_tx._from master_copy = internal_tx.to processed_successfully = True if function_name == 'setup' and contract_address != NULL_ADDRESS: logger.debug('Processing Safe setup') owners = arguments['_owners'] threshold = arguments['_threshold'] fallback_handler = arguments.get('fallbackHandler', NULL_ADDRESS) nonce = 0 try: safe_contract: SafeContract = SafeContract.objects.get(address=contract_address) if not safe_contract.ethereum_tx_id or not safe_contract.erc20_block_number: safe_contract.ethereum_tx = internal_tx.ethereum_tx safe_contract.erc20_block_number = internal_tx.ethereum_tx.block_id safe_contract.save(update_fields=['ethereum_tx', 'erc20_block_number']) except SafeContract.DoesNotExist: SafeContract.objects.create(address=contract_address, ethereum_tx=internal_tx.ethereum_tx, erc20_block_number=max(internal_tx.ethereum_tx.block_id - 5760, 0)) logger.info('Found new Safe=%s', contract_address) SafeStatus.objects.create(internal_tx=internal_tx, address=contract_address, owners=owners, threshold=threshold, nonce=nonce, master_copy=master_copy, fallback_handler=fallback_handler) elif function_name in ('addOwnerWithThreshold', 'removeOwner', 'removeOwnerWithThreshold'): logger.debug('Processing owner/threshold modification') safe_status = self.get_last_safe_status_for_address(contract_address) safe_status.threshold = arguments['_threshold'] owner = arguments['owner'] try: if function_name == 'addOwnerWithThreshold': safe_status.owners.append(owner) else: # removeOwner, removeOwnerWithThreshold safe_status.owners.remove(owner) except ValueError: logger.error('Error processing trace=%s for contract=%s with tx-hash=%s', internal_tx.trace_address, contract_address, internal_tx.ethereum_tx_id) self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'swapOwner': logger.debug('Processing owner swap') old_owner = arguments['oldOwner'] new_owner = arguments['newOwner'] safe_status = self.get_last_safe_status_for_address(contract_address) safe_status.owners.remove(old_owner) safe_status.owners.append(new_owner) self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'changeThreshold': logger.debug('Processing threshold change') safe_status = self.get_last_safe_status_for_address(contract_address) safe_status.threshold = arguments['_threshold'] self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'changeMasterCopy': logger.debug('Processing master copy change') # TODO Ban address if it doesn't have a valid master copy safe_status = self.get_last_safe_status_for_address(contract_address) safe_status.master_copy = arguments['_masterCopy'] self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'setFallbackHandler': logger.debug('Setting FallbackHandler') safe_status = self.get_last_safe_status_for_address(contract_address) safe_status.fallback_handler = arguments['handler'] self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'enableModule': logger.debug('Enabling Module') safe_status = self.get_last_safe_status_for_address(contract_address) safe_status.enabled_modules.append(arguments['module']) self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'disableModule': logger.debug('Disabling Module') safe_status = self.get_last_safe_status_for_address(contract_address) safe_status.enabled_modules.remove(arguments['module']) self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'execTransactionFromModule': logger.debug('Executing Tx from Module') ethereum_tx = internal_tx.ethereum_tx module_internal_tx = internal_tx.get_previous_module_trace() module_address = module_internal_tx.to if module_internal_tx else NULL_ADDRESS module_data = HexBytes(arguments['data']) failed = self.is_module_failed(ethereum_tx, module_address) ModuleTransaction.objects.get_or_create( internal_tx=internal_tx, defaults={ 'created': internal_tx.ethereum_tx.block.timestamp, 'safe': contract_address, 'module': module_address, 'to': arguments['to'], 'value': arguments['value'], 'data': module_data if module_data else None, 'operation': arguments['operation'], 'failed': failed, } ) elif function_name == 'approveHash': logger.debug('Processing hash approval') multisig_transaction_hash = arguments['hashToApprove'] ethereum_tx = internal_tx.ethereum_tx # TODO Check previous trace is not a delegate call owner = internal_tx.get_previous_trace()._from safe_signature = SafeSignatureApprovedHash.build_for_owner(owner, multisig_transaction_hash) (multisig_confirmation, _) = MultisigConfirmation.objects.get_or_create(multisig_transaction_hash=multisig_transaction_hash, owner=owner, defaults={ 'ethereum_tx': ethereum_tx, 'signature': safe_signature.export_signature(), 'signature_type': safe_signature.signature_type.value, }) if not multisig_confirmation.ethereum_tx_id: multisig_confirmation.ethereum_tx = ethereum_tx multisig_confirmation.save(update_fields=['ethereum_tx']) elif function_name == 'execTransaction': logger.debug('Processing transaction execution') safe_status = self.get_last_safe_status_for_address(contract_address) nonce = safe_status.nonce if 'baseGas' in arguments: # `dataGas` was renamed to `baseGas` in v1.0.0 base_gas = arguments['baseGas'] safe_version = '1.0.0' else: base_gas = arguments['dataGas'] safe_version = '0.0.1' safe_tx = SafeTx(None, contract_address, arguments['to'], arguments['value'], arguments['data'], arguments['operation'], arguments['safeTxGas'], base_gas, arguments['gasPrice'], arguments['gasToken'], arguments['refundReceiver'], HexBytes(arguments['signatures']), safe_nonce=nonce, safe_version=safe_version) safe_tx_hash = safe_tx.safe_tx_hash ethereum_tx = internal_tx.ethereum_tx # Remove existing transaction with same nonce in case of bad indexing (one of the master copies can be # outdated and a tx with a wrong nonce could be indexed) # MultisigTransaction.objects.filter( # ethereum_tx=ethereum_tx, # nonce=safe_tx.safe_nonce, # safe=contract_address # ).exclude( # safe_tx_hash=safe_tx_hash # ).delete() # Remove old txs not used # MultisigTransaction.objects.filter( # ethereum_tx=None, # nonce__lt=safe_tx.safe_nonce, # safe=contract_address # ).delete() failed = self.is_failed(ethereum_tx, safe_tx_hash) multisig_tx, _ = MultisigTransaction.objects.get_or_create( safe_tx_hash=safe_tx_hash, defaults={ 'created': internal_tx.ethereum_tx.block.timestamp, 'safe': contract_address, 'ethereum_tx': ethereum_tx, 'to': safe_tx.to, 'value': safe_tx.value, 'data': safe_tx.data if safe_tx.data else None, 'operation': safe_tx.operation, 'safe_tx_gas': safe_tx.safe_tx_gas, 'base_gas': safe_tx.base_gas, 'gas_price': safe_tx.gas_price, 'gas_token': safe_tx.gas_token, 'refund_receiver': safe_tx.refund_receiver, 'nonce': safe_tx.safe_nonce, 'signatures': safe_tx.signatures, 'failed': failed, 'trusted': True, }) if not multisig_tx.ethereum_tx_id: multisig_tx.ethereum_tx = ethereum_tx multisig_tx.failed = failed multisig_tx.signatures = HexBytes(arguments['signatures']) multisig_tx.trusted = True multisig_tx.save(update_fields=['ethereum_tx', 'failed', 'signatures', 'trusted']) for safe_signature in SafeSignature.parse_signature(safe_tx.signatures, safe_tx_hash): multisig_confirmation, _ = MultisigConfirmation.objects.get_or_create( multisig_transaction_hash=safe_tx_hash, owner=safe_signature.owner, defaults={ 'ethereum_tx': None, 'multisig_transaction': multisig_tx, 'signature': safe_signature.export_signature(), 'signature_type': safe_signature.signature_type.value, } ) if multisig_confirmation.signature != safe_signature.signature: multisig_confirmation.signature = safe_signature.export_signature() multisig_confirmation.signature_type = safe_signature.signature_type.value multisig_confirmation.save(update_fields=['signature', 'signature_type']) safe_status.nonce = nonce + 1 self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'execTransactionFromModule': logger.debug('Not processing execTransactionFromModule') # No side effects or nonce increasing, but trace will be set as processed else: processed_successfully = False logger.debug('End processing') return processed_successfully
def send_multiple_txs( self, safe_address: str, safe_version: str, accounts: List[Account], payment_token: Optional[str] = None, number_txs: int = 100, ) -> List[bytes]: tx_hash = self.ethereum_client.send_eth_to( self.main_account.key, safe_address, self.ethereum_client.w3.eth.gasPrice, self.w3.toWei(1, "ether"), nonce=self.main_account_nonce, ) self.main_account_nonce += 1 self.stdout.write( self.style.SUCCESS( "Sent 1 ether for testing sending multiple txs, " "waiting for receipt with tx-hash=%s" % tx_hash.hex())) self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=500) self.stdout.write( self.style.SUCCESS("Sending %d txs of 1 wei" % number_txs)) safe_nonce = None tx_hashes = [] for _ in range(number_txs): tx = { "to": self.main_account.address, "value": 1, # Send 1 wei "data": None, "operation": 0, # CALL "gasToken": payment_token, } if payment_token: tx["gasToken"] = payment_token # We used payment * 2 to fund the safe, now we return ether to the main account r = requests.post(self.get_estimate_url(safe_address), json=tx) assert r.ok, "Estimate not working %s" % r.content self.stdout.write( self.style.SUCCESS("Estimation=%s for tx=%s" % (r.json(), tx))) # estimate_gas = r.json()['safeTxGas'] + r.json()['dataGas'] + r.json()['operationalGas'] # fees = r.json()['gasPrice'] * estimate_gas tx["dataGas"] = r.json()["dataGas"] tx["gasPrice"] = r.json()["gasPrice"] tx["safeTxGas"] = r.json()["safeTxGas"] if safe_nonce is not None: safe_nonce += 1 else: safe_nonce = (0 if r.json()["lastUsedNonce"] is None else r.json()["lastUsedNonce"] + 1) tx["nonce"] = safe_nonce tx["refundReceiver"] = None # Sign the tx safe_tx_hash = SafeTx( None, safe_address, tx["to"], tx["value"], tx["data"], tx["operation"], tx["safeTxGas"], tx["dataGas"], tx["gasPrice"], tx["gasToken"], tx["refundReceiver"], tx["nonce"], safe_version=safe_version, ).safe_tx_hash signatures = [ account.signHash(safe_tx_hash) for account in accounts[:2] ] curated_signatures = [{ "r": signature["r"], "s": signature["s"], "v": signature["v"] } for signature in signatures] tx["signatures"] = curated_signatures self.stdout.write( self.style.SUCCESS("Sending tx to stress test the server %s" % tx)) r = requests.post(self.get_tx_url(safe_address), json=tx) assert r.ok, "Error sending tx %s" % r.content tx_hash = r.json()["txHash"] tx_hashes.append(tx_hash) tx_receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=500) assert tx_receipt.status == 1, "Error with tx %s" % tx_hash.hex() for tx_hash in tx_hashes: tx_receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=500) assert tx_receipt.status == 1, "Error with tx %s" % tx_hash.hex() self.stdout.write( self.style.SUCCESS("Tx with tx-hash=%s was successful" % tx_hash)) return tx_hashes
def send_safe_tx( self, safe_address: str, safe_version: str, accounts: List[Account], payment_token: Optional[str] = None, wait_for_receipt: bool = True, ) -> bytes: safe_balance = self.w3.eth.get_balance(safe_address) tx = { "to": self.main_account.address, "value": safe_balance, "data": None, "operation": 0, # CALL "gasToken": payment_token, } if payment_token: tx["gasToken"] = payment_token # We used payment * 2 to fund the safe, now we return ether to the main account r = requests.post(self.get_estimate_url(safe_address), json=tx) assert r.ok, "Estimate not working %s" % r.content self.stdout.write( self.style.SUCCESS("Estimation=%s for tx=%s" % (r.json(), tx))) # estimate_gas = r.json()['safeTxGas'] + r.json()['dataGas'] + r.json()['operationalGas'] # fees = r.json()['gasPrice'] * estimate_gas if payment_token: # We can transfer the full amount as we are paying fees with a token tx["value"] = safe_balance else: estimate_gas = (r.json()["safeTxGas"] + r.json()["dataGas"] + r.json()["operationalGas"]) fees = r.json()["gasPrice"] * estimate_gas tx["value"] = safe_balance - fees tx["dataGas"] = r.json()["dataGas"] + r.json()["operationalGas"] tx["gasPrice"] = r.json()["gasPrice"] tx["safeTxGas"] = r.json()["safeTxGas"] tx["nonce"] = (0 if r.json()["lastUsedNonce"] is None else r.json()["lastUsedNonce"] + 1) tx["refundReceiver"] = None # Sign the tx safe_tx_hash = SafeTx( None, safe_address, tx["to"], tx["value"], tx["data"], tx["operation"], tx["safeTxGas"], tx["dataGas"], tx["gasPrice"], tx["gasToken"], tx["refundReceiver"], safe_nonce=tx["nonce"], safe_version=safe_version, ).safe_tx_hash signatures = [ account.signHash(safe_tx_hash) for account in accounts[:2] ] curated_signatures = [{ "r": signature["r"], "s": signature["s"], "v": signature["v"] } for signature in signatures] tx["signatures"] = curated_signatures self.stdout.write( self.style.SUCCESS( "Sending multisig tx to return some funds to the main owner %s" % tx)) r = requests.post(self.get_tx_url(safe_address), json=tx) assert r.ok, "Error sending tx %s" % r.content multisig_tx_hash = r.json()["txHash"] self.stdout.write( self.style.SUCCESS("Tx with tx-hash=%s was successful" % multisig_tx_hash)) if wait_for_receipt: self.w3.eth.wait_for_transaction_receipt(multisig_tx_hash, timeout=500) return multisig_tx_hash
def __process_decoded_transaction( self, internal_tx_decoded: InternalTxDecoded) -> bool: """ Decode internal tx and creates needed models :param internal_tx_decoded: InternalTxDecoded to process. It will be set as `processed` :return: True if tx could be processed, False otherwise """ function_name = internal_tx_decoded.function_name arguments = internal_tx_decoded.arguments internal_tx = internal_tx_decoded.internal_tx contract_address = internal_tx._from master_copy = internal_tx.to if internal_tx.gas_used < 1000: # When calling a non existing function, fallback of the proxy does not return any error but we can detect # this kind of functions due to little gas used. Some of this transactions get decoded as they were # valid in old versions of the proxies, like changes to `setup` return False processed_successfully = True logger.debug( 'Start processing InternalTxDecoded in tx-hash=%s', HexBytes(internal_tx_decoded.internal_tx.ethereum_tx_id).hex()) if function_name == 'setup' and contract_address != NULL_ADDRESS: logger.debug('Processing Safe setup') owners = arguments['_owners'] threshold = arguments['_threshold'] fallback_handler = arguments.get('fallbackHandler', NULL_ADDRESS) nonce = 0 try: safe_contract: SafeContract = SafeContract.objects.get( address=contract_address) if not safe_contract.ethereum_tx_id or not safe_contract.erc20_block_number: safe_contract.ethereum_tx = internal_tx.ethereum_tx safe_contract.erc20_block_number = internal_tx.ethereum_tx.block_id safe_contract.save( update_fields=['ethereum_tx', 'erc20_block_number']) except SafeContract.DoesNotExist: blocks_one_day = int(24 * 60 * 60 / 15) # 15 seconds block SafeContract.objects.create( address=contract_address, ethereum_tx=internal_tx.ethereum_tx, erc20_block_number=max( internal_tx.ethereum_tx.block_id - blocks_one_day, 0)) logger.info('Found new Safe=%s', contract_address) SafeStatus.objects.create(internal_tx=internal_tx, address=contract_address, owners=owners, threshold=threshold, nonce=nonce, master_copy=master_copy, fallback_handler=fallback_handler) self.clear_cache(contract_address) elif function_name in ('addOwnerWithThreshold', 'removeOwner', 'removeOwnerWithThreshold'): logger.debug('Processing owner/threshold modification') safe_status = self.get_last_safe_status_for_address( contract_address) safe_status.threshold = arguments['_threshold'] owner = arguments['owner'] if function_name == 'addOwnerWithThreshold': safe_status.owners.append(owner) else: # removeOwner, removeOwnerWithThreshold self.remove_owner(internal_tx, safe_status, owner) self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'swapOwner': logger.debug('Processing owner swap') old_owner = arguments['oldOwner'] new_owner = arguments['newOwner'] safe_status = self.get_last_safe_status_for_address( contract_address) self.remove_owner(internal_tx, safe_status, old_owner) safe_status.owners.append(new_owner) self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'changeThreshold': logger.debug('Processing threshold change') safe_status = self.get_last_safe_status_for_address( contract_address) safe_status.threshold = arguments['_threshold'] self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'changeMasterCopy': logger.debug('Processing master copy change') # TODO Ban address if it doesn't have a valid master copy safe_status = self.get_last_safe_status_for_address( contract_address) safe_status.master_copy = arguments['_masterCopy'] self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'setFallbackHandler': logger.debug('Setting FallbackHandler') safe_status = self.get_last_safe_status_for_address( contract_address) safe_status.fallback_handler = arguments['handler'] self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'setGuard': safe_status = self.get_last_safe_status_for_address( contract_address) safe_status.guard = arguments[ 'guard'] if arguments['guard'] != NULL_ADDRESS else None if safe_status.guard: logger.debug('Setting Guard') else: logger.debug('Unsetting Guard') self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'enableModule': logger.debug('Enabling Module') safe_status = self.get_last_safe_status_for_address( contract_address) safe_status.enabled_modules.append(arguments['module']) self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'disableModule': logger.debug('Disabling Module') safe_status = self.get_last_safe_status_for_address( contract_address) safe_status.enabled_modules.remove(arguments['module']) self.store_new_safe_status(safe_status, internal_tx) elif function_name in { 'execTransactionFromModule', 'execTransactionFromModuleReturnData' }: logger.debug('Executing Tx from Module') # TODO Add test with previous traces for processing a module transaction ethereum_tx = internal_tx.ethereum_tx # Someone calls Module -> Module calls Safe Proxy -> Safe Proxy delegate calls Master Copy # The trace that is been processed is the last one, so indexer needs to go at least 2 traces back previous_trace = self.ethereum_client.parity.get_previous_trace( internal_tx.ethereum_tx_id, internal_tx.trace_address_as_list, number_traces=2, skip_delegate_calls=True) if not previous_trace: message = f'Cannot find previous trace for tx-hash={HexBytes(internal_tx.ethereum_tx_id).hex()} and ' \ f'trace-address={internal_tx.trace_address}' logger.warning(message) raise ValueError(message) module_internal_tx = InternalTx.objects.build_from_trace( previous_trace, internal_tx.ethereum_tx) module_address = module_internal_tx.to if module_internal_tx else NULL_ADDRESS module_data = HexBytes(arguments['data']) failed = self.is_module_failed(ethereum_tx, module_address, contract_address) ModuleTransaction.objects.get_or_create( internal_tx=internal_tx, defaults={ 'created': internal_tx.ethereum_tx.block.timestamp, 'safe': contract_address, 'module': module_address, 'to': arguments['to'], 'value': arguments['value'], 'data': module_data if module_data else None, 'operation': arguments['operation'], 'failed': failed, }) elif function_name == 'approveHash': logger.debug('Processing hash approval') multisig_transaction_hash = arguments['hashToApprove'] ethereum_tx = internal_tx.ethereum_tx previous_trace = self.ethereum_client.parity.get_previous_trace( internal_tx.ethereum_tx_id, internal_tx.trace_address_as_list, skip_delegate_calls=True) if not previous_trace: message = f'Cannot find previous trace for tx-hash={HexBytes(internal_tx.ethereum_tx_id).hex()} and ' \ f'trace-address={internal_tx.trace_address}' logger.warning(message) raise ValueError(message) previous_internal_tx = InternalTx.objects.build_from_trace( previous_trace, internal_tx.ethereum_tx) owner = previous_internal_tx._from safe_signature = SafeSignatureApprovedHash.build_for_owner( owner, multisig_transaction_hash) (multisig_confirmation, _) = MultisigConfirmation.objects.get_or_create( multisig_transaction_hash=multisig_transaction_hash, owner=owner, defaults={ 'created': internal_tx.ethereum_tx.block.timestamp, 'ethereum_tx': ethereum_tx, 'signature': safe_signature.export_signature(), 'signature_type': safe_signature.signature_type.value, }) if not multisig_confirmation.ethereum_tx_id: multisig_confirmation.ethereum_tx = ethereum_tx multisig_confirmation.save(update_fields=['ethereum_tx']) elif function_name == 'execTransaction': logger.debug('Processing transaction execution') safe_status = self.get_last_safe_status_for_address( contract_address) nonce = safe_status.nonce if 'baseGas' in arguments: # `dataGas` was renamed to `baseGas` in v1.0.0 base_gas = arguments['baseGas'] safe_version = '1.0.0' else: base_gas = arguments['dataGas'] safe_version = '0.0.1' safe_tx = SafeTx(None, contract_address, arguments['to'], arguments['value'], arguments['data'], arguments['operation'], arguments['safeTxGas'], base_gas, arguments['gasPrice'], arguments['gasToken'], arguments['refundReceiver'], HexBytes(arguments['signatures']), safe_nonce=nonce, safe_version=safe_version) safe_tx_hash = safe_tx.safe_tx_hash ethereum_tx = internal_tx.ethereum_tx # Remove existing transaction with same nonce in case of bad indexing (one of the master copies can be # outdated and a tx with a wrong nonce could be indexed) # MultisigTransaction.objects.filter( # ethereum_tx=ethereum_tx, # nonce=safe_tx.safe_nonce, # safe=contract_address # ).exclude( # safe_tx_hash=safe_tx_hash # ).delete() # Remove old txs not used # MultisigTransaction.objects.filter( # ethereum_tx=None, # nonce__lt=safe_tx.safe_nonce, # safe=contract_address # ).delete() failed = self.is_failed(ethereum_tx, safe_tx_hash) multisig_tx, _ = MultisigTransaction.objects.get_or_create( safe_tx_hash=safe_tx_hash, defaults={ 'created': internal_tx.ethereum_tx.block.timestamp, 'safe': contract_address, 'ethereum_tx': ethereum_tx, 'to': safe_tx.to, 'value': safe_tx.value, 'data': safe_tx.data if safe_tx.data else None, 'operation': safe_tx.operation, 'safe_tx_gas': safe_tx.safe_tx_gas, 'base_gas': safe_tx.base_gas, 'gas_price': safe_tx.gas_price, 'gas_token': safe_tx.gas_token, 'refund_receiver': safe_tx.refund_receiver, 'nonce': safe_tx.safe_nonce, 'signatures': safe_tx.signatures, 'failed': failed, 'trusted': True, }) if not multisig_tx.ethereum_tx_id: multisig_tx.ethereum_tx = ethereum_tx multisig_tx.failed = failed multisig_tx.signatures = HexBytes(arguments['signatures']) multisig_tx.trusted = True multisig_tx.save(update_fields=[ 'ethereum_tx', 'failed', 'signatures', 'trusted' ]) for safe_signature in SafeSignature.parse_signature( safe_tx.signatures, safe_tx_hash): multisig_confirmation, _ = MultisigConfirmation.objects.get_or_create( multisig_transaction_hash=safe_tx_hash, owner=safe_signature.owner, defaults={ 'created': internal_tx.ethereum_tx.block.timestamp, 'ethereum_tx': None, 'multisig_transaction': multisig_tx, 'signature': safe_signature.export_signature(), 'signature_type': safe_signature.signature_type.value, }) if multisig_confirmation.signature != safe_signature.signature: multisig_confirmation.signature = safe_signature.export_signature( ) multisig_confirmation.signature_type = safe_signature.signature_type.value multisig_confirmation.save( update_fields=['signature', 'signature_type']) safe_status.nonce = nonce + 1 self.store_new_safe_status(safe_status, internal_tx) elif function_name == 'execTransactionFromModule': logger.debug('Not processing execTransactionFromModule') # No side effects or nonce increasing, but trace will be set as processed else: processed_successfully = False logger.debug('End processing') return processed_successfully
def send_multiple_txs(self, safe_address: str, safe_version: str, accounts: List[Account], payment_token: Optional[str] = None, number_txs: int = 100) -> List[bytes]: tx_hash = send_eth(self.w3, self.main_account, safe_address, self.w3.toWei(1, 'ether'), nonce=self.main_account_nonce) self.main_account_nonce += 1 self.stdout.write( self.style.SUCCESS( 'Sent 1 ether for testing sending multiple txs, ' 'waiting for receipt with tx-hash=%s' % tx_hash.hex())) self.w3.eth.waitForTransactionReceipt(tx_hash, timeout=500) self.stdout.write( self.style.SUCCESS('Sending %d txs of 1 wei' % number_txs)) safe_nonce = None tx_hashes = [] for _ in range(number_txs): tx = { 'to': self.main_account.address, 'value': 1, # Send 1 wei 'data': None, 'operation': 0, # CALL 'gasToken': payment_token, } if payment_token: tx['gasToken'] = payment_token # We used payment * 2 to fund the safe, now we return ether to the main account r = requests.post(self.get_estimate_url(safe_address), json=tx) assert r.ok, "Estimate not working %s" % r.content self.stdout.write( self.style.SUCCESS('Estimation=%s for tx=%s' % (r.json(), tx))) # estimate_gas = r.json()['safeTxGas'] + r.json()['dataGas'] + r.json()['operationalGas'] # fees = r.json()['gasPrice'] * estimate_gas tx['dataGas'] = r.json()['dataGas'] tx['gasPrice'] = r.json()['gasPrice'] tx['safeTxGas'] = r.json()['safeTxGas'] if safe_nonce is not None: safe_nonce += 1 else: safe_nonce = 0 if r.json( )['lastUsedNonce'] is None else r.json()['lastUsedNonce'] + 1 tx['nonce'] = safe_nonce tx['refundReceiver'] = None # Sign the tx safe_tx_hash = SafeTx(None, safe_address, tx['to'], tx['value'], tx['data'], tx['operation'], tx['safeTxGas'], tx['dataGas'], tx['gasPrice'], tx['gasToken'], tx['refundReceiver'], tx['nonce'], safe_version=safe_version).safe_tx_hash signatures = [ account.signHash(safe_tx_hash) for account in accounts[:2] ] curated_signatures = [{ 'r': signature['r'], 's': signature['s'], 'v': signature['v'] } for signature in signatures] tx['signatures'] = curated_signatures self.stdout.write( self.style.SUCCESS('Sending tx to stress test the server %s' % tx)) r = requests.post(self.get_tx_url(safe_address), json=tx) assert r.ok, "Error sending tx %s" % r.content tx_hash = r.json()['txHash'] tx_hashes.append(tx_hash) tx_receipt = self.w3.eth.waitForTransactionReceipt(tx_hash, timeout=500) assert tx_receipt.status == 1, 'Error with tx %s' % tx_hash.hex() for tx_hash in tx_hashes: tx_receipt = self.w3.eth.waitForTransactionReceipt(tx_hash, timeout=500) assert tx_receipt.status == 1, 'Error with tx %s' % tx_hash.hex() self.stdout.write( self.style.SUCCESS('Tx with tx-hash=%s was successful' % tx_hash)) return tx_hashes
def __process_decoded_transaction( self, internal_tx_decoded: InternalTxDecoded) -> bool: """ Decode internal tx and creates needed models :param internal_tx_decoded: InternalTxDecoded to process. It will be set as `processed` :return: True if tx could be processed, False otherwise """ internal_tx = internal_tx_decoded.internal_tx logger.debug( "Start processing InternalTxDecoded in tx-hash=%s", HexBytes(internal_tx_decoded.internal_tx.ethereum_tx_id).hex(), ) if internal_tx.gas_used < 1000: # When calling a non existing function, fallback of the proxy does not return any error but we can detect # this kind of functions due to little gas used. Some of this transactions get decoded as they were # valid in old versions of the proxies, like changes to `setup` logger.debug( "Calling a non existing function, will not process it", ) return False function_name = internal_tx_decoded.function_name arguments = internal_tx_decoded.arguments contract_address = internal_tx._from master_copy = internal_tx.to processed_successfully = True if function_name == "setup" and contract_address != NULL_ADDRESS: # Index new Safes logger.debug("Processing Safe setup") owners = arguments["_owners"] threshold = arguments["_threshold"] fallback_handler = arguments.get("fallbackHandler", NULL_ADDRESS) nonce = 0 try: safe_contract: SafeContract = SafeContract.objects.get( address=contract_address) if (not safe_contract.ethereum_tx_id or not safe_contract.erc20_block_number): safe_contract.ethereum_tx = internal_tx.ethereum_tx safe_contract.erc20_block_number = internal_tx.block_number safe_contract.save( update_fields=["ethereum_tx", "erc20_block_number"]) except SafeContract.DoesNotExist: blocks_one_day = int(24 * 60 * 60 / 15) # 15 seconds block SafeContract.objects.create( address=contract_address, ethereum_tx=internal_tx.ethereum_tx, erc20_block_number=max( internal_tx.block_number - blocks_one_day, 0), ) logger.info("Found new Safe=%s", contract_address) self.store_new_safe_status( SafeLastStatus( internal_tx=internal_tx, address=contract_address, owners=owners, threshold=threshold, nonce=nonce, master_copy=master_copy, fallback_handler=fallback_handler, ), internal_tx, ) else: safe_last_status = self.get_last_safe_status_for_address( contract_address) if not safe_last_status: # Usually this happens from Safes coming from a not supported Master Copy # TODO When archive node is available, build SafeStatus from blockchain status logger.debug( "Cannot process trace as `SafeLastStatus` is not found for Safe=%s", contract_address, ) processed_successfully = False elif function_name in ( "addOwnerWithThreshold", "removeOwner", "removeOwnerWithThreshold", ): logger.debug("Processing owner/threshold modification") safe_last_status.threshold = (arguments["_threshold"] or safe_last_status.threshold ) # Event doesn't have threshold owner = arguments["owner"] if function_name == "addOwnerWithThreshold": safe_last_status.owners.insert(0, owner) else: # removeOwner, removeOwnerWithThreshold self.swap_owner(internal_tx, safe_last_status, owner, None) self.store_new_safe_status(safe_last_status, internal_tx) elif function_name == "swapOwner": logger.debug("Processing owner swap") old_owner = arguments["oldOwner"] new_owner = arguments["newOwner"] self.swap_owner(internal_tx, safe_last_status, old_owner, new_owner) self.store_new_safe_status(safe_last_status, internal_tx) elif function_name == "changeThreshold": logger.debug("Processing threshold change") safe_last_status.threshold = arguments["_threshold"] self.store_new_safe_status(safe_last_status, internal_tx) elif function_name == "changeMasterCopy": logger.debug("Processing master copy change") # TODO Ban address if it doesn't have a valid master copy old_safe_version = self.get_safe_version_from_master_copy( safe_last_status.master_copy) safe_last_status.master_copy = arguments["_masterCopy"] new_safe_version = self.get_safe_version_from_master_copy( safe_last_status.master_copy) if (old_safe_version and new_safe_version and self.is_version_breaking_signatures( old_safe_version, new_safe_version)): # Transactions queued not executed are not valid anymore MultisigTransaction.objects.queued( contract_address).delete() self.store_new_safe_status(safe_last_status, internal_tx) elif function_name == "setFallbackHandler": logger.debug("Setting FallbackHandler") safe_last_status.fallback_handler = arguments["handler"] self.store_new_safe_status(safe_last_status, internal_tx) elif function_name == "setGuard": safe_last_status.guard = (arguments["guard"] if arguments["guard"] != NULL_ADDRESS else None) if safe_last_status.guard: logger.debug("Setting Guard") else: logger.debug("Unsetting Guard") self.store_new_safe_status(safe_last_status, internal_tx) elif function_name == "enableModule": logger.debug("Enabling Module") safe_last_status.enabled_modules.append(arguments["module"]) self.store_new_safe_status(safe_last_status, internal_tx) elif function_name == "disableModule": logger.debug("Disabling Module") safe_last_status.enabled_modules.remove(arguments["module"]) self.store_new_safe_status(safe_last_status, internal_tx) elif function_name in { "execTransactionFromModule", "execTransactionFromModuleReturnData", }: logger.debug("Executing Tx from Module") # TODO Add test with previous traces for processing a module transaction ethereum_tx = internal_tx.ethereum_tx if "module" in arguments: # L2 Safe with event SafeModuleTransaction indexed using events module_address = arguments["module"] else: # Regular Safe indexed using tracing # Someone calls Module -> Module calls Safe Proxy -> Safe Proxy delegate calls Master Copy # The trace that is been processed is the last one, so indexer needs to get the previous trace previous_trace = self.ethereum_client.parity.get_previous_trace( internal_tx.ethereum_tx_id, internal_tx.trace_address_as_list, skip_delegate_calls=True, ) if not previous_trace: message = ( f"Cannot find previous trace for tx-hash={HexBytes(internal_tx.ethereum_tx_id).hex()} " f"and trace-address={internal_tx.trace_address}") logger.warning(message) raise ValueError(message) module_internal_tx = InternalTx.objects.build_from_trace( previous_trace, internal_tx.ethereum_tx) module_address = (module_internal_tx._from if module_internal_tx else NULL_ADDRESS) failed = self.is_module_failed(ethereum_tx, module_address, contract_address) module_data = HexBytes(arguments["data"]) ModuleTransaction.objects.get_or_create( internal_tx=internal_tx, defaults={ "created": internal_tx.timestamp, "safe": contract_address, "module": module_address, "to": arguments["to"], "value": arguments["value"], "data": module_data if module_data else None, "operation": arguments["operation"], "failed": failed, }, ) elif function_name == "approveHash": logger.debug("Processing hash approval") multisig_transaction_hash = arguments["hashToApprove"] ethereum_tx = internal_tx.ethereum_tx if "owner" in arguments: # Event approveHash owner = arguments["owner"] else: previous_trace = self.ethereum_client.parity.get_previous_trace( internal_tx.ethereum_tx_id, internal_tx.trace_address_as_list, skip_delegate_calls=True, ) if not previous_trace: message = ( f"Cannot find previous trace for tx-hash={HexBytes(internal_tx.ethereum_tx_id).hex()} and " f"trace-address={internal_tx.trace_address}") logger.warning(message) raise ValueError(message) previous_internal_tx = InternalTx.objects.build_from_trace( previous_trace, internal_tx.ethereum_tx) owner = previous_internal_tx._from safe_signature = SafeSignatureApprovedHash.build_for_owner( owner, multisig_transaction_hash) (multisig_confirmation, _) = MultisigConfirmation.objects.get_or_create( multisig_transaction_hash=multisig_transaction_hash, owner=owner, defaults={ "created": internal_tx.timestamp, "ethereum_tx": ethereum_tx, "signature": safe_signature.export_signature(), "signature_type": safe_signature.signature_type.value, }, ) if not multisig_confirmation.ethereum_tx_id: multisig_confirmation.ethereum_tx = ethereum_tx multisig_confirmation.save(update_fields=["ethereum_tx"]) elif function_name == "execTransaction": logger.debug("Processing transaction execution") # Events for L2 Safes store information about nonce nonce = (arguments["nonce"] if "nonce" in arguments else safe_last_status.nonce) if ("baseGas" in arguments ): # `dataGas` was renamed to `baseGas` in v1.0.0 base_gas = arguments["baseGas"] safe_version = (self.get_safe_version_from_master_copy( safe_last_status.master_copy) or "1.0.0") else: base_gas = arguments["dataGas"] safe_version = "0.0.1" safe_tx = SafeTx( None, contract_address, arguments["to"], arguments["value"], arguments["data"], arguments["operation"], arguments["safeTxGas"], base_gas, arguments["gasPrice"], arguments["gasToken"], arguments["refundReceiver"], HexBytes(arguments["signatures"]), safe_nonce=nonce, safe_version=safe_version, chain_id=self.ethereum_client.get_chain_id(), ) safe_tx_hash = safe_tx.safe_tx_hash ethereum_tx = internal_tx.ethereum_tx failed = self.is_failed(ethereum_tx, safe_tx_hash) multisig_tx, _ = MultisigTransaction.objects.get_or_create( safe_tx_hash=safe_tx_hash, defaults={ "created": internal_tx.timestamp, "safe": contract_address, "ethereum_tx": ethereum_tx, "to": safe_tx.to, "value": safe_tx.value, "data": safe_tx.data if safe_tx.data else None, "operation": safe_tx.operation, "safe_tx_gas": safe_tx.safe_tx_gas, "base_gas": safe_tx.base_gas, "gas_price": safe_tx.gas_price, "gas_token": safe_tx.gas_token, "refund_receiver": safe_tx.refund_receiver, "nonce": safe_tx.safe_nonce, "signatures": safe_tx.signatures, "failed": failed, "trusted": True, }, ) # Don't modify created if not multisig_tx.ethereum_tx_id: multisig_tx.ethereum_tx = ethereum_tx multisig_tx.failed = failed multisig_tx.signatures = HexBytes(arguments["signatures"]) multisig_tx.trusted = True multisig_tx.save(update_fields=[ "ethereum_tx", "failed", "signatures", "trusted" ]) for safe_signature in SafeSignature.parse_signature( safe_tx.signatures, safe_tx_hash): ( multisig_confirmation, _, ) = MultisigConfirmation.objects.get_or_create( multisig_transaction_hash=safe_tx_hash, owner=safe_signature.owner, defaults={ "created": internal_tx.timestamp, "ethereum_tx": None, "multisig_transaction": multisig_tx, "signature": safe_signature.export_signature(), "signature_type": safe_signature.signature_type.value, }, ) if multisig_confirmation.signature != safe_signature.signature: multisig_confirmation.signature = ( safe_signature.export_signature()) multisig_confirmation.signature_type = ( safe_signature.signature_type.value) multisig_confirmation.save( update_fields=["signature", "signature_type"]) safe_last_status.nonce = nonce + 1 self.store_new_safe_status(safe_last_status, internal_tx) elif function_name == "execTransactionFromModule": logger.debug("Not processing execTransactionFromModule") # No side effects or nonce increasing, but trace will be set as processed else: processed_successfully = False logger.warning( "Cannot process InternalTxDecoded function_name=%s and arguments=%s", function_name, arguments, ) logger.debug("End processing") return processed_successfully
def process_decoded_transaction(self, internal_tx_decoded: InternalTxDecoded) -> bool: """ Decode internal tx and creates needed models :param internal_tx_decoded: InternalTxDecoded to process. It will be set as `processed` :return: True if tx could be processed, False otherwise """ function_name = internal_tx_decoded.function_name arguments = internal_tx_decoded.arguments internal_tx = internal_tx_decoded.internal_tx contract_address = internal_tx._from master_copy = internal_tx.to processed_successfully = True if function_name == 'setup' and contract_address != NULL_ADDRESS: owners = arguments['_owners'] threshold = arguments['_threshold'] _, created = SafeContract.objects.get_or_create(address=contract_address, defaults={ 'ethereum_tx': internal_tx.ethereum_tx, 'erc20_block_number': internal_tx.ethereum_tx.block_id, }) if created: logger.info('Found new Safe=%s', contract_address) SafeStatus.objects.create(internal_tx=internal_tx, address=contract_address, owners=owners, threshold=threshold, nonce=0, master_copy=master_copy) elif function_name in ('addOwnerWithThreshold', 'removeOwner', 'removeOwnerWithThreshold'): safe_status = SafeStatus.objects.last_for_address(contract_address) safe_status.threshold = arguments['_threshold'] owner = arguments['owner'] try: if function_name == 'addOwnerWithThreshold': safe_status.owners.append(owner) else: # removeOwner, removeOwnerWithThreshold safe_status.owners.remove(owner) except ValueError: logger.error('Error processing trace=%s for contract=%s with tx-hash=%s', internal_tx.trace_address, contract_address, internal_tx.ethereum_tx_id) safe_status.store_new(internal_tx) elif function_name == 'swapOwner': old_owner = arguments['oldOwner'] new_owner = arguments['newOwner'] safe_status = SafeStatus.objects.last_for_address(contract_address) safe_status.owners.remove(old_owner) safe_status.owners.append(new_owner) safe_status.store_new(internal_tx) elif function_name == 'changeThreshold': safe_status = SafeStatus.objects.last_for_address(contract_address) safe_status.threshold = arguments['_threshold'] safe_status.store_new(internal_tx) elif function_name == 'changeMasterCopy': # TODO Ban address if it doesn't have a valid master copy safe_status = SafeStatus.objects.last_for_address(contract_address) safe_status.master_copy = arguments['_masterCopy'] safe_status.store_new(internal_tx) elif function_name == 'execTransaction': safe_status = SafeStatus.objects.last_for_address(contract_address) nonce = safe_status.nonce if 'baseGas' in arguments: # `dataGas` was renamed to `baseGas` in v1.0.0 base_gas = arguments['baseGas'] safe_version = '1.0.0' else: base_gas = arguments['dataGas'] safe_version = '0.0.1' safe_tx = SafeTx(None, contract_address, arguments['to'], arguments['value'], arguments['data'], arguments['operation'], arguments['safeTxGas'], base_gas, arguments['gasPrice'], arguments['gasToken'], arguments['refundReceiver'], HexBytes(arguments['signatures']), safe_nonce=nonce, safe_version=safe_version) safe_tx_hash = safe_tx.safe_tx_hash ethereum_tx = internal_tx.ethereum_tx # Remove existing transaction with same nonce in case of bad indexing (one of the master copies can be # outdated and a tx with a wrong nonce could be indexed) MultisigTransaction.objects.filter( ethereum_tx=ethereum_tx, nonce=safe_tx.safe_nonce, safe=contract_address ).exclude( safe_tx_hash=safe_tx_hash ).delete() # Remove old txs not used MultisigTransaction.objects.filter( ethereum_tx=None, nonce__lt=safe_tx.safe_nonce, safe=contract_address ) multisig_tx, created = MultisigTransaction.objects.get_or_create( safe_tx_hash=safe_tx_hash, defaults={ 'safe': contract_address, 'ethereum_tx': ethereum_tx, 'to': safe_tx.to, 'value': safe_tx.value, 'data': safe_tx.data if safe_tx.data else None, 'operation': safe_tx.operation, 'safe_tx_gas': safe_tx.safe_tx_gas, 'base_gas': safe_tx.base_gas, 'gas_price': safe_tx.gas_price, 'gas_token': safe_tx.gas_token, 'refund_receiver': safe_tx.refund_receiver, 'nonce': safe_tx.safe_nonce, 'signatures': safe_tx.signatures, }) if not created and not multisig_tx.ethereum_tx: multisig_tx.ethereum_tx = ethereum_tx multisig_tx.signatures = HexBytes(arguments['signatures']) multisig_tx.save(update_fields=['ethereum_tx', 'signatures']) for safe_signature in SafeSignature.parse_signatures(safe_tx.signatures, safe_tx_hash): multisig_confirmation, _ = MultisigConfirmation.objects.get_or_create( multisig_transaction_hash=safe_tx_hash, owner=safe_signature.owner, defaults={ 'ethereum_tx': None, 'multisig_transaction': multisig_tx, 'signature': safe_signature.signature, } ) if multisig_confirmation.signature != safe_signature.signature: multisig_confirmation.signature = safe_signature.signature multisig_confirmation.save(update_fields=['signature']) safe_status.nonce = nonce + 1 safe_status.store_new(internal_tx) elif function_name == 'approveHash': multisig_transaction_hash = arguments['hashToApprove'] ethereum_tx = internal_tx.ethereum_tx owner = internal_tx.get_previous_trace()._from (multisig_confirmation, created) = MultisigConfirmation.objects.get_or_create(multisig_transaction_hash=multisig_transaction_hash, owner=owner, defaults={ 'ethereum_tx': ethereum_tx, }) if not created and not multisig_confirmation.ethereum_tx_id: multisig_confirmation.ethereum_tx = ethereum_tx multisig_confirmation.save() elif function_name == 'execTransactionFromModule': # No side effects or nonce increasing, but trace will be set as processed pass else: processed_successfully = False internal_tx_decoded.set_processed() return processed_successfully