def validate(self, data): super().validate(data) if not SafeContract.objects.filter(address=data['safe']).exists(): raise ValidationError( f"Safe={data['safe']} does not exist or it's still not indexed" ) ethereum_client = EthereumClientProvider() safe = Safe(data['safe'], ethereum_client) # Check owners and pending owners try: safe_owners = safe.retrieve_owners(block_identifier='pending') except BadFunctionCallOutput: # Error using pending block identifier safe_owners = safe.retrieve_owners(block_identifier='latest') signature = data['signature'] delegate = data['delegate'] operation_hash = DelegateSignatureHelper.calculate_hash(delegate) safe_signatures = SafeSignature.parse_signature( signature, operation_hash) if not safe_signatures: raise ValidationError('Cannot a valid signature') elif len(safe_signatures) > 1: raise ValidationError( 'More than one signatures detected, just one is expected') safe_signature = safe_signatures[0] delegator = safe_signature.owner if delegator not in safe_owners: if safe_signature.signature_type == SafeSignatureType.EOA: # Maybe it's an `eth_sign` signature without Gnosis Safe `v + 4`, let's try safe_signatures = SafeSignature.parse_signature( signature, DelegateSignatureHelper.calculate_hash(delegate, eth_sign=True)) safe_signature = safe_signatures[0] delegator = safe_signature.owner if delegator not in safe_owners: raise ValidationError( 'Signing owner is not an owner of the Safe') if not safe_signature.is_valid(): raise ValidationError( f'Signature of type={safe_signature.signature_type.name} for delegator={delegator} ' f'is not valid') data['delegator'] = delegator return data
def add_signature_type(apps, schema_editor): MultisigConfirmation = apps.get_model("history", "MultisigConfirmation") MultisigTransaction = apps.get_model("history", "MultisigTransaction") for multisig_confirmation in MultisigConfirmation.objects.all(): if not multisig_confirmation.signature: # It's an approvedHash multisig_confirmation.signature = SafeSignatureApprovedHash.build_for_owner( multisig_confirmation.owner, multisig_confirmation.multisig_transaction_hash, ).export_signature() multisig_confirmation.signature_type = SafeSignatureType.APPROVED_HASH.value multisig_confirmation.save( update_fields=["signature_type", "signature"]) else: for safe_signature in SafeSignature.parse_signature( multisig_confirmation.signature, multisig_confirmation.multisig_transaction_hash, ): multisig_confirmation.signature_type = ( safe_signature.signature_type.value) multisig_confirmation.save(update_fields=["signature_type"]) for multisig_tx in MultisigTransaction.objects.exclude(signatures=None): for safe_signature in SafeSignature.parse_signature( multisig_tx.signatures.tobytes(), HexBytes(multisig_tx.safe_tx_hash)): multisig_confirmation, _ = MultisigConfirmation.objects.get_or_create( multisig_transaction_hash=multisig_tx.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.export_signature() or multisig_confirmation.signature_type != safe_signature.signature_type.value): multisig_confirmation.signature = safe_signature.export_signature( ) multisig_confirmation.signature_type = ( safe_signature.signature_type.value) multisig_confirmation.save( update_fields=["signature", "signature_type"])
def check_signature(self, signature: bytes, operation_hash: bytes, safe_owners: List[str]) -> Optional[str]: """ Checks signature and returns a valid owner if found, None otherwise :param signature: :param operation_hash: :param safe_owners: :return: Valid delegator address if found, None otherwise """ safe_signatures = SafeSignature.parse_signature( signature, operation_hash) if not safe_signatures: raise ValidationError('Signature is not valid') elif len(safe_signatures) > 1: raise ValidationError( 'More than one signatures detected, just one is expected') safe_signature = safe_signatures[0] delegator = safe_signature.owner if delegator in safe_owners: if not safe_signature.is_valid(): raise ValidationError( f'Signature of type={safe_signature.signature_type.name} ' f'for delegator={delegator} is not valid') return delegator
def check_signature( self, ethereum_client: EthereumClient, safe_address: ChecksumAddress, signature: bytes, operation_hash: bytes, valid_delegators: List[ChecksumAddress], ) -> Optional[ChecksumAddress]: """ Checks signature and returns a valid owner if found, None otherwise :param ethereum_client: :param safe_address: :param signature: :param operation_hash: :param valid_delegators: :return: Valid delegator address if found, None otherwise """ safe_signatures = SafeSignature.parse_signature( signature, operation_hash) if not safe_signatures: raise ValidationError("Signature is not valid") if len(safe_signatures) > 1: raise ValidationError( "More than one signatures detected, just one is expected") safe_signature = safe_signatures[0] delegator = safe_signature.owner if delegator in valid_delegators: if not safe_signature.is_valid(ethereum_client, safe_address): raise ValidationError( f"Signature of type={safe_signature.signature_type.name} " f"for delegator={delegator} is not valid") return delegator
def check_delegate_signature( self, ethereum_client: EthereumClient, signature: bytes, operation_hash: bytes, delegator: ChecksumAddress, ) -> bool: """ Checks signature and returns a valid owner if found, None otherwise :param ethereum_client: :param signature: :param operation_hash: :param delegator: :return: `True` if signature is valid for the delegator, `False` otherwise """ safe_signatures = SafeSignature.parse_signature( signature, operation_hash) if not safe_signatures: raise ValidationError("Signature is not valid") if len(safe_signatures) > 1: raise ValidationError( "More than one signatures detected, just one is expected") safe_signature = safe_signatures[0] owner = safe_signature.owner if owner == delegator: if not safe_signature.is_valid(ethereum_client, owner): raise ValidationError( f"Signature of type={safe_signature.signature_type.name} " f"for delegator={delegator} is not valid") return True return False
def validate_signature(self, signature: bytes): safe_tx_hash = self.context['safe_tx_hash'] try: multisig_transaction = MultisigTransaction.objects.select_related( 'ethereum_tx' ).get(safe_tx_hash=safe_tx_hash) except MultisigTransaction.DoesNotExist: raise NotFound(f'Multisig transaction with safe-tx-hash={safe_tx_hash} was not found') safe_address = multisig_transaction.safe if multisig_transaction.executed: raise ValidationError(f'Transaction with safe-tx-hash={safe_tx_hash} was already executed') ethereum_client = EthereumClientProvider() safe = Safe(safe_address, ethereum_client) try: safe_owners = safe.retrieve_owners(block_identifier='pending') except BadFunctionCallOutput: # Error using pending block identifier safe_owners = safe.retrieve_owners(block_identifier='latest') parsed_signatures = SafeSignature.parse_signature(signature, safe_tx_hash) signature_owners = [] for safe_signature in parsed_signatures: owner = safe_signature.owner if owner not in safe_owners: raise ValidationError(f'Signer={owner} is not an owner. Current owners={safe_owners}') if not safe_signature.is_valid(ethereum_client, safe.address): raise ValidationError(f'Signature={safe_signature.signature.hex()} for owner={owner} is not valid') if owner in signature_owners: raise ValidationError(f'Signature for owner={owner} is duplicated') signature_owners.append(owner) return signature
def owners(self) -> Optional[List[str]]: if not self.signatures: return None else: signatures = bytes(self.signatures) safe_signatures = SafeSignature.parse_signature( signatures, self.safe_tx_hash) return [safe_signature.owner for safe_signature in safe_signatures]
def signers(self) -> List[str]: if not self.signatures: return [] else: signatures = bytes(self.signatures) safe_signatures = SafeSignature.parse_signature( signatures, self.safe_tx_hash) return [safe_signature.owner for safe_signature in safe_signatures]
def add_signature_type(apps, schema_editor): MultisigConfirmation = apps.get_model('history', 'MultisigConfirmation') MultisigTransaction = apps.get_model('history', 'MultisigTransaction') for multisig_confirmation in MultisigConfirmation.objects.all(): if not multisig_confirmation.signature: # It's an approvedHash multisig_confirmation.signature = SafeSignatureApprovedHash.build_for_owner( multisig_confirmation.owner, multisig_confirmation. multisig_transaction_hash).export_signature() multisig_confirmation.signature_type = SafeSignatureType.APPROVED_HASH.value multisig_confirmation.save( update_fields=['signature_type', 'signature']) else: for safe_signature in SafeSignature.parse_signature( multisig_confirmation.signature, multisig_confirmation.multisig_transaction_hash): multisig_confirmation.signature_type = safe_signature.signature_type.value multisig_confirmation.save(update_fields=['signature_type']) for multisig_tx in MultisigTransaction.objects.exclude(signatures=None): for safe_signature in SafeSignature.parse_signature( multisig_tx.signatures.tobytes(), HexBytes(multisig_tx.safe_tx_hash)): multisig_confirmation, _ = MultisigConfirmation.objects.get_or_create( multisig_transaction_hash=multisig_tx.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.export_signature() or \ multisig_confirmation.signature_type != safe_signature.signature_type.value: multisig_confirmation.signature = safe_signature.export_signature( ) multisig_confirmation.signature_type = safe_signature.signature_type.value multisig_confirmation.save( update_fields=['signature', 'signature_type'])
def validate(self, data: Dict[str, Any]): data = super().validate(data) signature_owners = [] owners_without_safe = [] signatures = data.get("signatures") or [] safe_addresses = data["safes"] if signatures: valid_owners = self.get_valid_owners(safe_addresses) for signature in signatures: hash_to_sign = calculate_device_registration_hash( data["timestamp"], data["uuid"], data["cloud_messaging_token"], data["safes"], ) parsed_signatures = SafeSignature.parse_signature( signature, hash_to_sign) if not parsed_signatures: raise ValidationError("Signature cannot be parsed") for safe_signature in parsed_signatures: if (safe_signature.signature_type != SafeSignatureType.EOA or not safe_signature.is_valid()): raise ValidationError( "An externally owned account signature was expected" ) owner = safe_signature.owner if owner in (signature_owners + owners_without_safe): raise ValidationError( f"Signature for owner={owner} is duplicated") if owner not in valid_owners: owners_without_safe.append(owner) # raise ValidationError(f'Owner={owner} is not an owner of any of the safes={data["safes"]}. ' # f'Expected hash to sign {hash_to_sign.hex()}') else: signature_owners.append(owner) if len(signatures) > len(signature_owners + owners_without_safe): raise ValidationError( "Number of signatures is less than the number of owners detected" ) data["owners_registered"] = signature_owners data["owners_not_registered"] = owners_without_safe return data
def validate(self, data: Dict[str, Any]): data = super().validate(data) signature_owners = [] owners_without_safe = [] signatures = data.get('signatures') or [] if signatures: current_owners = { owner for safe in data['safes'] for owner in get_safe_owners(safe) } for signature in signatures: hash_to_sign = calculate_device_registration_hash( data['timestamp'], data['uuid'], data['cloud_messaging_token'], data['safes']) parsed_signatures = SafeSignature.parse_signature( signature, hash_to_sign) if not parsed_signatures: raise ValidationError('Signature cannot be parsed') for safe_signature in parsed_signatures: if safe_signature.signature_type != SafeSignatureType.EOA or not safe_signature.is_valid( ): raise ValidationError( 'An externally owned account signature was expected' ) owner = safe_signature.owner if owner in (signature_owners + owners_without_safe): raise ValidationError( f'Signature for owner={owner} is duplicated') elif owner not in current_owners: owners_without_safe.append(owner) # raise ValidationError(f'Owner={owner} is not an owner of any of the safes={data["safes"]}. ' # f'Expected hash to sign {hash_to_sign.hex()}') else: signature_owners.append(owner) if len(signatures) > len(signature_owners + owners_without_safe): raise ValidationError( 'Number of signatures is less than the number of owners detected' ) data['owners_registered'] = signature_owners data['owners_not_registered'] = owners_without_safe return data
def save(self, **kwargs): safe_tx_hash = self.context['safe_tx_hash'] signature = self.validated_data['signature'] multisig_confirmations = [] parsed_signatures = SafeSignature.parse_signature(signature, safe_tx_hash) for safe_signature in parsed_signatures: multisig_confirmation, _ = MultisigConfirmation.objects.get_or_create( multisig_transaction_hash=safe_tx_hash, owner=safe_signature.owner, defaults={ 'multisig_transaction_id': safe_tx_hash, 'signature': safe_signature.export_signature(), 'signature_type': safe_signature.signature_type.value, } ) multisig_confirmations.append(multisig_confirmation) if self.validated_data['signature']: MultisigTransaction.objects.filter(safe_tx_hash=safe_tx_hash).update(trusted=True) return multisig_confirmations
def validate(self, data): super().validate(data) ethereum_client = EthereumClientProvider() safe = Safe(data['safe'], ethereum_client) safe_tx = safe.build_multisig_tx(data['to'], data['value'], data['data'], data['operation'], data['safe_tx_gas'], data['base_gas'], data['gas_price'], data['gas_token'], data['refund_receiver'], safe_nonce=data['nonce']) contract_transaction_hash = safe_tx.safe_tx_hash # Check safe tx hash matches if contract_transaction_hash != data['contract_transaction_hash']: raise ValidationError( f'Contract-transaction-hash={contract_transaction_hash.hex()} ' f'does not match provided contract-tx-hash={data["contract_transaction_hash"].hex()}' ) # Check there's not duplicated tx with same `nonce` or same `safeTxHash` for the same Safe. # We allow duplicated if existing tx is not executed multisig_transactions = MultisigTransaction.objects.filter( safe=safe.address, nonce=data['nonce']).executed() if multisig_transactions: for multisig_transaction in multisig_transactions: if multisig_transaction.safe_tx_hash == contract_transaction_hash.hex( ): raise ValidationError( f'Tx with safe-tx-hash={contract_transaction_hash.hex()} ' f'for safe={safe.address} was already executed in ' f'tx-hash={multisig_transaction.ethereum_tx_id}') raise ValidationError( f'Tx with nonce={safe_tx.safe_nonce} for safe={safe.address} ' f'already executed in tx-hash={multisig_transactions[0].ethereum_tx_id}' ) # Check owners and pending owners try: safe_owners = safe.retrieve_owners(block_identifier='pending') except BadFunctionCallOutput: # Error using pending block identifier safe_owners = safe.retrieve_owners(block_identifier='latest') data['safe_owners'] = safe_owners delegates = SafeContractDelegate.objects.get_delegates_for_safe( safe.address) allowed_senders = safe_owners + delegates if not data['sender'] in allowed_senders: raise ValidationError( f'Sender={data["sender"]} is not an owner or delegate. ' f'Current owners={safe_owners}. Delegates={delegates}') signature_owners = [] # TODO Make signature mandatory signature = data.get('signature', b'') parsed_signatures = SafeSignature.parse_signature( signature, contract_transaction_hash) data['parsed_signatures'] = parsed_signatures for safe_signature in parsed_signatures: owner = safe_signature.owner if not safe_signature.is_valid(ethereum_client, safe.address): raise ValidationError( f'Signature={safe_signature.signature.hex()} for owner={owner} is not valid' ) if owner in delegates and len(parsed_signatures) > 1: raise ValidationError( f'Just one signature is expected if using delegates') if owner not in allowed_senders: raise ValidationError( f'Signer={owner} is not an owner or delegate. ' f'Current owners={safe_owners}. Delegates={delegates}') if owner in signature_owners: raise ValidationError( f'Signature for owner={owner} is duplicated') signature_owners.append(owner) # TODO Make signature mandatory. len(signature_owners) must be >= 1 if signature_owners and data['sender'] not in signature_owners: raise ValidationError( f'Signature does not match sender={data["sender"]}. ' f'Calculated owners={signature_owners}') return data
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
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 validate(self, data): super().validate(data) ethereum_client = EthereumClientProvider() safe = Safe(data['safe'], ethereum_client) safe_tx = safe.build_multisig_tx(data['to'], data['value'], data['data'], data['operation'], data['safe_tx_gas'], data['base_gas'], data['gas_price'], data['gas_token'], data['refund_receiver'], safe_nonce=data['nonce']) contract_transaction_hash = safe_tx.safe_tx_hash # Check safe tx hash matches if contract_transaction_hash != data['contract_transaction_hash']: raise ValidationError( f'Contract-transaction-hash={contract_transaction_hash.hex()} ' f'does not match provided contract-tx-hash={data["contract_transaction_hash"].hex()}' ) # Check there's not duplicated tx with same `nonce` for the same Safe. # We allow duplicated if existing tx is not executed try: multisig_transaction: MultisigTransaction = MultisigTransaction.objects.exclude( ethereum_tx=None).exclude( safe_tx_hash=contract_transaction_hash).get( safe=safe.address, nonce=data['nonce']) if multisig_transaction.safe_tx_hash != contract_transaction_hash: raise ValidationError( f'Tx with nonce={safe_tx.safe_nonce} for safe={safe.address} already executed in ' f'tx-hash={multisig_transaction.ethereum_tx_id}') except MultisigTransaction.DoesNotExist: pass # Check owners and old owners, owner might be removed but that tx can still be signed by that owner if not safe.retrieve_is_owner(data['sender']): try: # TODO Fix this, we can use SafeStatus now if not safe.retrieve_is_owner( data['sender'], block_identifier=max( 0, ethereum_client.current_block_number - 20)): raise ValidationError('User is not an owner') except BadFunctionCallOutput: # If it didn't exist 20 blocks ago raise ValidationError('User is not an owner') signature = data.get('signature') if signature is not None: # TODO Support signatures with multiple owners if len(signature) != 65: raise ValidationError( 'Signatures with more than one owner still not supported') safe_signature = SafeSignature(signature, contract_transaction_hash) # TODO Support contract signatures and approved hashes if safe_signature.signature_type == SafeSignatureType.CONTRACT_SIGNATURE: raise ValidationError('Contract signatures not supported') elif safe_signature.signature_type == SafeSignatureType.APPROVED_HASH: # Index it automatically later del data['signature'] address = safe_signature.owner if address != data['sender']: raise ValidationError( f'Signature does not match sender={data["sender"]}. Calculated owner={address}' ) return data
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 __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 validate(self, data): super().validate(data) ethereum_client = EthereumClientProvider() safe = Safe(data["safe"], ethereum_client) try: safe_version = safe.retrieve_version() except BadFunctionCallOutput as e: raise ValidationError( f"Could not get Safe version from blockchain, check contract exists on network " f"{ethereum_client.get_network().name}") from e except IOError: raise ValidationError( "Problem connecting to the ethereum node, please try again later" ) safe_tx = safe.build_multisig_tx( data["to"], data["value"], data["data"], data["operation"], data["safe_tx_gas"], data["base_gas"], data["gas_price"], data["gas_token"], data["refund_receiver"], safe_nonce=data["nonce"], safe_version=safe_version, ) contract_transaction_hash = safe_tx.safe_tx_hash # Check safe tx hash matches if contract_transaction_hash != data["contract_transaction_hash"]: raise ValidationError( f"Contract-transaction-hash={contract_transaction_hash.hex()} " f'does not match provided contract-tx-hash={data["contract_transaction_hash"].hex()}' ) # Check there's not duplicated tx with same `nonce` or same `safeTxHash` for the same Safe. # We allow duplicated if existing tx is not executed multisig_transactions = MultisigTransaction.objects.filter( safe=safe.address, nonce=data["nonce"]).executed() if multisig_transactions: for multisig_transaction in multisig_transactions: if multisig_transaction.safe_tx_hash == contract_transaction_hash.hex( ): raise ValidationError( f"Tx with safe-tx-hash={contract_transaction_hash.hex()} " f"for safe={safe.address} was already executed in " f"tx-hash={multisig_transaction.ethereum_tx_id}") raise ValidationError( f"Tx with nonce={safe_tx.safe_nonce} for safe={safe.address} " f"already executed in tx-hash={multisig_transactions[0].ethereum_tx_id}" ) # Check owners and pending owners try: safe_owners = safe.retrieve_owners(block_identifier="pending") except BadFunctionCallOutput: # Error using pending block identifier safe_owners = safe.retrieve_owners(block_identifier="latest") except IOError: raise ValidationError( "Problem connecting to the ethereum node, please try again later" ) data["safe_owners"] = safe_owners delegates = SafeContractDelegate.objects.get_delegates_for_safe_and_owners( safe.address, safe_owners) allowed_senders = set(safe_owners) | delegates if not data["sender"] in allowed_senders: raise ValidationError( f'Sender={data["sender"]} is not an owner or delegate. ' f"Current owners={safe_owners}. Delegates={delegates}") signature_owners = [] # TODO Make signature mandatory signature = data.get("signature", b"") parsed_signatures = SafeSignature.parse_signature( signature, contract_transaction_hash) data["parsed_signatures"] = parsed_signatures # If there's at least one signature, transaction is trusted (until signatures are mandatory) data["trusted"] = bool(parsed_signatures) for safe_signature in parsed_signatures: owner = safe_signature.owner if not safe_signature.is_valid(ethereum_client, safe.address): raise ValidationError( f"Signature={safe_signature.signature.hex()} for owner={owner} is not valid" ) if owner in delegates and len(parsed_signatures) > 1: raise ValidationError( "Just one signature is expected if using delegates") if owner not in allowed_senders: raise ValidationError( f"Signer={owner} is not an owner or delegate. " f"Current owners={safe_owners}. Delegates={delegates}") if owner in signature_owners: raise ValidationError( f"Signature for owner={owner} is duplicated") signature_owners.append(owner) # TODO Make signature mandatory. len(signature_owners) must be >= 1 if signature_owners and data["sender"] not in signature_owners: raise ValidationError( f'Signature does not match sender={data["sender"]}. ' f"Calculated owners={signature_owners}") return data