def test_get_orders(self): self.exchange.address = Address('0x12459C951127e0c374FF9105DdA097662A027093') sai_address = Address('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359') gem_address = Address('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') # when zrx_relayer_api = ZrxRelayerApi(self.exchange, "https://api.ercdex.com/api/standard/1") orders1 = zrx_relayer_api.get_orders(sai_address, gem_address) orders2 = zrx_relayer_api.get_orders(gem_address, sai_address) # then assert len(orders1) >= 0 assert len(orders2) >= 0
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()