def test_should_execute_arbitrage_in_one_transaction_if_tx_manager_configured(
            self, deployment: Deployment):
        # given
        tx_manager = TxManager.deploy(deployment.web3)

        # and
        keeper = ArbitrageKeeper(args=args(
            f"--eth-from {deployment.our_address.address}"
            f" --tub-address {deployment.tub.address}"
            f" --tap-address {deployment.tap.address}"
            f" --oasis-address {deployment.otc.address}"
            f" --base-token {deployment.sai.address}"
            f" --min-profit 13.0 --max-engagement 100.0"
            f" --tx-manager {tx_manager.address}"),
                                 web3=deployment.web3)

        # and
        DSValue(web3=deployment.web3,
                address=deployment.tub.pip()).poke_with_int(
                    Wad.from_number(500).value).transact()
        deployment.tub.mold_gap(Wad.from_number(1.05)).transact()
        deployment.tub.join(Wad.from_number(1000)).transact()
        deployment.tap.mold_gap(Wad.from_number(1.05)).transact()

        # and
        deployment.sai.mint(Wad.from_number(1000)).transact()

        # and
        deployment.otc.approve(
            [deployment.gem, deployment.sai, deployment.skr], directly())
        deployment.otc.add_token_pair_whitelist(
            deployment.sai.address, deployment.skr.address).transact()
        deployment.otc.add_token_pair_whitelist(
            deployment.skr.address, deployment.gem.address).transact()
        deployment.otc.add_token_pair_whitelist(
            deployment.gem.address, deployment.sai.address).transact()
        deployment.otc.make(deployment.skr.address,
                            Wad.from_number(105), deployment.sai.address,
                            Wad.from_number(100)).transact()
        deployment.otc.make(deployment.gem.address,
                            Wad.from_number(110), deployment.skr.address,
                            Wad.from_number(105)).transact()
        deployment.otc.make(deployment.sai.address,
                            Wad.from_number(115), deployment.gem.address,
                            Wad.from_number(110)).transact()
        assert len(deployment.otc.get_orders()) == 3

        # when
        keeper.approve()
        block_number_before = deployment.web3.eth.blockNumber
        keeper.process_block()
        block_number_after = deployment.web3.eth.blockNumber

        # then
        assert len(deployment.otc.get_orders()) == 0

        # and
        # [keeper used only one transaction, as TxManager is configured]
        assert (block_number_after - block_number_before) == 1
Exemple #2
0
def test_via_tx_manager_approval_should_raise_exception_if_approval_fails():
    # given
    global web3, our_address, second_address, token
    tx = TxManager.deploy(web3)
    tx.execute = MagicMock(return_value=FailingTransact())

    # when
    with pytest.raises(Exception):
        via_tx_manager(tx)(token, second_address, "some-name")
Exemple #3
0
def test_via_tx_manager_approval():
    # given
    global web3, our_address, second_address, token
    tx = TxManager.deploy(web3)

    # when
    via_tx_manager(tx)(token, second_address, "some-name")

    # then
    assert token.allowance_of(tx.address, second_address) == Wad(2**256 - 1)
Exemple #4
0
 def setup_method(self):
     self.web3 = Web3(HTTPProvider("http://localhost:8555"))
     self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
     self.our_address = Address(self.web3.eth.defaultAccount)
     self.other_address = Address(self.web3.eth.accounts[1])
     self.tx = TxManager.deploy(self.web3)
     self.token1 = DSToken.deploy(self.web3, 'ABC')
     self.token1.mint(Wad.from_number(1000000)).transact()
     self.token2 = DSToken.deploy(self.web3, 'DEF')
     self.token2.mint(Wad.from_number(1000000)).transact()
Exemple #5
0
def test_via_tx_manager_approval_should_not_approve_if_already_approved():
    # given
    global web3, our_address, second_address, token
    tx = TxManager.deploy(web3)
    tx.execute([],
               [token.approve(second_address, Wad(2**248 + 19)).invocation()
                ]).transact()

    # when
    via_tx_manager(tx)(token, second_address, "some-name")

    # then
    assert token.allowance_of(tx.address, second_address) == Wad(2**248 + 19)
