def _check_channel_state_for_settle(
        self,
        participant1: typing.Address,
        participant2: typing.Address,
        channel_identifier: typing.ChannelID,
    ) -> None:
        channel_data = self.detail_channel(participant1, participant2,
                                           channel_identifier)
        if channel_data.state == ChannelState.SETTLED:
            raise RaidenRecoverableError('Channel is already settled', )
        elif channel_data.state == ChannelState.REMOVED:
            raise RaidenRecoverableError(
                'Channel is already unlocked. It cannot be settled', )
        elif channel_data.state == ChannelState.OPENED:
            raise RaidenUnrecoverableError(
                'Channel is still open. It cannot be settled', )
        elif channel_data.state == ChannelState.CLOSED:
            if self.client.block_number() < channel_data.settle_block_number:
                raise RaidenUnrecoverableError(
                    'Channel cannot be settled before settlement window is over',
                )

            raise RaidenUnrecoverableError(
                "Settling this channel failed although the channel's current state "
                "is closed.", )
Exemple #2
0
    def _check_channel_state_for_deposit(
        self,
        participant1,
        participant2,
        channel_identifier,
        deposit_amount,
    ):
        participant_details = self.detail_participants(
            participant1,
            participant2,
            channel_identifier,
        )

        channel_state = self._get_channel_state(
            participant1=self.node_address,
            participant2=participant2,
            channel_identifier=channel_identifier,
        )
        # Check if deposit is being made on a nonexistent channel
        if channel_state in (ChannelState.NONEXISTENT, ChannelState.REMOVED):
            raise RaidenUnrecoverableError(
                f'Channel between participant {participant1} '
                f'and {participant2} does not exist', )
        # Deposit was prohibited because the channel is settled
        elif channel_state == ChannelState.SETTLED:
            raise RaidenUnrecoverableError(
                'Deposit is not possible due to channel being settled', )
        # Deposit was prohibited because the channel is closed
        elif channel_state == ChannelState.CLOSED:
            raise RaidenRecoverableError('Channel is already closed', )
        elif participant_details.our_details.deposit < deposit_amount:
            raise RaidenUnrecoverableError('Deposit amount decreased')
Exemple #3
0
    def _check_channel_state_for_withdraw(
        self,
        participant1,
        participant2,
        channel_identifier,
        withdraw_amount,
    ):
        participant_details = self.detail_participants(
            participant1,
            participant2,
            channel_identifier,
        )

        if participant_details.our_details.withdrawn > withdraw_amount:
            raise WithdrawMismatch('Withdraw amount decreased')

        channel_state = self._get_channel_state(
            participant1=participant1,
            participant2=participant2,
            channel_identifier=channel_identifier,
        )

        if channel_state in (ChannelState.NONEXISTENT, ChannelState.REMOVED):
            raise RaidenUnrecoverableError(
                f'Channel between participant {participant1} '
                f'and {participant2} does not exist', )
        elif channel_state == ChannelState.SETTLED:
            raise RaidenUnrecoverableError(
                'A settled channel cannot be closed', )
        elif channel_state == ChannelState.CLOSED:
            raise RaidenRecoverableError('Channel is already closed', )
Exemple #4
0
    def _plan_withdraw_check_result(
            self, transaction_sent: Optional[TransactionSent],
            amount_to_plan_withdraw: TokenAmount
    ) -> Optional[TransactionMined]:
        if transaction_sent is None:
            failed_at = self.client.get_block(BLOCK_ID_LATEST)
            failed_at_blocknumber = failed_at["number"]

            self.client.check_for_insufficient_eth(
                transaction_name="planWithdraw",
                transaction_executed=False,
                required_gas=self.gas_measurements["UserDeposit.planWithdraw"],
                block_identifier=failed_at_blocknumber,
            )
            raise RaidenRecoverableError(
                "Plan withdraw transaction failed to be sent for an unknown reason."
            )

        transaction_mined = self.client.poll_transaction(transaction_sent)

        if not was_transaction_successfully_mined(transaction_mined):
            if amount_to_plan_withdraw <= 0:
                raise RaidenRecoverableError(
                    f"Planned withdraw amount was <= 0: {amount_to_plan_withdraw}."
                )

            failed_at_blocknumber = BlockNumber(
                transaction_mined.receipt["blockNumber"])

            current_balance = self.get_total_deposit(
                address=self.node_address,
                block_identifier=failed_at_blocknumber)

            if current_balance < amount_to_plan_withdraw:
                raise RaidenRecoverableError(
                    f"Couldn't plan withdraw because planned amount "
                    f"{amount_to_plan_withdraw} exceeded current balance of {current_balance}."
                )

            raise RaidenRecoverableError(
                "Plan withdraw failed for an unknown reason.")

        return transaction_mined
Exemple #5
0
    def _check_channel_state_for_settle(self, participant1, participant2,
                                        channel_identifier):
        channel_data = self.detail_channel(participant1, participant2,
                                           channel_identifier)
        if channel_data.state == ChannelState.SETTLED:
            raise RaidenRecoverableError('Channel is already settled', )
        elif channel_data.state == ChannelState.REMOVED:
            raise RaidenRecoverableError(
                'Channel is already unlocked. It cannot be settled', )
        elif channel_data.state == ChannelState.OPENED:
            raise RaidenUnrecoverableError(
                'Channel is still open. It cannot be settled', )
        elif channel_data.state == ChannelState.CLOSED:
            if self.client.block_number() < channel_data.settle_block_number:
                raise RaidenUnrecoverableError(
                    'Channel cannot be settled before settlement window is over',
                )

            raise RaidenRecoverableError(
                'Channel cannot be settled before closing', )
    def _withdraw_check_result(
        self,
        transaction_sent: Optional[TransactionSent],
        amount_to_withdraw: TokenAmount,
        token: Token,
        previous_token_balance: TokenAmount,
    ) -> None:
        if transaction_sent is None:
            failed_at = self.client.get_block(BLOCK_ID_LATEST)
            failed_at_blocknumber = failed_at["number"]

            self.client.check_for_insufficient_eth(
                transaction_name="withdraw",
                transaction_executed=False,
                required_gas=self.gas_measurements["UserDeposit.withdraw"],
                block_identifier=failed_at_blocknumber,
            )
            raise RaidenRecoverableError(
                "Withdraw transaction failed to be sent for an unknown reason."
            )

        transaction_mined = self.client.poll_transaction(transaction_sent)

        if not was_transaction_successfully_mined(transaction_mined):
            failed_at_blocknumber = BlockNumber(transaction_mined.receipt["blockNumber"])

            withdraw_plan = self.get_withdraw_plan(
                withdrawer_address=self.node_address, block_identifier=failed_at_blocknumber
            )
            whole_balance = self.whole_balance(block_identifier=failed_at_blocknumber)

            if amount_to_withdraw > withdraw_plan.withdraw_amount:
                raise RaidenRecoverableError(
                    f"Couldn't withdraw {amount_to_withdraw}, "
                    f"current withdraw plan only allows for {withdraw_plan.withdraw_amount}."
                )

            if withdraw_plan.withdraw_block > failed_at_blocknumber:
                raise RaidenRecoverableError(
                    f"Couldn't withdraw at block {failed_at_blocknumber}. "
                    f"The current withdraw plan requires block number "
                    f"{withdraw_plan.withdraw_block}."
                )

            if whole_balance - amount_to_withdraw < 0:
                raise RaidenRecoverableError(
                    f"The current whole balance is {whole_balance}. "
                    f"The withdraw of {amount_to_withdraw} would have lead to an underflow."
                )

            current_token_balance = TokenAmount(
                token.balance_of(self.node_address, block_identifier=failed_at_blocknumber)
            )
            # FIXME: This seems fishy. The token balance could have an unexpected value for
            #        unrelated reasons (e.g. concurrent token transfers via transferFrom).
            if current_token_balance != previous_token_balance + amount_to_withdraw:
                raise RaidenRecoverableError("Token transfer during withdraw failed.")

            raise RaidenRecoverableError("Withdraw failed for an unknown reason.")
Exemple #7
0
    def add_token(self, token_address: typing.TokenAddress):
        if not is_binary_address(token_address):
            raise InvalidAddress('Expected binary address format for token')

        log.info(
            'add_token called',
            node=pex(self.node_address),
            token_address=pex(token_address),
            registry_address=pex(self.address),
        )

        transaction_hash = self.proxy.transact(
            'createERC20TokenNetwork',
            token_address,
        )

        self.client.poll(transaction_hash)
        receipt_or_none = check_transaction_threw(self.client,
                                                  transaction_hash)
        if receipt_or_none:
            log.info(
                'add_token failed',
                node=pex(self.node_address),
                token_address=pex(token_address),
                registry_address=pex(self.address),
            )
            if self.get_token_network(token_address):
                raise RaidenRecoverableError('Token already registered')
            raise TransactionThrew('createERC20TokenNetwork', receipt_or_none)

        token_network_address = self.get_token_network(token_address)

        if token_network_address is None:
            log.info(
                'add_token failed and check_transaction_threw didnt detect it',
                node=pex(self.node_address),
                token_address=pex(token_address),
                registry_address=pex(self.address),
            )

            raise RuntimeError('token_to_token_networks failed')

        log.info(
            'add_token successful',
            node=pex(self.node_address),
            token_address=pex(token_address),
            registry_address=pex(self.address),
            token_network_address=pex(token_network_address),
        )

        return token_network_address
