class EthereumEvent(models.Model): objects = EthereumEventManager.from_queryset(EthereumEventQuerySet)() ethereum_tx = models.ForeignKey(EthereumTx, on_delete=models.CASCADE, related_name='events') log_index = models.PositiveIntegerField() address = EthereumAddressField(db_index=True) topic = Sha3HashField(db_index=True) topics = ArrayField(Sha3HashField()) arguments = JSONField() class Meta: indexes = [GinIndex(fields=['arguments'])] unique_together = (('ethereum_tx', 'log_index'), ) # There are also 2 indexes created manually by 0026 migration, both Btree for arguments->to and arguments->from # To use that indexes json queries must be rewritten to use `::text` fields def __str__(self): return f'Tx-hash={self.ethereum_tx_id} Log-index={self.log_index} Topic={self.topic} Arguments={self.arguments}' @property def created(self): return self.ethereum_tx.block.timestamp def is_erc20(self) -> bool: return self.topic == ERC20_721_TRANSFER_TOPIC and 'value' in self.arguments and 'to' in self.arguments def is_erc721(self) -> bool: return self.topic == ERC20_721_TRANSFER_TOPIC and 'tokenId' in self.arguments and 'to' in self.arguments
class SafeFunding(TimeStampedModel): objects = SafeFundingQuerySet.as_manager() safe = models.OneToOneField(SafeContract, primary_key=True, on_delete=models.CASCADE) safe_funded = models.BooleanField(default=False) deployer_funded = models.BooleanField( default=False, db_index=True) # Set when deployer_funded_tx_hash is mined deployer_funded_tx_hash = Sha3HashField(unique=True, blank=True, null=True) safe_deployed = models.BooleanField( default=False, db_index=True) # Set when safe_deployed_tx_hash is mined # We could use SafeCreation.tx_hash, but we would run into troubles because of Ganache safe_deployed_tx_hash = Sha3HashField(unique=True, blank=True, null=True) def is_all_funded(self): return self.safe_funded and self.deployer_funded def status(self): if self.safe_deployed: return 'DEPLOYED' elif self.safe_deployed_tx_hash: return 'DEPLOYED_UNCHECKED' elif self.deployer_funded: return 'DEPLOYER_FUNDED' elif self.deployer_funded_tx_hash: return 'DEPLOYER_FUNDED_UNCHECKED' elif self.safe_funded: return 'DEPLOYER_NOT_FUNDED_SAFE_WITH_BALANCE' else: return 'SAFE_WITHOUT_BALANCE' def __str__(self): s = 'Safe %s - ' % self.safe.address if self.safe_deployed: s += 'deployed' elif self.safe_deployed_tx_hash: s += 'deployed but not checked' elif self.deployer_funded: s += 'with deployer funded' elif self.deployer_funded_tx_hash: s += 'with deployer funded but not checked' elif self.safe_funded: s += 'has enough balance, but deployer is not funded yet' else: s = 'Safe %s' % self.safe.address return s
class SafeMultisigTx(TimeStampedModel): objects = SafeMultisigTxManager.from_queryset(SafeMultisigTxQuerySet)() safe = models.ForeignKey(SafeContract, on_delete=models.CASCADE, related_name='multisig_txs') ethereum_tx = models.ForeignKey(EthereumTx, on_delete=models.CASCADE, related_name='multisig_txs') to = EthereumAddressField(null=True, blank=True, db_index=True) value = Uint256Field() data = models.BinaryField(null=True, blank=True) operation = models.PositiveSmallIntegerField( choices=[(tag.value, tag.name) for tag in SafeOperation]) safe_tx_gas = Uint256Field() data_gas = Uint256Field() gas_price = Uint256Field() gas_token = EthereumAddressField(null=True, blank=True) refund_receiver = EthereumAddressField(null=True, blank=True) signatures = models.BinaryField() nonce = Uint256Field() safe_tx_hash = Sha3HashField(unique=True, null=True, blank=True) def __str__(self): return '{} - {} - Safe {}'.format(self.ethereum_tx.tx_hash, SafeOperation(self.operation).name, self.safe.address) def get_safe_tx(self, ethereum_client: Optional[EthereumClient] = None ) -> 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 refund_benefit(self) -> Optional[int]: """ :return: Difference of the calculated payment fee and the actual executed payment fee. It will be `None` if transaction was not mined yet or if a `gas_token` was used (not easy to calculate the ether conversion at that point) """ if self.ethereum_tx_id and (not self.gas_token or self.gas_token == NULL_ADDRESS ) and self.ethereum_tx.gas_used: payment_fee = min(self.gas_price, self.ethereum_tx.gas_price) executed_fee = self.ethereum_tx.gas_used * self.ethereum_tx.gas_price return payment_fee - executed_fee def signers(self) -> List[str]: return self.get_safe_tx().signers
class SafeCreation(TimeStampedModel): objects = SafeCreationManager() deployer = EthereumAddressField(primary_key=True) safe = models.OneToOneField(SafeContract, on_delete=models.CASCADE) master_copy = EthereumAddressField() funder = EthereumAddressField(null=True) owners = ArrayField(EthereumAddressField()) threshold = Uint256Field() payment = Uint256Field() tx_hash = Sha3HashField(unique=True) gas = Uint256Field() gas_price = Uint256Field() payment_token = EthereumAddressField(null=True) value = Uint256Field() v = models.PositiveSmallIntegerField() r = Uint256Field() s = Uint256Field() data = models.BinaryField(null=True) signed_tx = models.BinaryField(null=True) def __str__(self): return 'Safe {} - Deployer {}'.format(self.safe, self.deployer) def wei_deploy_cost(self) -> int: """ :return: int: Cost to deploy the contract in wei """ return self.gas * self.gas_price
class EthereumBlock(models.Model): objects = EthereumBlockManager() number = models.PositiveIntegerField(primary_key=True, unique=True) gas_limit = models.PositiveIntegerField() gas_used = models.PositiveIntegerField() timestamp = models.DateTimeField() block_hash = Sha3HashField(unique=True)
class EthereumTx(TimeStampedModel): objects = EthereumTxManager() block = models.ForeignKey(EthereumBlock, on_delete=models.CASCADE, null=True, default=None, related_name='txs') # If mined tx_hash = Sha3HashField(unique=True, primary_key=True) gas_used = Uint256Field(null=True, default=None) # If mined status = models.IntegerField( null=True, default=None, db_index=True) # If mined. Old txs don't have `status` transaction_index = models.PositiveIntegerField(null=True, default=None) # If mined _from = EthereumAddressField(null=True, db_index=True) gas = Uint256Field() gas_price = Uint256Field() data = models.BinaryField(null=True) nonce = Uint256Field() to = EthereumAddressField(null=True, db_index=True) value = Uint256Field() def __str__(self): return '{} status={} from={} to={}'.format(self.tx_hash, self.status, self._from, self.to) @property def success(self) -> Optional[bool]: if self.status is not None: return self.status == 1
class SafeMultisigTx(TimeStampedModel): objects = SafeMultisigTxManager.from_queryset(SafeMultisigTxQuerySet)() safe = models.ForeignKey(SafeContract, on_delete=models.CASCADE, related_name='multisig_txs') ethereum_tx = models.ForeignKey(EthereumTx, on_delete=models.CASCADE, related_name='multisig_txs') to = EthereumAddressField(null=True, db_index=True) value = Uint256Field() data = models.BinaryField(null=True) operation = models.PositiveSmallIntegerField(choices=[(tag.value, tag.name) for tag in SafeOperation]) safe_tx_gas = Uint256Field() data_gas = Uint256Field() gas_price = Uint256Field() gas_token = EthereumAddressField(null=True) refund_receiver = EthereumAddressField(null=True) signatures = models.BinaryField() nonce = Uint256Field() safe_tx_hash = Sha3HashField(unique=True, null=True) class Meta: unique_together = (('safe', 'nonce'),) def __str__(self): return '{} - {} - Safe {}'.format(self.ethereum_tx.tx_hash, SafeOperation(self.operation).name, self.safe.address) def get_safe_tx(self, ethereum_client: Optional[EthereumClient] = None) -> 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)
class EthereumTx(TimeStampedModel): objects = EthereumTxManager() block = models.ForeignKey( EthereumBlock, on_delete=models.CASCADE, null=True, blank=True, default=None, related_name="txs", ) # If mined tx_hash = Sha3HashField(unique=True, primary_key=True) gas_used = Uint256Field(null=True, blank=True, default=None) # If mined status = models.IntegerField( null=True, blank=True, default=None, db_index=True) # If mined. Old txs don't have `status` transaction_index = models.PositiveIntegerField(null=True, blank=True, default=None) # If mined _from = EthereumAddressField(null=True, db_index=True) gas = Uint256Field() gas_price = Uint256Field() data = models.BinaryField(null=True, blank=True) nonce = Uint256Field() to = EthereumAddressField(null=True, blank=True, db_index=True) value = Uint256Field() max_fee_per_gas = Uint256Field(default=0) max_priority_fee_per_gas = Uint256Field(default=0) def __str__(self): return "{} status={} from={} to={}".format(self.tx_hash, self.status, self._from, self.to) @property def success(self) -> Optional[bool]: if self.status is not None: return self.status == 1 @property def fee(self) -> int: return self.gas * self.gas_price def is_eip1559(self): return self.max_fee_per_gas or self.max_priority_fee_per_gas def as_tx_dict(self) -> TxParams: tx_params: TxParams = { "data": bytes(self.data) if self.data else b"", "from": self._from, "gas": self.gas, "gasPrice": self.gas_price, "nonce": self.nonce, "to": self.to, "value": self.value, } if self.is_eip1559(): tx_params["maxFeePerGas"] = self.max_fee_per_gas tx_params["maxPriorityFeePerGas"] = self.max_priority_fee_per_gas else: tx_params["gasPrice"] = self.gas_price return tx_params
class MultisigConfirmation(TimeStampedModel): objects = MultisigConfirmationQuerySet.as_manager() ethereum_tx = models.ForeignKey( EthereumTx, on_delete=models.CASCADE, related_name='multisig_confirmations', null=True) # `null=True` for signature confirmations multisig_transaction = models.ForeignKey(MultisigTransaction, on_delete=models.CASCADE, null=True, related_name="confirmations") multisig_transaction_hash = Sha3HashField( null=True, db_index=True) # Use this while we don't have a `multisig_transaction` owner = EthereumAddressField() signature = HexField(null=True, default=None, max_length=500) # Off chain signatures class Meta: unique_together = (('multisig_transaction_hash', 'owner'), ) def __str__(self): if self.multisig_transaction_id: return f'Confirmation of owner={self.owner} for transaction-hash={self.multisig_transaction_hash}' else: return f'Confirmation of owner={self.owner} for existing transaction={self.multisig_transaction_hash}'
class MultisigConfirmation(TimeStampedModel): objects = MultisigConfirmationManager.from_queryset( MultisigConfirmationQuerySet)() ethereum_tx = models.ForeignKey( EthereumTx, on_delete=models.CASCADE, related_name='multisig_confirmations', null=True) # `null=True` for signature confirmations multisig_transaction = models.ForeignKey(MultisigTransaction, on_delete=models.CASCADE, null=True, related_name='confirmations') multisig_transaction_hash = Sha3HashField( null=True, db_index=True) # Use this while we don't have a `multisig_transaction` owner = EthereumAddressField() signature = HexField(null=True, default=None, max_length=2000) signature_type = models.PositiveSmallIntegerField(choices=[ (tag.value, tag.name) for tag in SafeSignatureType ], db_index=True) class Meta: unique_together = (('multisig_transaction_hash', 'owner'), ) ordering = ['created'] def __str__(self): if self.multisig_transaction_id: return f'Confirmation of owner={self.owner} for transaction-hash={self.multisig_transaction_hash}' else: return f'Confirmation of owner={self.owner} for existing transaction={self.multisig_transaction_hash}'
class EthereumBlock(models.Model): objects = EthereumBlockManager.from_queryset(EthereumBlockQuerySet)() number = models.PositiveIntegerField(primary_key=True) gas_limit = models.PositiveIntegerField() gas_used = models.PositiveIntegerField() timestamp = models.DateTimeField() block_hash = Sha3HashField(unique=True) parent_hash = Sha3HashField(unique=True) # For reorgs, True if `current_block_number` - `number` >= MIN_CONFIRMATIONS confirmed = models.BooleanField(default=False, db_index=True) def __str__(self): return f'Block number={self.number} on {self.timestamp}' def set_confirmed(self): self.confirmed = True self.save(update_fields=['confirmed'])
class EthereumBlock(models.Model): objects = EthereumBlockManager() number = models.PositiveIntegerField(primary_key=True, unique=True) gas_limit = models.PositiveIntegerField() gas_used = models.PositiveIntegerField() timestamp = models.DateTimeField() block_hash = Sha3HashField(unique=True) def __str__(self): return f'Block={self.number} on {self.timestamp}'
class MultisigTransaction(TimeStampedModel): objects = MultisigTransactionManager.from_queryset( MultisigTransactionQuerySet)() safe_tx_hash = Sha3HashField(primary_key=True) safe = EthereumAddressField(db_index=True) ethereum_tx = models.ForeignKey(EthereumTx, null=True, default=None, blank=True, on_delete=models.SET_NULL, related_name='multisig_txs') to = EthereumAddressField(null=True, db_index=True) value = Uint256Field() data = models.BinaryField(null=True) operation = models.PositiveSmallIntegerField( choices=[(tag.value, tag.name) for tag in SafeOperation]) safe_tx_gas = Uint256Field() base_gas = Uint256Field() gas_price = Uint256Field() gas_token = EthereumAddressField(null=True) refund_receiver = EthereumAddressField(null=True) signatures = models.BinaryField(null=True) # When tx is executed nonce = Uint256Field(db_index=True) failed = models.BooleanField(null=True, default=None, db_index=True) origin = models.CharField( null=True, default=None, max_length=200) # To store arbitrary data on the tx trusted = models.BooleanField( default=False, db_index=True) # Txs proposed by a delegate or with one confirmation def __str__(self): return f'{self.safe} - {self.nonce} - {self.safe_tx_hash}' @property def execution_date(self) -> Optional[datetime.datetime]: if self.ethereum_tx_id and self.ethereum_tx.block_id is not None: return self.ethereum_tx.block.timestamp return None @property def executed(self) -> bool: return bool(self.ethereum_tx_id and (self.ethereum_tx.block_id is not None)) @property 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]
class EthereumEvent(models.Model): objects = EthereumEventManager.from_queryset(EthereumEventQuerySet)() ethereum_tx = models.ForeignKey(EthereumTx, on_delete=models.CASCADE, related_name='events') log_index = models.PositiveIntegerField() address = EthereumAddressField(db_index=True) topic = Sha3HashField(db_index=True) topics = ArrayField(Sha3HashField()) arguments = JSONField() class Meta: unique_together = (('ethereum_tx', 'log_index'),) def __str__(self): return f'Tx-hash={self.ethereum_tx_id} Log-index={self.log_index} Topic={self.topic} Arguments={self.arguments}' def is_erc20(self) -> bool: return self.topic == ERC20_721_TRANSFER_TOPIC and 'value' in self.arguments and 'to' in self.arguments def is_erc721(self) -> bool: return self.topic == ERC20_721_TRANSFER_TOPIC and 'tokenId' in self.arguments and 'to' in self.arguments
class EthereumTx(TimeStampedModel): objects = EthereumTxManager() block = models.ForeignKey(EthereumBlock, on_delete=models.CASCADE, null=True, default=None, related_name='txs') # If mined tx_hash = Sha3HashField(primary_key=True) gas_used = Uint256Field(null=True, default=None) # If mined status = models.IntegerField( null=True, default=None, db_index=True) # If mined. Old txs don't have `status` logs = ArrayField(JSONField(), null=True, default=None) # If mined transaction_index = models.PositiveIntegerField(null=True, default=None) # If mined _from = EthereumAddressField(null=True, db_index=True) gas = Uint256Field() gas_price = Uint256Field() data = models.BinaryField(null=True) nonce = Uint256Field() to = EthereumAddressField(null=True, db_index=True) value = Uint256Field() def __str__(self): return '{} status={} from={} to={}'.format(self.tx_hash, self.status, self._from, self.to) @property def execution_date(self) -> Optional[datetime.datetime]: if self.block_id is not None: return self.block.timestamp return None @property def success(self) -> Optional[bool]: if self.status is not None: return self.status == 1 def update_with_block_and_receipt(self, ethereum_block: 'EthereumBlock', tx_receipt: Dict[str, Any]): if self.block is None: self.block = ethereum_block self.gas_used = tx_receipt['gasUsed'] self.logs = [ clean_receipt_log(log) for log in tx_receipt.get('logs', list()) ] self.status = tx_receipt.get('status') self.transaction_index = tx_receipt['transactionIndex'] return self.save(update_fields=[ 'block', 'gas_used', 'logs', 'status', 'transaction_index' ])
class SafeCreation2(TimeStampedModel): objects = SafeCreationManager.from_queryset(SafeCreation2QuerySet)() safe = models.OneToOneField(SafeContract, on_delete=models.CASCADE, primary_key=True) master_copy = EthereumAddressField() proxy_factory = EthereumAddressField() salt_nonce = Uint256Field() owners = ArrayField(EthereumAddressField()) threshold = Uint256Field() # to = EthereumAddressField(null=True) # Contract address for optional delegate call # data = models.BinaryField(null=True) # Data payload for optional delegate call payment_token = EthereumAddressField(null=True) payment = Uint256Field() payment_receiver = EthereumAddressField( null=True) # If empty, `tx.origin` is used setup_data = models.BinaryField( null=True) # Binary data for safe `setup` call gas_estimated = Uint256Field() gas_price_estimated = Uint256Field() tx_hash = Sha3HashField(unique=True, null=True, default=None) block_number = models.IntegerField(null=True, default=None) # If mined class Meta: verbose_name_plural = "Safe creation2s" def __str__(self): if self.block_number: return 'Safe {} - Deployed on block number {}'.format( self.safe, self.block_number) else: return 'Safe {}'.format(self.safe) def deployed(self) -> bool: return self.block_number is not None def wei_estimated_deploy_cost(self) -> int: """ :return: int: Cost to deploy the contract in wei """ return self.gas_estimated * self.gas_price_estimated def gas_used(self) -> Optional[int]: """ :return: Gas used by the transaction if it was executed """ if self.tx_hash: try: return EthereumTx.objects.get(tx_hash=self.tx_hash).gas_used except EthereumTx.DoesNotExist: return None
class EthereumTx(TimeStampedModel): objects = EthereumTxManager() block = models.ForeignKey(EthereumBlock, on_delete=models.CASCADE, null=True, default=None, related_name='txs') # If mined tx_hash = Sha3HashField(unique=True, primary_key=True) gas_used = Uint256Field(null=True, default=None) # If mined _from = EthereumAddressField(null=True, db_index=True) gas = Uint256Field() gas_price = Uint256Field() data = models.BinaryField(null=True) nonce = Uint256Field() to = EthereumAddressField(null=True, db_index=True) value = Uint256Field() def __str__(self): return '{} from={} to={}'.format(self.tx_hash, self._from, self.to)
class MultisigTransaction(TimeStampedModel): objects = MultisigTransactionQuerySet.as_manager() safe_tx_hash = Sha3HashField(primary_key=True) safe = EthereumAddressField() ethereum_tx = models.ForeignKey(EthereumTx, null=True, default=None, blank=True, on_delete=models.SET_NULL, related_name='multisig_txs') to = EthereumAddressField(null=True, db_index=True) value = Uint256Field() data = models.BinaryField(null=True) operation = models.PositiveSmallIntegerField( choices=[(tag.value, tag.name) for tag in SafeOperation]) safe_tx_gas = Uint256Field() base_gas = Uint256Field() gas_price = Uint256Field() gas_token = EthereumAddressField(null=True) refund_receiver = EthereumAddressField(null=True) signatures = models.BinaryField(null=True) # When tx is executed nonce = Uint256Field() def __str__(self): return f'{self.safe} - {self.nonce} - {self.safe_tx_hash}' @property def execution_date(self) -> Optional[datetime.datetime]: if self.ethereum_tx_id and self.ethereum_tx.block: return self.ethereum_tx.block.timestamp return None @property def executed(self) -> bool: return bool(self.ethereum_tx_id and (self.ethereum_tx.block_id is not None)) def owners(self) -> Optional[List[str]]: if not self.signatures: return None else: # TODO Get owners from signatures. Not very trivial return []
class EthereumEvent(models.Model): objects = EthereumEventManager.from_queryset(EthereumEventQuerySet)() ethereum_tx = models.ForeignKey(EthereumTx, on_delete=models.CASCADE, related_name='events') log_index = models.PositiveIntegerField() token_address = EthereumAddressField(db_index=True) topic = Sha3HashField(db_index=True) arguments = JSONField() class Meta: unique_together = (('ethereum_tx', 'log_index'),) def __str__(self): return 'Tx-hash={} Log-index={} Arguments={}'.format(self.ethereum_tx_id, self.log_index, self.arguments) def is_erc20(self) -> bool: return 'value' in self.arguments def is_erc721(self) -> bool: return 'tokenId' in self.arguments