Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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)
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
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)
Ejemplo n.º 8
0
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}'
Ejemplo n.º 10
0
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}'
Ejemplo n.º 11
0
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'])
Ejemplo n.º 12
0
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}'
Ejemplo n.º 13
0
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]
Ejemplo n.º 14
0
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
Ejemplo n.º 15
0
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'
            ])
Ejemplo n.º 16
0
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
Ejemplo n.º 17
0
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 []
Ejemplo n.º 19
0
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