Exemple #8
0
    def add_token(self, token_address: typing.TokenAddress):
        if not is_binary_address(token_address):
            raise InvalidAddress('Expected binary address format for token')

        log_details = {
            'node': pex(self.node_address),
            'token_address': pex(token_address),
            'registry_address': pex(self.address),
        }
        log.debug('createERC20TokenNetwork called', **log_details)

        transaction_hash = self.proxy.transact(
            'createERC20TokenNetwork',
            safe_gas_limit(GAS_REQUIRED_FOR_CREATE_ERC20_TOKEN_NETWORK),
            token_address,
        )

        self.client.poll(transaction_hash)
        receipt_or_none = check_transaction_threw(self.client,
                                                  transaction_hash)
        if receipt_or_none:
            if self.get_token_network(token_address):
                msg = 'Token already registered'
                log.info(f'createERC20TokenNetwork failed, {msg}',
                         **log_details)
                raise RaidenRecoverableError(msg)

            log.critical(f'createERC20TokenNetwork failed', **log_details)
            raise TransactionThrew('createERC20TokenNetwork', receipt_or_none)

        token_network_address = self.get_token_network(token_address)

        if token_network_address is None:
            log.critical(
                'createERC20TokenNetwork failed and check_transaction_threw didnt detect it',
                **log_details,
            )

            raise RuntimeError('token_to_token_networks failed')

        log.info(
            'createERC20TokenNetwork successful',
            token_network_address=pex(token_network_address),
            **log_details,
        )

        return token_network_address
Exemple #9
0
    def _check_channel_state_for_close(self, participant1, participant2,
                                       channel_identifier):
        channel_state = self._get_channel_state(
            participant1=participant1,
            participant2=participant2,
            channel_identifier=channel_identifier,
        )

        if channel_state in (ChannelState.NONEXISTENT, ChannelState.REMOVED):
            raise RaidenUnrecoverableError(
                f'Channel between participant {participant1} '
                f'and {participant2} does not exist', )
        elif channel_state == ChannelState.SETTLED:
            raise RaidenUnrecoverableError(
                'A settled channel cannot be closed', )
        elif channel_state == ChannelState.CLOSED:
            raise RaidenRecoverableError('Channel is already closed', )
Exemple #10
0
 def _inspect_channel_identifier(
     self,
     participant1: typing.Address,
     participant2: typing.Address,
     called_by_fn: str,
     channel_identifier: typing.ChannelID = None,
 ) -> typing.ChannelID:
     if not channel_identifier:
         channel_identifier = self._call_and_check_result(
             'getChannelIdentifier',
             to_checksum_address(participant1),
             to_checksum_address(participant2),
         )
     assert isinstance(channel_identifier, typing.T_ChannelID)
     if channel_identifier == 0:
         raise RaidenRecoverableError(
             f'When calling {called_by_fn} either 0 value was given for the '
             'channel_identifier or getChannelIdentifier returned 0, meaning '
             f'no channel currently exists between {pex(participant1)} and '
             f'{pex(participant2)}', )
     return channel_identifier
    def _check_channel_state_for_update(
        self,
        channel_identifier: typing.ChannelID,
        closer: typing.Address,
        update_nonce: typing.Nonce,
        log_details: typing.Dict,
    ) -> None:
        """Check the channel state on chain to see if it has been updated.

        Compare the nonce we are about to update the contract with the
        updated nonce in the onchain state and if it's the same raise a
        RaidenRecoverableError"""
        closer_details = self.detail_participant(
            channel_identifier=channel_identifier,
            participant=closer,
            partner=self.node_address,
        )
        if closer_details.nonce == update_nonce:
            msg = ('updateNonClosingBalanceProof transaction has already '
                   'been mined and updated the channel succesfully.')
            log.warning(f'{msg}', **log_details)
            raise RaidenRecoverableError(msg)
Exemple #12
0
        def check_for_insufficient_token_balance(block_number: BlockNumber) -> None:

            failed_at_hash = encode_hex(self.client.blockhash_from_blocknumber(block_number))
            self.client.check_for_insufficient_eth(
                transaction_name="transfer",
                transaction_executed=False,
                required_gas=GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL,
                block_identifier=block_number,
            )

            balance = self.balance_of(self.client.address, failed_at_hash)
            if balance < amount:
                msg = (
                    f"Call to transfer will fail. Your balance of {balance} is "
                    f"below the required amount of {amount}."
                )
                if balance == 0:
                    msg += (
                        " Note: The balance was 0, which may also happen if the contract "
                        "is not a valid ERC20 token (balanceOf method missing)."
                    )
                raise RaidenRecoverableError(msg)
Exemple #13
0
    def transfer(self, to_address: Address, amount: TokenAmount) -> None:
        """ Transfer `amount` tokens to `to_address`.

        Note:

            We assume there to be sufficient balance as a precondition if
            this is called, so that is not checked as a precondition here.
        """
        # Note that given_block_identifier is not used here as there
        # are no preconditions to check before sending the transaction
        # There are no direct calls to this method in any event handler,
        # so a precondition check would make no sense.
        with self.token_lock:
            log_details = {
                "node": to_checksum_address(self.node_address),
                "contract": to_checksum_address(self.address),
                "to_address": to_checksum_address(to_address),
                "amount": amount,
            }

            with log_transaction(log, "transfer", log_details):
                checking_block = self.client.get_checking_block()
                gas_limit = self.proxy.estimate_gas(
                    checking_block, "transfer",
                    to_checksum_address(to_address), amount)
                failed_receipt = None

                if gas_limit is not None:
                    gas_limit = safe_gas_limit(gas_limit)
                    log_details["gas_limit"] = gas_limit

                    transaction_hash = self.proxy.transact(
                        "transfer", gas_limit, to_checksum_address(to_address),
                        amount)

                    receipt = self.client.poll(transaction_hash)
                    # TODO: check Transfer event (issue: #2598)
                    failed_receipt = check_transaction_threw(receipt=receipt)

                if gas_limit is None or failed_receipt is not None:
                    if failed_receipt:
                        failed_at_number = failed_receipt["blockNumber"]
                    else:
                        failed_at_number = checking_block
                    failed_at_hash = encode_hex(
                        self.client.blockhash_from_blocknumber(
                            failed_at_number))

                    self.proxy.rpc_client.check_for_insufficient_eth(
                        transaction_name="transfer",
                        transaction_executed=False,
                        required_gas=GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL,
                        block_identifier=failed_at_number,
                    )

                    balance = self.balance_of(self.client.address,
                                              failed_at_hash)
                    if balance < amount:
                        msg = (
                            f"Call to transfer will fail. Your balance of {balance} is "
                            f"below the required amount of {amount}.")
                        if balance == 0:
                            msg += (
                                " Note: The balance was 0, which may also happen if the contract "
                                "is not a valid ERC20 token (balanceOf method missing)."
                            )
                        raise RaidenRecoverableError(msg)

                    if gas_limit is None:
                        raise RaidenRecoverableError(
                            "Call to transfer will fail. Gas estimation failed for unknown "
                            "reason. Please make sure the contract is a valid ERC20 token."
                        )
                    else:
                        raise RaidenRecoverableError(
                            "Call to transfer failed for unknown reason. Please make sure the "
                            "contract is a valid ERC20 token.")
Exemple #14
0
    def settle(
        self,
        channel_identifier: typing.ChannelID,
        transferred_amount: int,
        locked_amount: int,
        locksroot: typing.Locksroot,
        partner: typing.Address,
        partner_transferred_amount: int,
        partner_locked_amount: int,
        partner_locksroot: typing.Locksroot,
    ):
        """ Settle the channel.

        Raises:
            ChannelBusyError: If the channel is busy with another operation
        """
        log_details = {
            'token_network': pex(self.address),
            'node': pex(self.node_address),
            'partner': pex(partner),
            'transferred_amount': transferred_amount,
            'locked_amount': locked_amount,
            'locksroot': encode_hex(locksroot),
            'partner_transferred_amount': partner_transferred_amount,
            'partner_locked_amount': partner_locked_amount,
            'partner_locksroot': encode_hex(partner_locksroot),
        }
        log.info('settle called', **log_details)

        self._check_for_outdated_channel(
            self.node_address,
            partner,
            channel_identifier,
        )

        with self.channel_operations_lock[partner]:
            if self._verify_settle_state(
                    transferred_amount,
                    locked_amount,
                    locksroot,
                    partner,
                    partner_transferred_amount,
                    partner_locked_amount,
                    partner_locksroot,
            ) is False:
                raise RaidenRecoverableError(
                    'local state can not be used to call settle')
            our_maximum = transferred_amount + locked_amount
            partner_maximum = partner_transferred_amount + partner_locked_amount

            # The second participant transferred + locked amount must be higher
            our_bp_is_larger = our_maximum > partner_maximum

            if our_bp_is_larger:
                transaction_hash = self.proxy.transact(
                    'settleChannel',
                    channel_identifier,
                    partner,
                    partner_transferred_amount,
                    partner_locked_amount,
                    partner_locksroot,
                    self.node_address,
                    transferred_amount,
                    locked_amount,
                    locksroot,
                )
            else:
                transaction_hash = self.proxy.transact(
                    'settleChannel',
                    channel_identifier,
                    self.node_address,
                    transferred_amount,
                    locked_amount,
                    locksroot,
                    partner,
                    partner_transferred_amount,
                    partner_locked_amount,
                    partner_locksroot,
                )

            self.client.poll(transaction_hash)
            receipt_or_none = check_transaction_threw(self.client,
                                                      transaction_hash)
            if receipt_or_none:
                log.info('settle failed', **log_details)
                self._check_channel_state_for_settle(
                    self.node_address,
                    partner,
                    channel_identifier,
                )
                raise TransactionThrew('Settle', receipt_or_none)

            log.info('settle successful', **log_details)