Exemple #6
0
def test_via_tx_manager_approval_should_obey_gas_price():
    # given
    global web3, our_address, second_address, token
    tx = TxManager.deploy(web3)

    # when
    via_tx_manager(tx, gas_strategy=FixedGasPrice(15000000000))(token,
                                                                second_address,
                                                                "some-name")

    # then
    assert web3.eth.getBlock(
        'latest',
        full_transactions=True).transactions[0].gasPrice == 15000000000
    def __init__(self, args, **kwargs):
        """Pass in arguements assign necessary variables/objects and instantiate other Classes"""

        parser = argparse.ArgumentParser("simple-arbitrage-keeper")

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help=
            "Ethereum address from which to send transactions; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            required=True,
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=/path/to/keystore.json,pass_file=/path/to/passphrase.txt')"
        )

        parser.add_argument(
            "--uniswap-entry-exchange",
            type=str,
            required=True,
            help=
            "Ethereum address of the Uniswap Exchange contract for the entry token market; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--uniswap-arb-exchange",
            type=str,
            required=True,
            help=
            "Ethereum address of the Uniswap Exchange contract for the arb token market; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--oasis-address",
            type=str,
            required=True,
            help=
            "Ethereum address of the OasisDEX contract; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--oasis-api-endpoint",
            type=str,
            required=True,
            help=
            "Endpoint of of the Oasis V2 REST API (e.g. 'https://kovan-api.oasisdex.com' )"
        )

        parser.add_argument(
            "--relayer-per-page",
            type=int,
            default=100,
            help=
            "Number of orders to fetch per one page from the 0x Relayer API (default: 100)"
        )

        parser.add_argument(
            "--tx-manager",
            type=str,
            required=True,
            help=
            "Ethereum address of the TxManager contract to use for multi-step arbitrage; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--gas-price",
            type=int,
            default=0,
            help=
            "Gas price in Wei (default: node default), (e.g. 1000000000 for 1 GWei)"
        )

        parser.add_argument(
            "--entry-token",
            type=str,
            required=True,
            help=
            "The token address that the bot starts and ends with in every transaction; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--arb-token",
            type=str,
            required=True,
            help=
            "The token address that arbitraged between both exchanges; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--arb-token-name",
            type=str,
            required=True,
            help=
            "The token name that arbitraged between both exchanges (e.g. 'SAI', 'WETH', 'REP')"
        )

        parser.add_argument(
            "--min-profit",
            type=int,
            required=True,
            help=
            "Ether amount of minimum profit (in base token) from one arbitrage operation (e.g. 1 for 1 Sai min profit)"
        )

        parser.add_argument(
            "--max-engagement",
            type=int,
            required=True,
            help=
            "Ether amount of maximum engagement (in base token) in one arbitrage operation (e.g. 100 for 100 Sai max engagement)"
        )

        parser.add_argument(
            "--max-errors",
            type=int,
            default=100,
            help=
            "Maximum number of allowed errors before the keeper terminates (default: 100)"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"https://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        self.sai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359'))  # Mainnet Sai
        self.dai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0x6b175474e89094c44da98b954eedeac495271d0f'))  # Mainnet Dai

        self.ksai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0xC4375B7De8af5a38a93548eb8453a498222C4fF2'))  #Kovan Sai
        self.kdai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa'))  #Kovan Dai

        self.entry_token = ERC20Token(web3=self.web3,
                                      address=Address(
                                          self.arguments.entry_token))
        self.arb_token = ERC20Token(web3=self.web3,
                                    address=Address(self.arguments.arb_token))
        self.arb_token.name = self.arguments.arb_token_name \
            if self.arguments.arb_token_name != 'WETH' else 'ETH'


        self.uniswap_entry_exchange = UniswapWrapper(self.web3, self.entry_token.address, Address(self.arguments.uniswap_entry_exchange)) \
            if self.arguments.uniswap_entry_exchange is not None else None

        self.uniswap_arb_exchange = UniswapWrapper(self.web3, self.arb_token.address, Address(self.arguments.uniswap_arb_exchange)) \
            if self.arguments.uniswap_arb_exchange is not None else None

        self.oasis_api_endpoint = OasisAPI(api_server=self.arguments.oasis_api_endpoint,
                                           entry_token_name=self.token_name(self.entry_token.address),
                                           arb_token_name=self.arb_token.name) \
            if self.arguments.oasis_api_endpoint is not None else None

        self.oasis = MatchingMarket(web3=self.web3,
                                    address=Address(
                                        self.arguments.oasis_address))

        self.min_profit = Wad(int(self.arguments.min_profit * 10**18))
        self.max_engagement = Wad(int(self.arguments.max_engagement * 10**18))
        self.max_errors = self.arguments.max_errors
        self.errors = 0

        if self.arguments.tx_manager:
            self.tx_manager = TxManager(web3=self.web3,
                                        address=Address(
                                            self.arguments.tx_manager))
            if self.tx_manager.owner() != self.our_address:
                raise Exception(
                    f"The TxManager has to be owned by the address the keeper is operating from."
                )
        else:
            self.tx_manager = None

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
class SimpleArbitrageKeeper:
    """Keeper to arbitrage on OasisDEX and Uniswap"""

    logger = logging.getLogger('simple-arbitrage-keeper')

    def __init__(self, args, **kwargs):
        """Pass in arguements assign necessary variables/objects and instantiate other Classes"""

        parser = argparse.ArgumentParser("simple-arbitrage-keeper")

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help=
            "Ethereum address from which to send transactions; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            required=True,
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=/path/to/keystore.json,pass_file=/path/to/passphrase.txt')"
        )

        parser.add_argument(
            "--uniswap-entry-exchange",
            type=str,
            required=True,
            help=
            "Ethereum address of the Uniswap Exchange contract for the entry token market; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--uniswap-arb-exchange",
            type=str,
            required=True,
            help=
            "Ethereum address of the Uniswap Exchange contract for the arb token market; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--oasis-address",
            type=str,
            required=True,
            help=
            "Ethereum address of the OasisDEX contract; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--oasis-api-endpoint",
            type=str,
            required=True,
            help=
            "Endpoint of of the Oasis V2 REST API (e.g. 'https://kovan-api.oasisdex.com' )"
        )

        parser.add_argument(
            "--relayer-per-page",
            type=int,
            default=100,
            help=
            "Number of orders to fetch per one page from the 0x Relayer API (default: 100)"
        )

        parser.add_argument(
            "--tx-manager",
            type=str,
            required=True,
            help=
            "Ethereum address of the TxManager contract to use for multi-step arbitrage; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--gas-price",
            type=int,
            default=0,
            help=
            "Gas price in Wei (default: node default), (e.g. 1000000000 for 1 GWei)"
        )

        parser.add_argument(
            "--entry-token",
            type=str,
            required=True,
            help=
            "The token address that the bot starts and ends with in every transaction; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--arb-token",
            type=str,
            required=True,
            help=
            "The token address that arbitraged between both exchanges; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--arb-token-name",
            type=str,
            required=True,
            help=
            "The token name that arbitraged between both exchanges (e.g. 'SAI', 'WETH', 'REP')"
        )

        parser.add_argument(
            "--min-profit",
            type=int,
            required=True,
            help=
            "Ether amount of minimum profit (in base token) from one arbitrage operation (e.g. 1 for 1 Sai min profit)"
        )

        parser.add_argument(
            "--max-engagement",
            type=int,
            required=True,
            help=
            "Ether amount of maximum engagement (in base token) in one arbitrage operation (e.g. 100 for 100 Sai max engagement)"
        )

        parser.add_argument(
            "--max-errors",
            type=int,
            default=100,
            help=
            "Maximum number of allowed errors before the keeper terminates (default: 100)"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"https://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        self.sai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359'))  # Mainnet Sai
        self.dai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0x6b175474e89094c44da98b954eedeac495271d0f'))  # Mainnet Dai

        self.ksai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0xC4375B7De8af5a38a93548eb8453a498222C4fF2'))  #Kovan Sai
        self.kdai = ERC20Token(
            web3=self.web3,
            address=Address(
                '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa'))  #Kovan Dai

        self.entry_token = ERC20Token(web3=self.web3,
                                      address=Address(
                                          self.arguments.entry_token))
        self.arb_token = ERC20Token(web3=self.web3,
                                    address=Address(self.arguments.arb_token))
        self.arb_token.name = self.arguments.arb_token_name \
            if self.arguments.arb_token_name != 'WETH' else 'ETH'


        self.uniswap_entry_exchange = UniswapWrapper(self.web3, self.entry_token.address, Address(self.arguments.uniswap_entry_exchange)) \
            if self.arguments.uniswap_entry_exchange is not None else None

        self.uniswap_arb_exchange = UniswapWrapper(self.web3, self.arb_token.address, Address(self.arguments.uniswap_arb_exchange)) \
            if self.arguments.uniswap_arb_exchange is not None else None

        self.oasis_api_endpoint = OasisAPI(api_server=self.arguments.oasis_api_endpoint,
                                           entry_token_name=self.token_name(self.entry_token.address),
                                           arb_token_name=self.arb_token.name) \
            if self.arguments.oasis_api_endpoint is not None else None

        self.oasis = MatchingMarket(web3=self.web3,
                                    address=Address(
                                        self.arguments.oasis_address))

        self.min_profit = Wad(int(self.arguments.min_profit * 10**18))
        self.max_engagement = Wad(int(self.arguments.max_engagement * 10**18))
        self.max_errors = self.arguments.max_errors
        self.errors = 0

        if self.arguments.tx_manager:
            self.tx_manager = TxManager(web3=self.web3,
                                        address=Address(
                                            self.arguments.tx_manager))
            if self.tx_manager.owner() != self.our_address:
                raise Exception(
                    f"The TxManager has to be owned by the address the keeper is operating from."
                )
        else:
            self.tx_manager = None

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))

    def main(self):
        """ Initialize the lifecycle and enter into the Keeper Lifecycle controller

        Each function supplied by the lifecycle will accept a callback function that will be executed.
        The lifecycle.on_block() function will enter into an infinite loop, but will gracefully shutdown
        if it recieves a SIGINT/SIGTERM signal.

        """

        with Lifecycle(self.web3) as lifecycle:
            self.lifecycle = lifecycle
            lifecycle.on_startup(self.startup)
            lifecycle.on_block(self.process_block)

    def startup(self):
        self.approve()

    def approve(self):
        """ Approve all components that need to access our balances

        Approve Oasis to access our tokens from our TxManager
        Approve Uniswap exchanges to access our tokens that they swap from our TxManager
        Approve TxManager to access our tokens from our ETH_FROM address

        """
        approval_method = via_tx_manager(self.tx_manager, gas_price=self.gas_price()) if self.tx_manager \
            else directly(gas_price=self.gas_price())

        self.oasis.approve([self.entry_token, self.arb_token], approval_method)

        if self.uniswap_entry_exchange:
            self.uniswap_entry_exchange.approve([self.entry_token],
                                                approval_method)

        if self.uniswap_arb_exchange:
            self.uniswap_arb_exchange.approve([self.arb_token],
                                              approval_method)

        if self.tx_manager:
            self.tx_manager.approve([self.entry_token, self.arb_token],
                                    directly(gas_price=self.gas_price()))

    def token_name(self, address: Address) -> str:
        if address == self.ksai.address or address == self.sai.address:
            return "SAI"
        elif address == self.kdai.address or address == self.dai.address:
            return "DAI"
        else:
            return str(address)

    def oasis_order_size(self, size: Wad = None):
        """ Calculate the an oasis order buy size when buying/selling the arb_token

        Query's the Oasis REST API and, if a `size` is not supplied, calculates a total amount of arb_token to be purchased.
        However, if size is supplied, it will calculate the total amount of entry_token that is purchased

        Args:
            size: The size of the arb_token that will be sold

        Returns:
            A :py:class:`pymaker.numeric.Wad` instance of either a final entry_token amount to be bought
            or a final arb_token amount to be bought
        """

        if self.oasis_api_endpoint is None:
            return None

        (bids, asks) = self.oasis_api_endpoint.get_orders()

        entry_token_amount = 0
        arb_token_amount = 0

        if size is None:
            for order in asks:
                entry_token_amount = entry_token_amount + order[0] * order[1]
                arb_token_amount = arb_token_amount + order[1]

                if entry_token_amount >= float(self.entry_amount):
                    #some linear interpolation
                    final_arb_token_amount = arb_token_amount - order[1] + \
                        (float(self.entry_amount) - (entry_token_amount - order[0] * order[1])) * (1/order[0])
                    return Wad(int(final_arb_token_amount * 10**18))

        else:
            for order in bids:
                entry_token_amount = entry_token_amount + order[0] * order[1]
                arb_token_amount = arb_token_amount + order[1]

                if arb_token_amount >= float(size):
                    #some linear interpolation
                    final_entry_token_amount = entry_token_amount - (order[0] * order[1]) + \
                        (float(size) - (arb_token_amount - order[1])) * (order[0])
                    return Wad(int(final_entry_token_amount * 10**18))

    def uniswap_order_size(self, size: Wad = None):
        """ Calculate the an Uniswap buy size when buying/selling the arb_token

        Reads the Uniswap entry, arb and eth exchange contracts, and if a `size` is not supplied,
        calculates a total amount of arb_token to be purchased. However, if size is supplied,
        it will calculate the total amount of entry_token that is purchased

        Args:
            size: The size of the arb_token that will be sold

        Returns:
            A :py:class:`pymaker.numeric.Wad` instance of either a final entry_token amount to be bought
            or a final arb_token amount to be bought
        """
        if size is None:
            ethAmt = self.uniswap_entry_exchange.uniswap_base.get_token_eth_input_price(
                self.entry_amount)
            arbAmt = self.uniswap_arb_exchange.uniswap_base.get_eth_token_input_price(
                ethAmt)
            return Wad(arbAmt)
        else:
            ethAmt = self.uniswap_arb_exchange.uniswap_base.get_token_eth_input_price(
                size)
            entryAmt = self.uniswap_entry_exchange.uniswap_base.get_eth_token_input_price(
                ethAmt)
            return Wad(entryAmt)

    def process_block(self):
        """Callback called on each new block. If too many errors, terminate the keeper to minimize potential damage."""
        if self.errors >= self.max_errors:
            self.lifecycle.terminate()
        else:
            self.find_best_opportunity_available()

    def find_best_opportunity_available(self):
        """Find the best arbitrage opportunity present and execute it.

        With an entry_token of entry_amount, calculate the profitability of buying an arb_token
        on Oasis and selling it on Uniswap. Calculate the profitability of the same operation
        but starting on Uniswap. Depending on the comparison between these profitabilities,
        assign the variables pertaining to start_exchange and end_exchange.

        If the highestProfit is beyond the minimum profit as set by the user, print the opportunity
        and attempt to execute it in one transaction
        """

        self.entry_amount = Wad.min(
            self.entry_token.balance_of(self.our_address), self.max_engagement)

        oasis_arb_amount = self.oasis_order_size()
        profit_oasis_to_uniswap = self.uniswap_order_size(
            oasis_arb_amount) - self.entry_amount

        uniswap_arb_amount = self.uniswap_order_size()
        profit_uniswap_to_oasis = self.oasis_order_size(
            uniswap_arb_amount) - self.entry_amount

        if profit_oasis_to_uniswap > profit_uniswap_to_oasis:
            self.start_exchange, self.start_exchange.name = self.oasis, 'Oasis'
            self.arb_amount = oasis_arb_amount * Wad.from_number(0.999999)
            self.end_exchange, self.end_exchange.name = self.uniswap_arb_exchange, 'Uniswap'

        else:
            self.start_exchange, self.start_exchange.name = self.uniswap_entry_exchange, 'Uniswap'
            self.arb_amount = uniswap_arb_amount * Wad.from_number(0.999999)
            self.end_exchange, self.end_exchange.name = self.oasis, 'Oasis'

        highestProfit = max(profit_oasis_to_uniswap, profit_uniswap_to_oasis)
        self.exit_amount = (highestProfit +
                            self.entry_amount) * Wad.from_number(0.999999)

        #Print the highest profit/(loss) to see how close we come to breaking even
        self.logger.info(
            f"Best trade regardless of profit/min-profit: {highestProfit} {self.token_name(self.entry_token.address)} "
            f"from {self.start_exchange.name} to {self.end_exchange.name}")

        opportunity = highestProfit if highestProfit > self.min_profit else None

        if opportunity:
            self.print_opportunity(opportunity)
            self.execute_opportunity_in_one_transaction()

    def print_opportunity(self, opportunity: Wad):
        """Print the details of the opportunity."""
        self.logger.info(
            f"Profit opportunity of {opportunity} {self.token_name(self.entry_token.address)} "
            f"from {self.start_exchange.name} to {self.end_exchange.name}")

    def execute_opportunity_in_one_transaction(self):
        """Execute the opportunity in one transaction, using the `tx_manager`.

        Sell entry_token and buy arb_token on start_exchange
        Sell arb_token and buy entry_token on end_exchange

        """

        tokens = [self.entry_token.address, self.arb_token.address]

        invocations = [
            self.start_exchange.make(pay_token=self.entry_token.address,
                                     pay_amount=self.entry_amount,
                                     buy_token=self.arb_token.address,
                                     buy_amount=self.arb_amount).invocation(),
            self.end_exchange.make(pay_token=self.arb_token.address,
                                   pay_amount=self.arb_amount,
                                   buy_token=self.entry_token.address,
                                   buy_amount=self.exit_amount).invocation()
        ]

        receipt = self.tx_manager.execute(tokens, invocations).transact(
            gas_price=self.gas_price(), gas_buffer=300000)

        if receipt:
            self.logger.info(
                f"The profit we made is {TransferFormatter().format_net(receipt.transfers, self.our_address, self.token_name)}"
            )
        else:
            self.errors += 1

    def gas_price(self):
        """ FixedGasPrice if gas_price argument present, otherwise node DefaultGasPrice """
        if self.arguments.gas_price > 0:
            return FixedGasPrice(self.arguments.gas_price)
        else:
            return DefaultGasPrice()
    def __init__(self, args, **kwargs):
        parser = argparse.ArgumentParser("arbitrage-keeper")

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help="Ethereum account from which to send transactions")

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')"
        )

        parser.add_argument("--tub-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Tub contract")

        parser.add_argument("--tap-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Tap contract")

        parser.add_argument(
            "--exchange-address",
            type=str,
            help="Ethereum address of the 0x Exchange contract")

        parser.add_argument("--oasis-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the OasisDEX contract")

        parser.add_argument(
            "--oasis-support-address",
            type=str,
            required=False,
            help="Ethereum address of the OasisDEX support contract")

        parser.add_argument("--relayer-api-server",
                            type=str,
                            help="Address of the 0x Relayer API")

        parser.add_argument(
            "--relayer-per-page",
            type=int,
            default=100,
            help=
            "Number of orders to fetch per one page from the 0x Relayer API (default: 100)"
        )

        parser.add_argument(
            "--tx-manager",
            type=str,
            help=
            "Ethereum address of the TxManager contract to use for multi-step arbitrage"
        )

        parser.add_argument("--gas-price",
                            type=int,
                            default=0,
                            help="Gas price in Wei (default: node default)")

        parser.add_argument(
            "--base-token",
            type=str,
            required=True,
            help="The token all arbitrage sequences will start and end with")

        parser.add_argument(
            "--min-profit",
            type=float,
            required=True,
            help="Minimum profit (in base token) from one arbitrage operation")

        parser.add_argument(
            "--max-engagement",
            type=float,
            required=True,
            help="Maximum engagement (in base token) in one arbitrage operation"
        )

        parser.add_argument(
            "--max-errors",
            type=int,
            default=100,
            help=
            "Maximum number of allowed errors before the keeper terminates (default: 100)"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        self.tub = Tub(web3=self.web3,
                       address=Address(self.arguments.tub_address))
        self.tap = Tap(web3=self.web3,
                       address=Address(self.arguments.tap_address))
        self.gem = ERC20Token(web3=self.web3, address=self.tub.gem())
        self.sai = ERC20Token(web3=self.web3, address=self.tub.sai())
        self.skr = ERC20Token(web3=self.web3, address=self.tub.skr())

        self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) \
            if self.arguments.exchange_address is not None else None
        self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) \
            if self.arguments.relayer_api_server is not None else None

        self.otc = MatchingMarket(
            web3=self.web3,
            address=Address(self.arguments.oasis_address),
            support_address=Address(self.arguments.oasis_support_address)
            if self.arguments.oasis_support_address is not None else None)

        self.base_token = ERC20Token(web3=self.web3,
                                     address=Address(
                                         self.arguments.base_token))
        self.min_profit = Wad.from_number(self.arguments.min_profit)
        self.max_engagement = Wad.from_number(self.arguments.max_engagement)
        self.max_errors = self.arguments.max_errors
        self.errors = 0

        if self.arguments.tx_manager:
            self.tx_manager = TxManager(web3=self.web3,
                                        address=Address(
                                            self.arguments.tx_manager))
            if self.tx_manager.owner() != self.our_address:
                raise Exception(
                    f"The TxManager has to be owned by the address the keeper is operating from."
                )
        else:
            self.tx_manager = None

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
class ArbitrageKeeper:
    """Keeper to arbitrage on OasisDEX, `join`, `exit`, `boom` and `bust`."""

    logger = logging.getLogger('arbitrage-keeper')

    def __init__(self, args, **kwargs):
        parser = argparse.ArgumentParser("arbitrage-keeper")

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help="Ethereum account from which to send transactions")

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')"
        )

        parser.add_argument("--tub-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Tub contract")

        parser.add_argument("--tap-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the Tap contract")

        parser.add_argument(
            "--exchange-address",
            type=str,
            help="Ethereum address of the 0x Exchange contract")

        parser.add_argument("--oasis-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the OasisDEX contract")

        parser.add_argument(
            "--oasis-support-address",
            type=str,
            required=False,
            help="Ethereum address of the OasisDEX support contract")

        parser.add_argument("--relayer-api-server",
                            type=str,
                            help="Address of the 0x Relayer API")

        parser.add_argument(
            "--relayer-per-page",
            type=int,
            default=100,
            help=
            "Number of orders to fetch per one page from the 0x Relayer API (default: 100)"
        )

        parser.add_argument(
            "--tx-manager",
            type=str,
            help=
            "Ethereum address of the TxManager contract to use for multi-step arbitrage"
        )

        parser.add_argument("--gas-price",
                            type=int,
                            default=0,
                            help="Gas price in Wei (default: node default)")

        parser.add_argument(
            "--base-token",
            type=str,
            required=True,
            help="The token all arbitrage sequences will start and end with")

        parser.add_argument(
            "--min-profit",
            type=float,
            required=True,
            help="Minimum profit (in base token) from one arbitrage operation")

        parser.add_argument(
            "--max-engagement",
            type=float,
            required=True,
            help="Maximum engagement (in base token) in one arbitrage operation"
        )

        parser.add_argument(
            "--max-errors",
            type=int,
            default=100,
            help=
            "Maximum number of allowed errors before the keeper terminates (default: 100)"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        self.tub = Tub(web3=self.web3,
                       address=Address(self.arguments.tub_address))
        self.tap = Tap(web3=self.web3,
                       address=Address(self.arguments.tap_address))
        self.gem = ERC20Token(web3=self.web3, address=self.tub.gem())
        self.sai = ERC20Token(web3=self.web3, address=self.tub.sai())
        self.skr = ERC20Token(web3=self.web3, address=self.tub.skr())

        self.zrx_exchange = ZrxExchange(web3=self.web3, address=Address(self.arguments.exchange_address)) \
            if self.arguments.exchange_address is not None else None
        self.zrx_relayer_api = ZrxRelayerApi(exchange=self.zrx_exchange, api_server=self.arguments.relayer_api_server) \
            if self.arguments.relayer_api_server is not None else None

        self.otc = MatchingMarket(
            web3=self.web3,
            address=Address(self.arguments.oasis_address),
            support_address=Address(self.arguments.oasis_support_address)
            if self.arguments.oasis_support_address is not None else None)

        self.base_token = ERC20Token(web3=self.web3,
                                     address=Address(
                                         self.arguments.base_token))
        self.min_profit = Wad.from_number(self.arguments.min_profit)
        self.max_engagement = Wad.from_number(self.arguments.max_engagement)
        self.max_errors = self.arguments.max_errors
        self.errors = 0

        if self.arguments.tx_manager:
            self.tx_manager = TxManager(web3=self.web3,
                                        address=Address(
                                            self.arguments.tx_manager))
            if self.tx_manager.owner() != self.our_address:
                raise Exception(
                    f"The TxManager has to be owned by the address the keeper is operating from."
                )
        else:
            self.tx_manager = None

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))

    def main(self):
        with Lifecycle(self.web3) as lifecycle:
            self.lifecycle = lifecycle
            lifecycle.on_startup(self.startup)
            lifecycle.on_block(self.process_block)

    def startup(self):
        self.approve()

    def approve(self):
        """Approve all components that need to access our balances"""
        approval_method = via_tx_manager(self.tx_manager, gas_price=self.gas_price()) if self.tx_manager \
            else directly(gas_price=self.gas_price())
        self.tub.approve(approval_method)
        self.tap.approve(approval_method)
        self.otc.approve([self.gem, self.sai, self.skr], approval_method)
        if self.zrx_exchange:
            self.zrx_exchange.approve([self.gem, self.sai], approval_method)
        if self.tx_manager:
            self.tx_manager.approve([self.gem, self.sai, self.skr],
                                    directly(gas_price=self.gas_price()))

    def token_name(self, address: Address) -> str:
        if address == self.sai.address:
            return "DAI"

        elif address == self.gem.address:
            return "WETH"

        elif address == self.skr.address:
            return "PETH"

        else:
            return str(address)

    def tub_conversions(self) -> List[Conversion]:
        return [
            TubJoinConversion(self.tub),
            TubExitConversion(self.tub),
            TubBoomConversion(self.tub, self.tap),
            TubBustConversion(self.tub, self.tap)
        ]

    def otc_orders(self, tokens):
        orders = []

        for token1 in tokens:
            for token2 in tokens:
                if token1 != token2:
                    orders = orders + self.otc.get_orders(token1, token2)

        return orders

    def otc_conversions(self, tokens) -> List[Conversion]:
        return list(
            map(lambda order: OasisTakeConversion(self.otc, order),
                self.otc_orders(tokens)))

    def zrx_orders(self, tokens):
        if self.zrx_exchange is None or self.zrx_relayer_api is None:
            return []

        orders = []

        for token1 in tokens:
            for token2 in tokens:
                if token1 != token2:
                    orders = orders + self.zrx_relayer_api.get_orders(
                        token1, token2)

        return list(
            filter(lambda order: order.expiration <= time.time(), orders))

    def zrx_conversions(self, tokens) -> List[Conversion]:
        return list(
            map(lambda order: ZrxFillOrderConversion(self.zrx_exchange, order),
                self.zrx_orders(tokens)))

    def all_conversions(self):
        return self.tub_conversions() + \
               self.otc_conversions([self.sai.address, self.skr.address, self.gem.address]) + \
               self.zrx_conversions([self.sai.address, self.gem.address])

    def process_block(self):
        """Callback called on each new block.
        If too many errors, terminate the keeper to minimize potential damage."""
        if self.errors >= self.max_errors:
            self.lifecycle.terminate()
        else:
            self.execute_best_opportunity_available()

    def execute_best_opportunity_available(self):
        """Find the best arbitrage opportunity present and execute it."""
        opportunity = self.best_opportunity(self.profitable_opportunities())
        if opportunity:
            self.print_opportunity(opportunity)
            self.execute_opportunity(opportunity)

    def profitable_opportunities(self):
        """Identify all profitable arbitrage opportunities within given limits."""
        entry_amount = Wad.min(self.base_token.balance_of(self.our_address),
                               self.max_engagement)
        opportunity_finder = OpportunityFinder(
            conversions=self.all_conversions())
        opportunities = opportunity_finder.find_opportunities(
            self.base_token.address, entry_amount)
        opportunities = filter(
            lambda op: op.total_rate() > Ray.from_number(1.000001),
            opportunities)
        opportunities = filter(
            lambda op: op.profit(self.base_token.address) > self.min_profit,
            opportunities)
        opportunities = sorted(
            opportunities,
            key=lambda op: op.profit(self.base_token.address),
            reverse=True)
        return opportunities

    def best_opportunity(self, opportunities: List[Sequence]):
        """Pick the best opportunity, or return None if no profitable opportunities."""
        return opportunities[0] if len(opportunities) > 0 else None

    def print_opportunity(self, opportunity: Sequence):
        """Print the details of the opportunity."""
        self.logger.info(
            f"Opportunity with id={opportunity.id()},"
            f" profit={opportunity.profit(self.base_token.address)} {self.token_name(self.base_token.address)}"
        )

        for index, conversion in enumerate(opportunity.steps, start=1):
            self.logger.info(
                f"Step {index}/{len(opportunity.steps)}: {conversion.name()}"
                f" (from {conversion.source_amount} {self.token_name(conversion.source_token)}"
                f" to {conversion.target_amount} {self.token_name(conversion.target_token)})"
            )

    def execute_opportunity(self, opportunity: Sequence):
        """Execute the opportunity either in one Ethereum transaction or step-by-step.
        Depending on whether `tx_manager` is available."""
        if self.tx_manager:
            self.execute_opportunity_in_one_transaction(opportunity)
        else:
            self.execute_opportunity_step_by_step(opportunity)

    def execute_opportunity_step_by_step(self, opportunity: Sequence):
        """Execute the opportunity step-by-step."""
        def incoming_transfer(our_address: Address):
            return lambda transfer: transfer.to_address == our_address

        def outgoing_transfer(our_address: Address):
            return lambda transfer: transfer.from_address == our_address

        all_transfers = []
        for step in opportunity.steps:
            receipt = step.transact().transact(gas_price=self.gas_price())
            if receipt:
                all_transfers += receipt.transfers
                outgoing = TransferFormatter().format(
                    filter(outgoing_transfer(self.our_address),
                           receipt.transfers), self.token_name)
                incoming = TransferFormatter().format(
                    filter(incoming_transfer(self.our_address),
                           receipt.transfers), self.token_name)
                self.logger.info(f"Exchanged {outgoing} to {incoming}")
            else:
                self.errors += 1
                return
        self.logger.info(
            f"The profit we made is {TransferFormatter().format_net(all_transfers, self.our_address, self.token_name)}"
        )

    def execute_opportunity_in_one_transaction(self, opportunity: Sequence):
        """Execute the opportunity in one transaction, using the `tx_manager`."""
        tokens = [self.sai.address, self.skr.address, self.gem.address]
        invocations = list(
            map(lambda step: step.transact().invocation(), opportunity.steps))
        receipt = self.tx_manager.execute(
            tokens, invocations).transact(gas_price=self.gas_price())
        if receipt:
            self.logger.info(
                f"The profit we made is {TransferFormatter().format_net(receipt.transfers, self.our_address, self.token_name)}"
            )
        else:
            self.errors += 1

    def gas_price(self):
        if self.arguments.gas_price > 0:
            return FixedGasPrice(self.arguments.gas_price)
        else:
            return DefaultGasPrice()