Exemple #15
0
    def transfer(self, to_address: Address, amount: TokenAmount) -> None:
        """ Transfer `amount` tokens to `to_address`.

        Note:

            We assume there to be sufficient balance as a precondition if
            this is called, so that is not checked as a precondition here.
        """
        def check_for_insufficient_token_balance(
                block_number: BlockNumber) -> None:

            failed_at_hash = encode_hex(
                self.client.blockhash_from_blocknumber(block_number))
            self.client.check_for_insufficient_eth(
                transaction_name="transfer",
                transaction_executed=False,
                required_gas=GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL,
                block_identifier=block_number,
            )

            balance = self.balance_of(self.client.address, failed_at_hash)
            if balance < amount:
                msg = (
                    f"Call to transfer will fail. Your balance of {balance} is "
                    f"below the required amount of {amount}.")
                if balance == 0:
                    msg += (
                        " Note: The balance was 0, which may also happen if the contract "
                        "is not a valid ERC20 token (balanceOf method missing)."
                    )
                raise RaidenRecoverableError(msg)

        # Note that given_block_identifier is not used here as there
        # are no preconditions to check before sending the transaction
        # There are no direct calls to this method in any event handler,
        # so a precondition check would make no sense.
        with self.token_lock:
            log_details: Dict[str, Any] = {}

            estimated_transaction = self.client.estimate_gas(
                self.proxy, "transfer", log_details, to_address, amount)

            if estimated_transaction is not None:
                # TODO: check Transfer event (issue: #2598)
                transaction_sent = self.client.transact(estimated_transaction)
                transaction_mined = self.client.poll_transaction(
                    transaction_sent)

                if was_transaction_successfully_mined(transaction_mined):
                    return

                failed_at_block_number = BlockNumber(
                    transaction_mined.receipt["blockNumber"])
                check_for_insufficient_token_balance(failed_at_block_number)
                raise RaidenRecoverableError(
                    "Call to transfer failed for unknown reason. Please make sure the "
                    "contract is a valid ERC20 token.")
            else:
                failed_at_block_number = self.client.get_block(
                    BLOCK_ID_LATEST)["number"]
                check_for_insufficient_token_balance(failed_at_block_number)
                raise RaidenRecoverableError(
                    "Call to transfer will fail. Gas estimation failed for unknown "
                    "reason. Please make sure the contract is a valid ERC20 token."
                )
Exemple #16
0
    def approve(self, allowed_address: Address,
                allowance: TokenAmount) -> None:
        """ Approve `allowed_address` to transfer up to `deposit` amount of token.

        Note:

            For channel deposit please use the channel proxy, since it does
            additional validations.
            We assume there to be sufficient balance as a precondition if this
            is called, so it is not checked as a precondition here.
        """
        # Note that given_block_identifier is not used here as there
        # are no preconditions to check before sending the transaction
        # There are no direct calls to this method in any event handler,
        # so a precondition check would make no sense.
        with self.token_lock:
            log_details = {
                "node": to_checksum_address(self.node_address),
                "contract": to_checksum_address(self.address),
                "allowed_address": to_checksum_address(allowed_address),
                "allowance": allowance,
            }

            with log_transaction(log, "approve", log_details):
                checking_block = self.client.get_checking_block()
                error_prefix = "Call to approve will fail"
                gas_limit = self.proxy.estimate_gas(
                    checking_block, "approve",
                    to_checksum_address(allowed_address), allowance)

                if gas_limit:
                    error_prefix = "Call to approve failed"
                    gas_limit = safe_gas_limit(gas_limit)
                    log_details["gas_limit"] = gas_limit
                    transaction_hash = self.proxy.transact(
                        "approve", gas_limit,
                        to_checksum_address(allowed_address), allowance)

                    receipt = self.client.poll(transaction_hash)
                    failed_receipt = check_transaction_threw(receipt=receipt)

                    if failed_receipt:
                        failed_at_blockhash = encode_hex(
                            failed_receipt["blockHash"])

                        if failed_receipt["cumulativeGasUsed"] == gas_limit:
                            msg = (
                                f"approve failed and all gas was used ({gas_limit}). "
                                f"Estimate gas may have underestimated approve, or "
                                f"succeeded even though an assert is triggered, or "
                                f"the smart contract code has a conditional assert."
                            )
                            raise RaidenRecoverableError(msg)

                        balance = self.balance_of(self.client.address,
                                                  failed_at_blockhash)
                        if balance < allowance:
                            msg = (
                                f"{error_prefix} Your balance of {balance} is "
                                "below the required amount of {allowance}.")
                            if balance == 0:
                                msg += (
                                    " Note: The balance was 0, which may also happen "
                                    "if the contract is not a valid ERC20 token "
                                    "(balanceOf method missing).")
                            raise RaidenRecoverableError(msg)

                        raise RaidenRecoverableError(
                            f"{error_prefix}. The reason is unknown, you have enough tokens for "
                            f"the requested allowance and enough eth to pay the gas. There may "
                            f"be a problem with the token contract.")

                else:
                    failed_at = self.proxy.rpc_client.get_block("latest")
                    failed_at_blockhash = encode_hex(failed_at["hash"])
                    failed_at_blocknumber = failed_at["number"]

                    self.proxy.rpc_client.check_for_insufficient_eth(
                        transaction_name="approve",
                        transaction_executed=False,
                        required_gas=GAS_REQUIRED_FOR_APPROVE,
                        block_identifier=failed_at_blocknumber,
                    )

                    balance = self.balance_of(self.client.address,
                                              failed_at_blockhash)
                    if balance < allowance:
                        msg = (f"{error_prefix} Your balance of {balance} is "
                               "below the required amount of {allowance}.")
                        if balance == 0:
                            msg += (
                                " Note: The balance was 0, which may also happen if the contract "
                                "is not a valid ERC20 token (balanceOf method missing)."
                            )
                        raise RaidenRecoverableError(msg)

                    raise RaidenRecoverableError(
                        f"{error_prefix} Gas estimation failed for unknown reason. "
                        f"Please make sure the contract is a valid ERC20 token."
                    )
    def _add_token(self, token_address: TokenAddress,
                   additional_arguments: Dict) -> Address:
        if not is_binary_address(token_address):
            raise InvalidAddress("Expected binary address format for token")

        token_proxy = Token(
            jsonrpc_client=self.client,
            token_address=token_address,
            contract_manager=self.contract_manager,
        )

        if token_proxy.total_supply() == "":
            raise InvalidToken(
                "Given token address does not follow the ERC20 standard (missing totalSupply()"
            )

        log_details = {
            "node": pex(self.node_address),
            "token_address": pex(token_address),
            "registry_address": pex(self.address),
        }
        log.debug("createERC20TokenNetwork called", **log_details)

        checking_block = self.client.get_checking_block()
        error_prefix = "Call to createERC20TokenNetwork will fail"

        kwarguments = {"_token_address": token_address}
        kwarguments.update(additional_arguments)
        gas_limit = self.proxy.estimate_gas(checking_block,
                                            "createERC20TokenNetwork",
                                            **kwarguments)

        if gas_limit:
            error_prefix = "Call to createERC20TokenNetwork failed"
            transaction_hash = self.proxy.transact(
                "createERC20TokenNetwork",
                safe_gas_limit(gas_limit,
                               GAS_REQUIRED_FOR_CREATE_ERC20_TOKEN_NETWORK),
                **kwarguments,
            )

            self.client.poll(transaction_hash)
            receipt_or_none = check_transaction_threw(self.client,
                                                      transaction_hash)

        transaction_executed = gas_limit is not None
        if not transaction_executed or receipt_or_none:
            if transaction_executed:
                block = receipt_or_none["blockNumber"]
            else:
                block = checking_block

            required_gas = gas_limit if gas_limit else GAS_REQUIRED_FOR_CREATE_ERC20_TOKEN_NETWORK
            self.proxy.jsonrpc_client.check_for_insufficient_eth(
                transaction_name="createERC20TokenNetwork",
                transaction_executed=transaction_executed,
                required_gas=required_gas,
                block_identifier=block,
            )

            if self.get_token_network(token_address, block):
                error_msg = f"{error_prefix}. Token already registered"
                log.warning(error_msg, **log_details)
                raise RaidenRecoverableError(error_msg)

            error_msg = f"{error_prefix}"
            log.critical(error_msg, **log_details)
            raise RaidenUnrecoverableError(error_msg)

        token_network_address = self.get_token_network(token_address, "latest")
        if token_network_address is None:
            msg = "createERC20TokenNetwork succeeded but token network address is Null"
            log.critical(msg, **log_details)
            raise RuntimeError(msg)

        log.info(
            "createERC20TokenNetwork successful",
            token_network_address=pex(token_network_address),
            **log_details,
        )

        return token_network_address
Exemple #18
0
    def approve(self, allowed_address: Address,
                allowance: TokenAmount) -> None:
        """ Approve `allowed_address` to transfer up to `deposit` amount of token.

        Note:

            For channel deposit please use the channel proxy, since it does
            additional validations.
            We assume there to be sufficient balance as a precondition if this
            is called, so it is not checked as a precondition here.
        """
        # Note that given_block_identifier is not used here as there
        # are no preconditions to check before sending the transaction
        # There are no direct calls to this method in any event handler,
        # so a precondition check would make no sense.
        with self.token_lock:
            log_details: Dict[str, Any] = {}

            error_prefix = "Call to approve will fail"
            estimated_transaction = self.client.estimate_gas(
                self.proxy, "approve", log_details, allowed_address, allowance)

            if estimated_transaction is not None:
                error_prefix = "Call to approve failed"
                transaction_sent = self.client.transact(estimated_transaction)
                transaction_mined = self.client.poll_transaction(
                    transaction_sent)

                if not was_transaction_successfully_mined(transaction_mined):
                    failed_receipt = transaction_mined.receipt
                    failed_at_blockhash = encode_hex(
                        failed_receipt["blockHash"])

                    check_transaction_failure(transaction_mined, self.client)

                    balance = self.balance_of(self.client.address,
                                              failed_at_blockhash)
                    if balance < allowance:
                        msg = (f"{error_prefix} Your balance of {balance} is "
                               "below the required amount of {allowance}.")
                        if balance == 0:
                            msg += (
                                " Note: The balance was 0, which may also happen "
                                "if the contract is not a valid ERC20 token "
                                "(balanceOf method missing).")
                        raise RaidenRecoverableError(msg)

                    raise RaidenRecoverableError(
                        f"{error_prefix}. The reason is unknown, you have enough tokens for "
                        f"the requested allowance and enough eth to pay the gas. There may "
                        f"be a problem with the token contract.")

            else:
                failed_at = self.client.get_block(BLOCK_ID_LATEST)
                failed_at_blockhash = encode_hex(failed_at["hash"])
                failed_at_blocknumber = failed_at["number"]

                self.client.check_for_insufficient_eth(
                    transaction_name="approve",
                    transaction_executed=False,
                    required_gas=GAS_REQUIRED_FOR_APPROVE,
                    block_identifier=failed_at_blocknumber,
                )

                balance = self.balance_of(self.client.address,
                                          failed_at_blockhash)
                if balance < allowance:
                    msg = (f"{error_prefix} Your balance of {balance} is "
                           "below the required amount of {allowance}.")
                    if balance == 0:
                        msg += (
                            " Note: The balance was 0, which may also happen if the contract "
                            "is not a valid ERC20 token (balanceOf method missing)."
                        )
                    raise RaidenRecoverableError(msg)

                raise RaidenRecoverableError(
                    f"{error_prefix} Gas estimation failed for unknown reason. "
                    f"Please make sure the contract is a valid ERC20 token.")
    def update_transfer(
        self,
        channel_identifier: typing.ChannelID,
        partner: typing.Address,
        balance_hash: typing.BalanceHash,
        nonce: typing.Nonce,
        additional_hash: typing.AdditionalHash,
        closing_signature: typing.Signature,
        non_closing_signature: typing.Signature,
    ):
        log_details = {
            'token_network': pex(self.address),
            'node': pex(self.node_address),
            'partner': pex(partner),
            'nonce': nonce,
            'balance_hash': encode_hex(balance_hash),
            'additional_hash': encode_hex(additional_hash),
            'closing_signature': encode_hex(closing_signature),
            'non_closing_signature': encode_hex(non_closing_signature),
        }
        log.debug('updateNonClosingBalanceProof called', **log_details)

        data_that_was_signed = pack_balance_proof(
            nonce=nonce,
            balance_hash=balance_hash,
            additional_hash=additional_hash,
            channel_identifier=channel_identifier,
            token_network_identifier=typing.TokenNetworkID(self.address),
            chain_id=self.proxy.contract.functions.chain_id().call(),
        )

        try:
            signer_address = eth_recover(
                data=data_that_was_signed,
                signature=closing_signature,
            )

            # InvalidSignature is raised by eth_utils.eth_recover if signature
            # is not bytes or has the incorrect length
            #
            # ValueError is raised if the PublicKey instantiation failed, let it
            # propagate because it's a memory pressure problem.
            #
            # Exception is raised if the public key recovery failed.
        except Exception:  # pylint: disable=broad-except
            msg = "Couldn't verify the balance proof signature"
            log.critical(f'updateNonClosingBalanceProof failed, {msg}',
                         **log_details)
            raise RaidenUnrecoverableError(msg)

        if signer_address != partner:
            msg = 'Invalid balance proof signature'
            log.critical(f'updateNonClosingBalanceProof failed, {msg}',
                         **log_details)
            raise RaidenUnrecoverableError(msg)

        self._check_for_outdated_channel(
            self.node_address,
            partner,
            channel_identifier,
        )

        detail = self.detail_channel(
            participant1=self.node_address,
            participant2=partner,
            channel_identifier=channel_identifier,
        )
        if detail.state != ChannelState.CLOSED:
            msg = 'Channel is not in a closed state'
            log.critical(f'updateNonClosingBalanceProof failed, {msg}',
                         **log_details)
            raise RaidenUnrecoverableError(msg)

        if detail.settle_block_number < self.client.block_number():
            msg = ('updateNonClosingBalanceProof cannot be called '
                   'because the settlement period is over')
            log.critical(f'updateNonClosingBalanceProof failed, {msg}',
                         **log_details)
            raise RaidenRecoverableError(msg)

        self._check_channel_state_for_update(
            channel_identifier=channel_identifier,
            closer=partner,
            update_nonce=nonce,
            log_details=log_details,
        )

        transaction_hash = self.proxy.transact(
            'updateNonClosingBalanceProof',
            safe_gas_limit(GAS_REQUIRED_FOR_UPDATE_BALANCE_PROOF),
            channel_identifier,
            partner,
            self.node_address,
            balance_hash,
            nonce,
            additional_hash,
            closing_signature,
            non_closing_signature,
        )

        self.client.poll(transaction_hash)

        receipt_or_none = check_transaction_threw(self.client,
                                                  transaction_hash)
        if receipt_or_none:
            if detail.settle_block_number < receipt_or_none['blockNumber']:
                msg = ('updateNonClosingBalanceProof transaction '
                       'was mined after settlement finished')
                log.critical(f'updateNonClosingBalanceProof failed, {msg}',
                             **log_details)
                raise RaidenRecoverableError(msg)

            self._check_channel_state_for_update(
                channel_identifier=channel_identifier,
                closer=partner,
                update_nonce=nonce,
                log_details=log_details,
            )

            # This should never happen if the settlement window and gas price
            # estimation is done properly
            channel_settled = self.channel_is_settled(
                participant1=self.node_address,
                participant2=partner,
                channel_identifier=channel_identifier,
            )
            if channel_settled is True:
                msg = 'Channel is settled'
                log.critical(f'updateNonClosingBalanceProof failed, {msg}',
                             **log_details)
                raise RaidenRecoverableError(msg)

            msg = 'Update NonClosing balance proof'
            log.critical(f'updateNonClosingBalanceProof failed, {msg}',
                         **log_details)
            raise TransactionThrew(msg, receipt_or_none)

        log.info('updateNonClosingBalanceProof successful', **log_details)
    def _deposit_check_result(
        self,
        transaction_sent: Optional[TransactionSent],
        token: Token,
        beneficiary: Address,
        total_deposit: TokenAmount,
        amount_to_deposit: TokenAmount,
    ) -> None:
        if transaction_sent is None:
            failed_at = self.client.get_block(BLOCK_ID_LATEST)
            failed_at_blocknumber = failed_at["number"]

            self.client.check_for_insufficient_eth(
                transaction_name="deposit",
                transaction_executed=False,
                required_gas=self.gas_measurements["UserDeposit.deposit"],
                block_identifier=failed_at_blocknumber,
            )

            latest_deposit = self.get_total_deposit(
                address=beneficiary, block_identifier=failed_at_blocknumber
            )
            amount_to_deposit = TokenAmount(total_deposit - latest_deposit)

            allowance = token.allowance(
                owner=self.node_address,
                spender=Address(self.address),
                block_identifier=failed_at_blocknumber,
            )
            whole_balance = self.whole_balance(block_identifier=failed_at_blocknumber)
            whole_balance_limit = self.whole_balance_limit(block_identifier=failed_at_blocknumber)

            if allowance < amount_to_deposit:
                msg = (
                    "The allowance is insufficient. Check concurrent deposits "
                    "for the same user deposit but different proxies."
                )
                raise RaidenRecoverableError(msg)

            if token.balance_of(self.node_address, failed_at_blocknumber) < amount_to_deposit:
                msg = "The address doesnt have enough tokens"
                raise RaidenRecoverableError(msg)

            if latest_deposit < total_deposit:
                msg = "Deposit amount did not increase after deposit transaction"
                raise RaidenRecoverableError(msg)

            if whole_balance + amount_to_deposit > UINT256_MAX:
                msg = (
                    f"Current whole balance is {whole_balance}. "
                    f"The new deposit of {amount_to_deposit} would lead to an overflow."
                )
                raise RaidenRecoverableError(msg)

            if whole_balance + amount_to_deposit > whole_balance_limit:
                msg = (
                    f"Current whole balance is {whole_balance}. "
                    f"With the new deposit of {amount_to_deposit}, the deposit "
                    f"limit of {whole_balance_limit} would be exceeded."
                )
                raise RaidenRecoverableError(msg)

            raise RaidenRecoverableError("Deposit failed of unknown reason")

        else:
            transaction_mined = self.client.poll_transaction(transaction_sent)

            if not was_transaction_successfully_mined(transaction_mined):
                failed_at_blocknumber = BlockNumber(transaction_mined.receipt["blockNumber"])

                latest_deposit = self.get_total_deposit(
                    address=beneficiary, block_identifier=failed_at_blocknumber
                )
                amount_to_deposit = TokenAmount(total_deposit - latest_deposit)

                allowance = token.allowance(
                    owner=self.node_address,
                    spender=Address(self.address),
                    block_identifier=failed_at_blocknumber,
                )

                whole_balance = self.whole_balance(block_identifier=failed_at_blocknumber)
                whole_balance_limit = self.whole_balance_limit(
                    block_identifier=failed_at_blocknumber
                )

                if latest_deposit >= total_deposit:
                    msg = "Deposit amount already increased after another transaction"
                    raise RaidenRecoverableError(msg)

                if allowance < amount_to_deposit:
                    msg = (
                        "The allowance is insufficient. Check concurrent deposits "
                        "for the same token network but different proxies."
                    )
                    raise RaidenRecoverableError(msg)

                # Because we acquired the lock for the token, and the gas estimation succeeded,
                # We know that the account had enough balance for the deposit transaction.
                if token.balance_of(self.node_address, failed_at_blocknumber) < amount_to_deposit:
                    msg = (
                        "Transaction failed and balance decreased unexpectedly. "
                        "This could be a bug in Raiden or a mallicious "
                        "ERC20 Token."
                    )
                    raise RaidenRecoverableError(msg)

                if whole_balance + amount_to_deposit > UINT256_MAX:
                    msg = (
                        f"Current whole balance is {whole_balance}. "
                        f"The new deposit of {amount_to_deposit} caused an overflow."
                    )
                    raise RaidenRecoverableError(msg)

                if whole_balance + amount_to_deposit > whole_balance_limit:
                    msg = (
                        f"Current whole balance is {whole_balance}. "
                        f"With the new deposit of {amount_to_deposit}, the deposit "
                        f"limit of {whole_balance_limit} was exceeded."
                    )
                    raise RaidenRecoverableError(msg)

                if latest_deposit < total_deposit:
                    msg = "Deposit amount did not increase after deposit transaction"
                    raise RaidenRecoverableError(msg)

                raise RaidenRecoverableError("Deposit failed of unknown reason")
    def _init(
        self, monitoring_service_address: MonitoringServiceAddress, one_to_n_address: OneToNAddress
    ) -> None:
        log_details: Dict[str, Any] = {}
        estimated_transaction = self.client.estimate_gas(
            self.proxy, "init", log_details, monitoring_service_address, one_to_n_address
        )

        if estimated_transaction is None:
            failed_at = self.client.get_block(BLOCK_ID_LATEST)
            failed_at_blocknumber = failed_at["number"]

            self.client.check_for_insufficient_eth(
                transaction_name="init",
                transaction_executed=False,
                required_gas=self.gas_measurements["UserDeposit.init"],
                block_identifier=failed_at_blocknumber,
            )

            existing_monitoring_service_address = self.monitoring_service_address(
                block_identifier=failed_at_blocknumber
            )
            existing_one_to_n_address = self.one_to_n_address(
                block_identifier=failed_at_blocknumber
            )
            if existing_monitoring_service_address != EMPTY_ADDRESS:
                msg = (
                    f"MonitoringService contract address was set to "
                    f"{to_checksum_address(existing_monitoring_service_address)}"
                )
                raise RaidenRecoverableError(msg)

            if existing_one_to_n_address != EMPTY_ADDRESS:
                msg = (
                    f"OneToN contract address was set to "
                    f"{to_checksum_address(existing_one_to_n_address)}"
                )
                raise RaidenRecoverableError(msg)

            raise RaidenRecoverableError("Deposit failed of unknown reason")

        else:
            transaction_sent = self.client.transact(estimated_transaction)
            transaction_mined = self.client.poll_transaction(transaction_sent)

            if not was_transaction_successfully_mined(transaction_mined):
                failed_at_blocknumber = BlockNumber(transaction_mined.receipt["blockNumber"])

                existing_monitoring_service_address = self.monitoring_service_address(
                    block_identifier=failed_at_blocknumber
                )
                existing_one_to_n_address = self.one_to_n_address(
                    block_identifier=failed_at_blocknumber
                )
                if existing_monitoring_service_address != EMPTY_ADDRESS:
                    msg = (
                        f"MonitoringService contract address was set to "
                        f"{to_checksum_address(existing_monitoring_service_address)}"
                    )
                    raise RaidenRecoverableError(msg)

                if existing_one_to_n_address != EMPTY_ADDRESS:
                    msg = (
                        f"OneToN contract address was set to "
                        f"{to_checksum_address(existing_one_to_n_address)}"
                    )
                    raise RaidenRecoverableError(msg)

                raise RaidenRecoverableError("Deposit failed of unknown reason")