Exemple #11
0
 def test_fail_when_no_contract_under_that_address(self):
     # expect
     with pytest.raises(Exception):
         TxManager(
             web3=self.web3,
             address=Address('0xdeadadd1e5500000000000000000000000000000'))
    def __init__(self, args, **kwargs):
        parser = argparse.ArgumentParser("arbitrage-keeper")

        parser.add_argument("--rpc-host", type=str, default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument("--rpc-port", type=int, default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout", type=int, default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument("--eth-from", type=str, required=True,
                            help="Ethereum account from which to send transactions")

        parser.add_argument("--tub-address", type=str, required=True,
                            help="Ethereum address of the Tub contract")

        parser.add_argument("--tap-address", type=str, required=True,
                            help="Ethereum address of the Tap contract")

        parser.add_argument("--oasis-address", type=str, required=True,
                            help="Ethereum address of the OasisDEX contract")

        parser.add_argument("--tx-manager", type=str,
                            help="Ethereum address of the TxManager contract to use for multi-step arbitrage")

        parser.add_argument("--gas-price", type=int, default=0,
                            help="Gas price in Wei (default: node default)")

        parser.add_argument("--base-token", type=str, required=True,
                            help="The token all arbitrage sequences will start and end with")

        parser.add_argument("--min-profit", type=float, required=True,
                            help="Minimum profit (in base token) from one arbitrage operation")

        parser.add_argument("--max-engagement", type=float, required=True,
                            help="Maximum engagement (in base token) in one arbitrage operation")

        parser.add_argument("--max-errors", type=int, default=100,
                            help="Maximum number of allowed errors before the keeper terminates (default: 100)")

        parser.add_argument("--debug", dest='debug', action='store_true',
                            help="Enable debug output")

        parser.add_argument("--ipcpath", type=str, required=True,
                            help="Local IPC Path")

        parser.add_argument("--eth-from-password", type=str, required=True,
                            help="Eth account password")

        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(IPCProvider(ipc_path=self.arguments.ipcpath))

        self.web3.eth.defaultAccount = self.arguments.eth_from

        if !self.web3.personal.unlockAccount(self.web3.eth.defaultAccount, self.arguments.eth_from_password):
            raise Exception(f"Incorrect account password")

        self.our_address = Address(self.arguments.eth_from)
        self.otc = MatchingMarket(web3=self.web3, address=Address(self.arguments.oasis_address))
        self.tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address))
        self.tap = Tap(web3=self.web3, address=Address(self.arguments.tap_address))
        self.gem = ERC20Token(web3=self.web3, address=self.tub.gem())
        self.sai = ERC20Token(web3=self.web3, address=self.tub.sai())
        self.skr = ERC20Token(web3=self.web3, address=self.tub.skr())

        self.base_token = ERC20Token(web3=self.web3, address=Address(self.arguments.base_token))
        self.min_profit = Wad.from_number(self.arguments.min_profit)
        self.max_engagement = Wad.from_number(self.arguments.max_engagement)
        self.max_errors = self.arguments.max_errors
        self.errors = 0

        if self.arguments.tx_manager:
            self.tx_manager = TxManager(web3=self.web3, address=Address(self.arguments.tx_manager))
            if self.tx_manager.owner() != self.our_address:
                raise Exception(f"The TxManager has to be owned by the address the keeper is operating from.")
        else:
            self.tx_manager = None

        logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s',
                            level=(logging.DEBUG if self.arguments.debug else logging.INFO))