Exemple #22
0
    def _register_secret_batch(
        self,
        secrets_to_register: List[Secret],
        transaction_result: AsyncResult,
        log_details: Dict[Any, Any],
    ) -> None:
        checking_block = self.client.get_checking_block()
        gas_limit = self.proxy.estimate_gas(checking_block,
                                            "registerSecretBatch",
                                            secrets_to_register)
        receipt = None
        transaction_hash = None
        msg = None

        if gas_limit:
            gas_limit = safe_gas_limit(
                gas_limit,
                len(secrets_to_register) * GAS_REQUIRED_PER_SECRET_IN_BATCH)
            log_details["gas_limit"] = gas_limit

            try:
                transaction_hash = self.proxy.transact("registerSecretBatch",
                                                       gas_limit,
                                                       secrets_to_register)
                receipt = self.client.poll(transaction_hash)
            except Exception as e:  # pylint: disable=broad-except
                msg = f"Unexpected exception {e} at sending registerSecretBatch transaction."

        # Clear `open_secret_transactions` regardless of the transaction being
        # successfully executed or not.
        with self._open_secret_transactions_lock:
            for secret in secrets_to_register:
                self.open_secret_transactions.pop(secret)

        # As of version `0.4.0` of the contract has *no* asserts or requires.
        # Therefore the only reason for the transaction to fail is if there is
        # a bug.
        unrecoverable_error = (gas_limit is None or receipt is None
                               or receipt["status"] == RECEIPT_FAILURE_CODE)

        exception: Union[RaidenRecoverableError, RaidenUnrecoverableError]
        if unrecoverable_error:
            # If the transaction was sent it must not fail. If this happened
            # some of our assumptions is broken therefore the error is
            # unrecoverable
            if receipt is not None:
                if receipt["gasUsed"] == gas_limit:
                    # The transaction failed and all gas was used. This can
                    # happen because of:
                    #
                    # - A compiler bug if an invalid opcode was executed.
                    # - A configuration bug if an assert was executed,
                    # because version 0.4.0 of the secret registry does not have an
                    # assert.
                    # - An ethereum client bug if the gas_limit was
                    # underestimated.
                    #
                    # Safety cannot be guaranteed under any of these cases,
                    # this error is unrecoverable.
                    error = (
                        "Secret registration failed because of a bug in either "
                        "the solidity compiler, the running ethereum client, or "
                        "a configuration error in Raiden.")
                else:
                    # The transaction failed and *not* all gas was used. This
                    # can happen because of:
                    #
                    # - A compiler bug if a revert was introduced.
                    # - A configuration bug, because for 0.4.0 the secret
                    # registry does not have a revert.
                    error = (
                        "Secret registration failed because of a configuration "
                        "bug or compiler bug. Please double check the secret "
                        "smart contract is at version 0.4.0, if it is then a "
                        "compiler bug was hit.")

                exception = RaidenUnrecoverableError(error)
                transaction_result.set_exception(exception)
                raise exception

            # If gas_limit is set and there is no receipt then an exception was
            # raised while sending the transaction. This should only happen if
            # the account is being used concurrently, which is not supported.
            # This can happen because:
            #
            # - The nonce of the transaction was already used
            # - The nonce was reused *and* the account didn't have enough ether
            # to pay for the gas
            #
            # Safety cannot be guaranteed under any of these cases, this error
            # is unrecoverable. *Note*: This assumes the ethereum client
            # takes into account the current transactions in the pool.
            if gas_limit:
                assert msg, "Unexpected control flow, an exception should have been raised."
                error = (
                    f"Sending the the transaction for registerSecretBatch "
                    f"failed with: `{msg}`.  This happens if the same ethereum "
                    f"account is being used by more than one program which is not "
                    f"supported.")

                exception = RaidenUnrecoverableError(error)
                transaction_result.set_exception(exception)
                raise exception

            # gas_limit can fail because:
            #
            # - The Ethereum client detected the transaction could not
            # successfully execute, this happens if an assert/revert is hit.
            # - The account is lacking funds to pay for the gas.
            #
            # Either of these is a bug. The contract does not use
            # assert/revert, and the account should always be funded
            self.proxy.jsonrpc_client.check_for_insufficient_eth(
                transaction_name="registerSecretBatch",
                transaction_executed=True,
                required_gas=gas_limit,
                block_identifier=checking_block,
            )
            error = "Call to registerSecretBatch couldn't be done"

            exception = RaidenRecoverableError(error)
            transaction_result.set_exception(exception)
            raise exception

        # The local **MUST** transaction_result be set before waiting for the
        # other results, otherwise we have a dead-lock
        transaction_result.set(transaction_hash)
Exemple #23
0
    def _init(
        self,
        monitoring_service_address: MonitoringServiceAddress,
        one_to_n_address: OneToNAddress,
        log_details: Dict[str, Any],
    ) -> None:
        checking_block = self.client.get_checking_block()
        gas_limit = self.proxy.estimate_gas(
            checking_block,
            "init",
            to_checksum_address(monitoring_service_address),
            to_checksum_address(one_to_n_address),
        )

        if not gas_limit:
            failed_at = self.proxy.rpc_client.get_block("latest")
            failed_at_blocknumber = failed_at["number"]

            self.proxy.rpc_client.check_for_insufficient_eth(
                transaction_name="init",
                transaction_executed=False,
                required_gas=self.gas_measurements["UserDeposit.init"],
                block_identifier=failed_at_blocknumber,
            )

            existing_monitoring_service_address = self.monitoring_service_address(
                block_identifier=failed_at_blocknumber)
            existing_one_to_n_address = self.one_to_n_address(
                block_identifier=failed_at_blocknumber)
            if existing_monitoring_service_address != EMPTY_ADDRESS:
                msg = (
                    f"MonitoringService contract address was set to "
                    f"{to_checksum_address(existing_monitoring_service_address)}"
                )
                raise RaidenRecoverableError(msg)

            if existing_one_to_n_address != EMPTY_ADDRESS:
                msg = (f"OneToN contract address was set to "
                       f"{to_checksum_address(existing_one_to_n_address)}")
                raise RaidenRecoverableError(msg)

            raise RaidenRecoverableError("Deposit failed of unknown reason")

        else:
            gas_limit = safe_gas_limit(gas_limit)
            log_details["gas_limit"] = gas_limit

            transaction_hash = self.proxy.transact(
                "init",
                gas_limit,
                to_checksum_address(monitoring_service_address),
                to_checksum_address(one_to_n_address),
            )

            receipt = self.client.poll(transaction_hash)
            failed_receipt = check_transaction_threw(receipt=receipt)

            if failed_receipt:
                failed_at_blocknumber = failed_receipt["blockNumber"]

                existing_monitoring_service_address = self.monitoring_service_address(
                    block_identifier=failed_at_blocknumber)
                existing_one_to_n_address = self.one_to_n_address(
                    block_identifier=failed_at_blocknumber)
                if existing_monitoring_service_address != EMPTY_ADDRESS:
                    msg = (
                        f"MonitoringService contract address was set to "
                        f"{to_checksum_address(existing_monitoring_service_address)}"
                    )
                    raise RaidenRecoverableError(msg)

                if existing_one_to_n_address != EMPTY_ADDRESS:
                    msg = (f"OneToN contract address was set to "
                           f"{to_checksum_address(existing_one_to_n_address)}")
                    raise RaidenRecoverableError(msg)

                raise RaidenRecoverableError(
                    "Deposit failed of unknown reason")
Exemple #24
0
    def register_secret_batch(self, secrets: List[Secret]):
        """Register a batch of secrets. Check if they are already registered at
        the given block identifier."""
        secrets_to_register = list()
        secrethashes_to_register = list()
        secrethashes_not_sent = list()
        transaction_result = AsyncResult()
        wait_for = set()

        # secret registration has no preconditions:
        #
        # - The action does not depend on any state, it's always valid to call
        #   it.
        # - This action is always susceptible to race conditions.
        #
        # Therefore this proxy only needs to detect if the secret is already
        # registered, to avoid sending obviously unecessary transactions, and
        # it has to handle race conditions.

        with self._open_secret_transactions_lock:
            verification_block_hash = self.client.get_confirmed_blockhash()

            for secret in secrets:
                secrethash = sha3(secret)
                secrethash_hex = encode_hex(secrethash)

                # Do the local test on `open_secret_transactions` first, then
                # if necessary do an RPC call.
                #
                # The call to `is_secret_registered` has two conflicting
                # requirements:
                #
                # - Avoid sending duplicated transactions for the same lock
                # - Operating on a consistent/confirmed view of the blockchain
                #   (if a secret has been registered in a block that is not
                #   confirmed it doesn't count yet, an optimization would be to
                #   *not* send the transaction and wait for the confirmation)
                #
                # The code below respects the consistent blockchain view,
                # meaning that if this proxy method is called with an old
                # blockhash an unecessary transaction will be sent, and the
                # error will be treated as a race-condition.
                other_result = self.open_secret_transactions.get(secret)

                if other_result is not None:
                    wait_for.add(other_result)
                    secrethashes_not_sent.append(secrethash_hex)
                elif not self.is_secret_registered(secrethash,
                                                   verification_block_hash):
                    secrets_to_register.append(secret)
                    secrethashes_to_register.append(secrethash_hex)
                    self.open_secret_transactions[secret] = transaction_result

        # From here on the lock is not required. Context-switches will happen
        # for the gas estimation and the transaction, however the
        # synchronization data is limited to the open_secret_transactions
        log_details = {
            "node": pex(self.node_address),
            "contract": pex(self.address),
            "secrethashes": secrethashes_to_register,
            "secrethashes_not_sent": secrethashes_not_sent,
        }

        if not secrets_to_register:
            log.debug("registerSecretBatch skipped, waiting for transactions",
                      **log_details)

            gevent.joinall(wait_for, raise_error=True)

            log.info("registerSecretBatch successful", **log_details)
            return

        checking_block = self.client.get_checking_block()
        gas_limit = self.proxy.estimate_gas(checking_block,
                                            "registerSecretBatch",
                                            secrets_to_register)
        receipt = None
        transaction_hash = None
        msg = None

        if gas_limit:
            gas_limit = safe_gas_limit(
                gas_limit,
                len(secrets_to_register) * GAS_REQUIRED_PER_SECRET_IN_BATCH)

            log.debug("registerSecretBatch called", **log_details)

            try:
                transaction_hash = self.proxy.transact("registerSecretBatch",
                                                       gas_limit,
                                                       secrets_to_register)
                self.client.poll(transaction_hash)
                receipt = self.client.get_transaction_receipt(transaction_hash)
            except Exception as e:  # pylint: disable=broad-except
                msg = f"Unexpected exception {e} at sending registerSecretBatch transaction."

        # Clear `open_secret_transactions` regardless of the transaction being
        # successfully executed or not.
        with self._open_secret_transactions_lock:
            for secret in secrets_to_register:
                self.open_secret_transactions.pop(secret)

        # As of version `0.4.0` of the contract has *no* asserts or requires.
        # Therefore the only reason for the transaction to fail is if there is
        # a bug.
        unrecoverable_error = (gas_limit is None or receipt is None
                               or receipt["status"] == RECEIPT_FAILURE_CODE)

        exception: Union[RaidenRecoverableError, RaidenUnrecoverableError]
        if unrecoverable_error:
            # If the transaction was sent it must not fail. If this happened
            # some of our assumptions is broken therefore the error is
            # unrecoverable
            if receipt is not None:
                if receipt["gasUsed"] == gas_limit:
                    # The transaction failed and all gas was used. This can
                    # happen because of:
                    #
                    # - A compiler bug if an invalid opcode was executed.
                    # - A configuration bug if an assert was executed,
                    # because version 0.4.0 of the secret registry does not have an
                    # assert.
                    # - An ethereum client bug if the gas_limit was
                    # underestimated.
                    #
                    # Safety cannot be guaranteed under any of these cases,
                    # this error is unrecoverable.
                    error = (
                        "Secret registration failed because of a bug in either "
                        "the solidity compiler, the running ethereum client, or "
                        "a configuration error in Raiden.")
                else:
                    # The transaction failed and *not* all gas was used. This
                    # can happen because of:
                    #
                    # - A compiler bug if a revert was introduced.
                    # - A configuration bug, because for 0.4.0 the secret
                    # registry does not have a revert.
                    error = (
                        "Secret registration failed because of a configuration "
                        "bug or compiler bug. Please double check the secret "
                        "smart contract is at version 0.4.0, if it is then a "
                        "compiler bug was hit.")

                log.critical(error, **log_details)
                exception = RaidenUnrecoverableError(error)
                transaction_result.set_exception(exception)
                raise exception

            # If gas_limit is set and there is no receipt then an exception was
            # raised while sending the transaction. This should only happen if
            # the account is being used concurrently, which is not supported.
            # This can happen because:
            #
            # - The nonce of the transaction was already used
            # - The nonce was reused *and* the account didn't have enough ether
            # to pay for the gas
            #
            # Safety cannot be guaranteed under any of these cases, this error
            # is unrecoverable. *Note*: This assumes the ethereum client
            # takes into account the current transactions in the pool.
            if gas_limit:
                assert msg, "Unexpected control flow, an exception should have been raised."
                error = (
                    f"Sending the the transaction for registerSecretBatch failed with: `{msg}`. "
                    f"This happens if the same ethereum account is being used by more than one "
                    f"program which is not supported.")

                log.critical(error, **log_details)
                exception = RaidenUnrecoverableError(error)
                transaction_result.set_exception(exception)
                raise exception

            # gas_limit can fail because:
            #
            # - The Ethereum client detected the transaction could not
            # successfully execute, this happens if an assert/revert is hit.
            # - The account is lacking funds to pay for the gas.
            #
            # Either of these is a bug. The contract does not use
            # assert/revert, and the account should always be funded
            self.proxy.jsonrpc_client.check_for_insufficient_eth(
                transaction_name="registerSecretBatch",
                transaction_executed=True,
                required_gas=gas_limit,
                block_identifier=checking_block,
            )
            error = "Call to registerSecretBatch couldn't be done"

            log.critical(error, **log_details)
            exception = RaidenRecoverableError(error)
            transaction_result.set_exception(exception)
            raise exception

        # The local **MUST** transaction_result be set before waiting for the
        # other results, otherwise we have a dead-lock
        transaction_result.set(transaction_hash)

        if wait_for:
            log.info("registerSecretBatch waiting for pending", **log_details)
            gevent.joinall(wait_for, raise_error=True)

        log.info("registerSecretBatch successful", **log_details)
    def _add_token(
        self,
        token_address: TokenAddress,
        channel_participant_deposit_limit: TokenAmount,
        token_network_deposit_limit: TokenAmount,
        log_details: Dict[Any, Any],
    ) -> TokenNetworkAddress:
        token_network_address = None

        checking_block = self.rpc_client.get_checking_block()

        kwargs = {
            "_token_address": token_address,
            "_channel_participant_deposit_limit":
            channel_participant_deposit_limit,
            "_token_network_deposit_limit": token_network_deposit_limit,
        }
        gas_limit = self.proxy.estimate_gas(checking_block,
                                            "createERC20TokenNetwork",
                                            **kwargs)

        if gas_limit:
            gas_limit = safe_gas_limit(
                gas_limit, self.gas_measurements[
                    "TokenNetworkRegistry createERC20TokenNetwork"])
            log_details["gas_limit"] = gas_limit
            transaction_hash = self.proxy.transact("createERC20TokenNetwork",
                                                   gas_limit, **kwargs)

            receipt = self.rpc_client.poll(transaction_hash)
            failed_receipt = check_transaction_threw(receipt=receipt)

            if failed_receipt:
                failed_at_blocknumber = failed_receipt["blockNumber"]

                max_token_networks = self.get_max_token_networks(
                    block_identifier=failed_at_blocknumber)
                token_networks_created = self.get_token_network_created(
                    block_identifier=failed_at_blocknumber)
                already_registered = self.get_token_network(
                    token_address=token_address,
                    block_identifier=failed_at_blocknumber)
                deprecation_executor = self.get_deprecation_executor(
                    block_identifier=failed_at_blocknumber)
                settlement_timeout_min = self.settlement_timeout_min(
                    block_identifier=failed_at_blocknumber)
                settlement_timeout_max = self.settlement_timeout_max(
                    block_identifier=failed_at_blocknumber)
                chain_id = self.get_chain_id(
                    block_identifier=failed_at_blocknumber)
                secret_registry_address = self.get_secret_registry_address(
                    block_identifier=failed_at_blocknumber)

                if failed_receipt["cumulativeGasUsed"] == gas_limit:
                    msg = (
                        f"createERC20TokenNetwork failed and all gas was used "
                        f"({gas_limit}). Estimate gas may have underestimated "
                        f"createERC20TokenNetwork, or succeeded even though an assert is "
                        f"triggered, or the smart contract code has an "
                        f"conditional assert.")
                    raise RaidenRecoverableError(msg)

                if token_networks_created >= max_token_networks:
                    raise RaidenRecoverableError(
                        "The number of existing token networks reached the maximum allowed"
                    )

                if already_registered:
                    # Race condition lost, the token network was created in a different
                    # transaction which got mined first.
                    raise RaidenRecoverableError(
                        "The token was already registered in the TokenNetworkRegistry."
                    )

                if deprecation_executor == NULL_ADDRESS_BYTES:
                    raise RaidenUnrecoverableError(
                        "The deprecation executor property for the "
                        "TokenNetworkRegistry is invalid.")

                if chain_id == 0:
                    raise RaidenUnrecoverableError(
                        "The chain ID property for the TokenNetworkRegistry is invalid."
                    )

                if secret_registry_address == NULL_ADDRESS_BYTES:
                    raise RaidenUnrecoverableError(
                        "The secret registry address for the token network is invalid."
                    )

                if settlement_timeout_min == 0:
                    raise RaidenUnrecoverableError(
                        "The minimum settlement timeout for the token network "
                        "should be larger than zero.")

                if settlement_timeout_min == 0:
                    raise RaidenUnrecoverableError(
                        "The minimum settlement timeout for the token network "
                        "should be larger than zero.")

                if settlement_timeout_max <= settlement_timeout_min:
                    raise RaidenUnrecoverableError(
                        "The maximum settlement timeout for the token network "
                        "should be larger than the minimum settlement timeout."
                    )

                # At this point, the TokenNetworkRegistry fails to instantiate
                # a new TokenNetwork.
                raise RaidenUnrecoverableError(
                    "createERC20TokenNetwork failed for an unknown reason")

            token_network_address = self.get_token_network(
                token_address, receipt["blockHash"])
            if token_network_address is None:
                msg = "createERC20TokenNetwork succeeded but token network address is Null"
                raise RaidenUnrecoverableError(msg)
        else:
            # The latest block can not be used reliably because of reorgs,
            # therefore every call using this block has to handle pruned data.
            failed_at = self.proxy.rpc_client.get_block("latest")
            failed_at_blocknumber = failed_at["number"]

            max_token_networks = self.get_max_token_networks(
                block_identifier=failed_at_blocknumber)
            token_networks_created = self.get_token_network_created(
                block_identifier=failed_at_blocknumber)

            already_registered = self.get_token_network(
                token_address=token_address,
                block_identifier=failed_at_blocknumber)
            deprecation_executor = self.get_deprecation_executor(
                block_identifier=failed_at_blocknumber)
            settlement_timeout_min = self.settlement_timeout_min(
                block_identifier=failed_at_blocknumber)
            settlement_timeout_max = self.settlement_timeout_max(
                block_identifier=failed_at_blocknumber)
            chain_id = self.get_chain_id(
                block_identifier=failed_at_blocknumber)
            secret_registry_address = self.get_secret_registry_address(
                block_identifier=failed_at_blocknumber)

            required_gas = (gas_limit if gas_limit else self.gas_measurements[
                "TokenNetworkRegistry createERC20TokenNetwork"])
            self.proxy.rpc_client.check_for_insufficient_eth(
                transaction_name="createERC20TokenNetwork",
                transaction_executed=False,
                required_gas=required_gas,
                block_identifier=failed_at_blocknumber,
            )

            if token_networks_created >= max_token_networks:
                raise RaidenRecoverableError(
                    "The number of existing token networks reached the maximum allowed"
                )

            if already_registered:
                # Race condition lost, the token network was created in a different
                # transaction which got mined first.
                raise RaidenRecoverableError(
                    "The token was already registered in the TokenNetworkRegistry."
                )

            if deprecation_executor == NULL_ADDRESS_BYTES:
                raise RaidenUnrecoverableError(
                    "The deprecation executor property for the "
                    "TokenNetworkRegistry is invalid.")

            if chain_id == 0:
                raise RaidenUnrecoverableError(
                    "The chain ID property for the TokenNetworkRegistry is invalid."
                )

            if chain_id != self.rpc_client.chain_id:
                raise RaidenUnrecoverableError(
                    f"The provided chain ID {chain_id} does not match the "
                    f"network Raiden is running on: {self.rpc_client.chain_id}."
                )

            if secret_registry_address == NULL_ADDRESS_BYTES:
                raise RaidenUnrecoverableError(
                    "The secret registry address for the token network is invalid."
                )

            if settlement_timeout_min == 0:
                raise RaidenUnrecoverableError(
                    "The minimum settlement timeout for the token network "
                    "should be larger than zero.")

            if settlement_timeout_min == 0:
                raise RaidenUnrecoverableError(
                    "The minimum settlement timeout for the token network "
                    "should be larger than zero.")

            if settlement_timeout_max <= settlement_timeout_min:
                raise RaidenUnrecoverableError(
                    "The maximum settlement timeout for the token network "
                    "should be larger than the minimum settlement timeout.")

            if self.get_token_network(token_address, failed_at_blocknumber):
                raise RaidenRecoverableError("Token already registered")

            # At this point, the TokenNetworkRegistry fails to instantiate
            # a new TokenNetwork.
            raise RaidenUnrecoverableError(
                "createERC20TokenNetwork failed for an unknown reason")
        return token_network_address
Exemple #26
0
    def _deposit(
        self,
        beneficiary: Address,
        token: Token,
        total_deposit: TokenAmount,
        amount_to_deposit: TokenAmount,
        log_details: Dict[str, Any],
    ) -> None:
        token.approve(allowed_address=Address(self.address),
                      allowance=amount_to_deposit)

        checking_block = self.client.get_checking_block()
        gas_limit = self.proxy.estimate_gas(checking_block, "deposit",
                                            to_checksum_address(beneficiary),
                                            total_deposit)

        if not gas_limit:
            failed_at = self.proxy.rpc_client.get_block("latest")
            failed_at_blocknumber = failed_at["number"]

            self.proxy.rpc_client.check_for_insufficient_eth(
                transaction_name="deposit",
                transaction_executed=False,
                required_gas=self.gas_measurements["UserDeposit.deposit"],
                block_identifier=failed_at_blocknumber,
            )

            latest_deposit = self.get_total_deposit(
                address=self.node_address,
                block_identifier=failed_at_blocknumber)
            amount_to_deposit = TokenAmount(total_deposit - latest_deposit)

            allowance = token.allowance(
                owner=self.node_address,
                spender=Address(self.address),
                block_identifier=failed_at_blocknumber,
            )
            whole_balance = self.whole_balance(
                block_identifier=failed_at_blocknumber)
            whole_balance_limit = self.whole_balance_limit(
                block_identifier=failed_at_blocknumber)

            if allowance < amount_to_deposit:
                msg = (
                    "The allowance is insufficient. Check concurrent deposits "
                    "for the same user deposit but different proxies.")
                raise RaidenRecoverableError(msg)

            if token.balance_of(self.node_address,
                                failed_at_blocknumber) < amount_to_deposit:
                msg = "The address doesnt have enough tokens"
                raise RaidenRecoverableError(msg)

            if latest_deposit < total_deposit:
                msg = "Deposit amount did not increase after deposit transaction"
                raise RaidenRecoverableError(msg)

            if whole_balance + amount_to_deposit > UINT256_MAX:
                msg = (
                    f"Current whole balance is {whole_balance}. "
                    f"The new deposit of {amount_to_deposit} would lead to an overflow."
                )
                raise RaidenRecoverableError(msg)

            if whole_balance + amount_to_deposit > whole_balance_limit:
                msg = (
                    f"Current whole balance is {whole_balance}. "
                    f"With the new deposit of {amount_to_deposit}, the deposit "
                    f"limit of {whole_balance_limit} would be exceeded.")
                raise RaidenRecoverableError(msg)

            raise RaidenRecoverableError("Deposit failed of unknown reason")

        else:
            gas_limit = safe_gas_limit(gas_limit)
            log_details["gas_limit"] = gas_limit

            transaction_hash = self.proxy.transact(
                "deposit", gas_limit, to_checksum_address(beneficiary),
                total_deposit)

            receipt = self.client.poll(transaction_hash)
            failed_receipt = check_transaction_threw(receipt=receipt)

            if failed_receipt:
                failed_at_blocknumber = failed_receipt["blockNumber"]

                latest_deposit = self.get_total_deposit(
                    address=self.node_address,
                    block_identifier=failed_at_blocknumber)
                amount_to_deposit = TokenAmount(total_deposit - latest_deposit)

                allowance = token.allowance(
                    owner=self.node_address,
                    spender=Address(self.address),
                    block_identifier=failed_at_blocknumber,
                )

                whole_balance = self.whole_balance(
                    block_identifier=failed_at_blocknumber)
                whole_balance_limit = self.whole_balance_limit(
                    block_identifier=failed_at_blocknumber)

                if latest_deposit >= total_deposit:
                    msg = "Deposit amount already increased after another transaction"
                    raise RaidenRecoverableError(msg)

                if allowance < amount_to_deposit:
                    msg = (
                        "The allowance is insufficient. Check concurrent deposits "
                        "for the same token network but different proxies.")
                    raise RaidenRecoverableError(msg)

                # Because we acquired the lock for the token, and the gas estimation succeeded,
                # We know that the account had enough balance for the deposit transaction.
                if token.balance_of(self.node_address,
                                    failed_at_blocknumber) < amount_to_deposit:
                    msg = (
                        f"Transaction failed and balance decreased unexpectedly. "
                        f"This could be a bug in Raiden or a mallicious "
                        f"ERC20 Token.")
                    raise RaidenRecoverableError(msg)

                if whole_balance + amount_to_deposit > UINT256_MAX:
                    msg = (
                        f"Current whole balance is {whole_balance}. "
                        f"The new deposit of {amount_to_deposit} caused an overflow."
                    )
                    raise RaidenRecoverableError(msg)

                if whole_balance + amount_to_deposit > whole_balance_limit:
                    msg = (
                        f"Current whole balance is {whole_balance}. "
                        f"With the new deposit of {amount_to_deposit}, the deposit "
                        f"limit of {whole_balance_limit} was exceeded.")
                    raise RaidenRecoverableError(msg)

                if latest_deposit < total_deposit:
                    msg = "Deposit amount did not increase after deposit transaction"
                    raise RaidenRecoverableError(msg)

                raise RaidenRecoverableError(
                    "Deposit failed of unknown reason")
    def _add_token(
        self,
        token_address: TokenAddress,
        channel_participant_deposit_limit: TokenAmount,
        token_network_deposit_limit: TokenAmount,
        log_details: Dict[Any, Any],
    ) -> Tuple[TransactionHash, TokenNetworkAddress]:
        token_network_address = None

        kwargs = {
            "_token_address": token_address,
            "_channel_participant_deposit_limit":
            channel_participant_deposit_limit,
            "_token_network_deposit_limit": token_network_deposit_limit,
        }
        estimated_transaction = self.rpc_client.estimate_gas(
            self.proxy, "createERC20TokenNetwork", log_details, **kwargs)

        if estimated_transaction is not None:
            estimated_transaction.estimated_gas = safe_gas_limit(
                estimated_transaction.estimated_gas,
                self.gas_measurements[
                    "TokenNetworkRegistry createERC20TokenNetwork"],
            )

            transaction_sent = self.rpc_client.transact(estimated_transaction)
            transaction_mined = self.rpc_client.poll_transaction(
                transaction_sent)
            receipt = transaction_mined.receipt

            if not was_transaction_successfully_mined(transaction_mined):
                failed_at_blocknumber = BlockNumber(receipt["blockNumber"])

                max_token_networks = self.get_max_token_networks(
                    block_identifier=failed_at_blocknumber)
                token_networks_created = self.get_token_network_created(
                    block_identifier=failed_at_blocknumber)
                already_registered = self.get_token_network(
                    token_address=token_address,
                    block_identifier=failed_at_blocknumber)
                deprecation_executor = self.get_deprecation_executor(
                    block_identifier=failed_at_blocknumber)
                settlement_timeout_min = self.settlement_timeout_min(
                    block_identifier=failed_at_blocknumber)
                settlement_timeout_max = self.settlement_timeout_max(
                    block_identifier=failed_at_blocknumber)
                chain_id = self.get_chain_id(
                    block_identifier=failed_at_blocknumber)
                secret_registry_address = self.get_secret_registry_address(
                    block_identifier=failed_at_blocknumber)

                try:
                    # Creating a new instance to run the constructor checks.
                    token_proxy = Token(
                        jsonrpc_client=self.rpc_client,
                        token_address=token_address,
                        contract_manager=self.proxy_manager.contract_manager,
                        block_identifier=failed_at_blocknumber,
                    )
                except AddressWithoutCode:
                    # This cannot be an unrecoverable error, since the ERC20
                    # code is external.
                    raise RaidenRecoverableError(
                        "Token disappeared! The address "
                        f"{to_checksum_address(token_address)} did have code at "
                        f"block {log_details['given_block_identifier']}, however "
                        f"at block {failed_at_blocknumber} when the registration "
                        "transaction was mined the address didn't have code "
                        "anymore.")

                check_transaction_failure(transaction_mined, self.rpc_client)

                check_address_has_code_handle_pruned_block(
                    client=self.rpc_client,
                    address=Address(secret_registry_address),
                    contract_name=CONTRACT_SECRET_REGISTRY,
                    expected_code=decode_hex(
                        self.proxy_manager.contract_manager.
                        get_runtime_hexcode(CONTRACT_SECRET_REGISTRY)),
                    given_block_identifier=failed_at_blocknumber,
                )

                if token_networks_created >= max_token_networks:
                    raise RaidenRecoverableError(
                        "The number of existing token networks reached the maximum allowed"
                    )

                if already_registered:
                    # Race condition lost, the token network was created in a different
                    # transaction which got mined first.
                    raise RaidenRecoverableError(
                        "The token was already registered in the TokenNetworkRegistry."
                    )

                if deprecation_executor == NULL_ADDRESS_BYTES:
                    raise RaidenUnrecoverableError(
                        "The deprecation executor property for the "
                        "TokenNetworkRegistry is invalid.")

                if chain_id == 0:
                    raise RaidenUnrecoverableError(
                        "The chain ID property for the TokenNetworkRegistry is invalid."
                    )

                if chain_id != self.rpc_client.chain_id:
                    raise RaidenUnrecoverableError(
                        f"The provided chain ID {chain_id} does not match the "
                        f"network Raiden is running on: {self.rpc_client.chain_id}."
                    )

                if secret_registry_address == NULL_ADDRESS_BYTES:
                    raise RaidenUnrecoverableError(
                        "The secret registry address for the token network is invalid."
                    )

                if settlement_timeout_min == 0:
                    raise RaidenUnrecoverableError(
                        "The minimum settlement timeout for the token network "
                        "should be larger than zero.")

                if settlement_timeout_max <= settlement_timeout_min:
                    raise RaidenUnrecoverableError(
                        "The maximum settlement timeout for the token network "
                        "should be larger than the minimum settlement timeout."
                    )

                total_supply = token_proxy.total_supply(
                    block_identifier=failed_at_blocknumber)
                if not total_supply or total_supply <= 0:
                    raise RaidenRecoverableError(
                        f"The given token address is not a valid ERC20 token, "
                        f"total_supply() returned an invalid value {total_supply}."
                    )

                # At this point, the TokenNetworkRegistry fails to instantiate
                # a new TokenNetwork.
                raise RaidenUnrecoverableError(
                    "createERC20TokenNetwork failed for an unknown reason, even "
                    "though the gas estimation succeeded.")

            succeeded_at_blockhash = receipt["blockHash"]
            token_network_address = self.get_token_network(
                token_address, succeeded_at_blockhash)
            if token_network_address is None:
                msg = "createERC20TokenNetwork succeeded but token network address is Null"
                raise RaidenUnrecoverableError(msg)
        else:  # `estimated_transaction` is None
            # The latest block can not be used reliably because of reorgs,
            # therefore every call using this block has to handle pruned data.
            failed_at_block = self.rpc_client.get_block(BLOCK_ID_LATEST)
            failed_at_blockhash = failed_at_block["hash"].hex()
            failed_at_blocknumber = failed_at_block["number"]

            max_token_networks = self.get_max_token_networks(
                block_identifier=failed_at_blocknumber)
            token_networks_created = self.get_token_network_created(
                block_identifier=failed_at_blocknumber)

            already_registered = self.get_token_network(
                token_address=token_address,
                block_identifier=failed_at_blocknumber)
            deprecation_executor = self.get_deprecation_executor(
                block_identifier=failed_at_blocknumber)
            settlement_timeout_min = self.settlement_timeout_min(
                block_identifier=failed_at_blocknumber)
            settlement_timeout_max = self.settlement_timeout_max(
                block_identifier=failed_at_blocknumber)
            chain_id = self.get_chain_id(
                block_identifier=failed_at_blocknumber)
            secret_registry_address = self.get_secret_registry_address(
                block_identifier=failed_at_blocknumber)

            try:
                # Creating a new instance to run the constructor checks.
                token_proxy = Token(
                    jsonrpc_client=self.rpc_client,
                    token_address=token_address,
                    contract_manager=self.proxy_manager.contract_manager,
                    block_identifier=failed_at_blocknumber,
                )
            except AddressWithoutCode:
                # This cannot be an unrecoverable error, since the ERC20
                # code is external.
                raise RaidenRecoverableError(
                    "Token disappeared! The address "
                    "{to_checksum_address(token_address)} did have code at "
                    "block {log_details['given_block_identifier']}, however "
                    "at block {failed_at_blocknumber} when the registration "
                    "transaction was mined the address didn't have code "
                    "anymore.")

            check_address_has_code_handle_pruned_block(
                client=self.rpc_client,
                address=Address(secret_registry_address),
                contract_name=CONTRACT_SECRET_REGISTRY,
                expected_code=decode_hex(
                    self.proxy_manager.contract_manager.get_runtime_hexcode(
                        CONTRACT_SECRET_REGISTRY)),
                given_block_identifier=failed_at_blocknumber,
            )

            required_gas = self.gas_measurements[
                "TokenNetworkRegistry createERC20TokenNetwork"]

            self.rpc_client.check_for_insufficient_eth(
                transaction_name="createERC20TokenNetwork",
                transaction_executed=False,
                required_gas=required_gas,
                block_identifier=failed_at_blocknumber,
            )

            if token_networks_created >= max_token_networks:
                raise RaidenRecoverableError(
                    "The number of existing token networks reached the maximum allowed"
                )

            if already_registered:
                # Race condition lost, the token network was created in a different
                # transaction which got mined first.
                raise RaidenRecoverableError(
                    "The token was already registered in the TokenNetworkRegistry."
                )

            if deprecation_executor == NULL_ADDRESS_BYTES:
                raise RaidenUnrecoverableError(
                    "The deprecation executor property for the TokenNetworkRegistry is invalid."
                )

            if chain_id == 0:
                raise RaidenUnrecoverableError(
                    "The chain ID property for the TokenNetworkRegistry is invalid."
                )

            if chain_id != self.rpc_client.chain_id:
                raise RaidenUnrecoverableError(
                    f"The provided chain ID {chain_id} does not match the "
                    f"network Raiden is running on: {self.rpc_client.chain_id}."
                )

            if secret_registry_address == NULL_ADDRESS_BYTES:
                raise RaidenUnrecoverableError(
                    "The secret registry address for the token network is invalid."
                )

            if settlement_timeout_min <= 0:
                raise RaidenUnrecoverableError(
                    "The minimum settlement timeout for the token network "
                    "should be larger than zero.")

            if settlement_timeout_max <= settlement_timeout_min:
                raise RaidenUnrecoverableError(
                    "The maximum settlement timeout for the token network "
                    "should be larger than the minimum settlement timeout.")

            total_supply = token_proxy.total_supply(
                block_identifier=failed_at_blocknumber)
            if not total_supply or total_supply <= 0:
                raise RaidenRecoverableError(
                    f"The given token address is not a valid ERC20 token, "
                    f"total_supply() returned an invalid value {total_supply}."
                )

            # At this point, the TokenNetworkRegistry fails to instantiate
            # a new TokenNetwork.
            raise RaidenUnrecoverableError(
                f"createERC20TokenNetwork gas estimation failed for an unknown "
                f"reason. Reference block {failed_at_blockhash} "
                f"{failed_at_blocknumber}.")
        return (
            TransactionHash(transaction_mined.transaction_hash),
            TokenNetworkAddress(token_network_address),
        )