Exemple #1
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='auction-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('--type',
                            type=str,
                            choices=['flip', 'flap', 'flop'],
                            help="Auction type in which to participate")
        parser.add_argument(
            '--ilk',
            type=str,
            help=
            "Name of the collateral type for a flip keeper (e.g. 'ETH-B', 'ZRX-A'); "
            "available collateral types can be found at the left side of the CDP Portal"
        )

        parser.add_argument(
            '--bid-only',
            dest='create_auctions',
            action='store_false',
            help="Do not take opportunities to create new auctions")
        parser.add_argument(
            '--max-auctions',
            type=int,
            default=100,
            help="Maximum number of auctions to simultaneously interact with, "
            "used to manage OS and hardware limitations")
        parser.add_argument(
            '--min-flip-lot',
            type=float,
            default=0,
            help="Minimum lot size to create or bid upon a flip auction")

        parser.add_argument(
            "--vulcanize-endpoint",
            type=str,
            help=
            "When specified, frob history will be queried from a VulcanizeDB lite node, "
            "reducing load on the Ethereum node for flip auctions")
        parser.add_argument(
            '--from-block',
            type=int,
            help=
            "Starting block from which to look at history (set to block where MCD was deployed)"
        )

        parser.add_argument(
            '--vat-dai-target',
            type=float,
            help="Amount of Dai to keep in the Vat contract (e.g. 2000)")
        parser.add_argument(
            '--keep-dai-in-vat-on-exit',
            dest='exit_dai_on_shutdown',
            action='store_false',
            help=
            "Retain Dai in the Vat on exit, saving gas when restarting the keeper"
        )
        parser.add_argument('--keep-gem-in-vat-on-exit',
                            dest='exit_gem_on_shutdown',
                            action='store_false',
                            help="Retain collateral in the Vat on exit")

        parser.add_argument(
            "--model",
            type=str,
            required=True,
            nargs='+',
            help="Commandline to use in order to start the bidding model")

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

        self.arguments = parser.parse_args(args)

        # Configure connection to the chain
        if self.arguments.rpc_host.startswith("http"):
            endpoint_uri = f"{self.arguments.rpc_host}:{self.arguments.rpc_port}"
        else:
            # Should probably default this to use TLS, but I don't want to break existing configs
            endpoint_uri = f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}"
        self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=endpoint_uri,
                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)

        # Check configuration for retrieving urns/bites
        if self.arguments.type == 'flip' and self.arguments.create_auctions \
                and self.arguments.from_block is None and self.arguments.vulcanize_endpoint is None:
            raise RuntimeError(
                "Either --from-block or --vulcanize-endpoint must be specified to kick off "
                "flip auctions")
        if self.arguments.type == 'flip' and not self.arguments.ilk:
            raise RuntimeError(
                "--ilk must be supplied when configuring a flip keeper")
        if self.arguments.type == 'flop' and self.arguments.create_auctions \
                and self.arguments.from_block is None:
            raise RuntimeError(
                "--from-block must be specified to kick off flop auctions")

        # Configure core and token contracts
        mcd = DssDeployment.from_node(web3=self.web3)
        self.vat = mcd.vat
        self.cat = mcd.cat
        self.vow = mcd.vow
        self.mkr = mcd.mkr
        self.dai_join = mcd.dai_adapter
        if self.arguments.type == 'flip':
            self.collateral = mcd.collaterals[self.arguments.ilk]
            self.ilk = self.collateral.ilk
            self.gem_join = self.collateral.adapter
        else:
            self.collateral = None
            self.ilk = None
            self.gem_join = None

        # Configure auction contracts
        self.flipper = self.collateral.flipper if self.arguments.type == 'flip' else None
        self.flapper = mcd.flapper if self.arguments.type == 'flap' else None
        self.flopper = mcd.flopper if self.arguments.type == 'flop' else None
        self.urn_history = None
        if self.flipper:
            self.min_flip_lot = Wad.from_number(self.arguments.min_flip_lot)
            self.strategy = FlipperStrategy(self.flipper, self.min_flip_lot)
            self.urn_history = UrnHistory(self.web3, mcd, self.ilk,
                                          self.arguments.from_block,
                                          self.arguments.vulcanize_endpoint)
        elif self.flapper:
            self.strategy = FlapperStrategy(self.flapper, self.mkr.address)
        elif self.flopper:
            self.strategy = FlopperStrategy(self.flopper)
        else:
            raise RuntimeError("Please specify auction type")

        # Create the collection used to manage auctions relevant to this keeper
        self.auctions = Auctions(
            flipper=self.flipper.address if self.flipper else None,
            flapper=self.flapper.address if self.flapper else None,
            flopper=self.flopper.address if self.flopper else None,
            model_factory=ModelFactory(' '.join(self.arguments.model)))
        self.auctions_lock = threading.Lock()
        self.dead_auctions = set()

        self.vat_dai_target = Wad.from_number(self.arguments.vat_dai_target) if \
            self.arguments.vat_dai_target is not None else None

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
        # reduce logspew
        logging.getLogger('urllib3').setLevel(logging.INFO)
        logging.getLogger("web3").setLevel(logging.INFO)
        logging.getLogger("asyncio").setLevel(logging.INFO)
        logging.getLogger("requests").setLevel(logging.INFO)
Exemple #2
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='auction-keeper')

        parser.add_argument(
            "--rpc-host",
            type=str,
            default="http://localhost:8545",
            help=
            "JSON-RPC endpoint URI with port (default: `http://localhost: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('--type',
                            type=str,
                            choices=['clip', 'flip', 'flap', 'flop'],
                            help="Auction type in which to participate")
        parser.add_argument(
            '--ilk',
            type=str,
            help=
            "Name of the collateral type for a clip or flip keeper (e.g. 'ETH-B', 'ZRX-A'); "
            "available collateral types can be found at the left side of the Oasis Borrow"
        )

        parser.add_argument(
            '--bid-only',
            dest='create_auctions',
            action='store_false',
            help="Do not take opportunities to create new auctions")
        parser.add_argument('--kick-only',
                            dest='bid_on_auctions',
                            action='store_false',
                            help="Do not bid on auctions")
        parser.add_argument(
            '--deal-for',
            type=str,
            nargs="+",
            help="List of addresses for which auctions will be dealt")

        parser.add_argument('--min-auction',
                            type=int,
                            default=1,
                            help="Lowest auction id to consider")
        parser.add_argument(
            '--max-auctions',
            type=int,
            default=1000,
            help="Maximum number of auctions to simultaneously interact with, "
            "used to manage OS and hardware limitations")
        parser.add_argument(
            '--min-collateral-lot',
            type=float,
            default=0,
            help=
            "Minimum lot size to create or bid upon/take from a collateral auction"
        )
        parser.add_argument(
            '--bid-check-interval',
            type=float,
            default=4.0,
            help=
            "Period of timer [in seconds] used to check bidding models for changes"
        )
        parser.add_argument(
            '--bid-delay',
            type=float,
            default=0.0,
            help=
            "Seconds to wait between bids, used to manage OS and hardware limitations"
        )
        parser.add_argument(
            '--shard-id',
            type=int,
            default=0,
            help=
            "When sharding auctions across multiple keepers, this identifies the shard"
        )
        parser.add_argument(
            '--shards',
            type=int,
            default=1,
            help=
            "Number of shards; should be one greater than your highest --shard-id"
        )

        parser.add_argument(
            '--from-block',
            type=int,
            help=
            "Starting block from which to find vaults to bite or debt to queue "
            "(set to block where MCD was deployed)")
        parser.add_argument(
            '--chunk-size',
            type=int,
            default=20000,
            help=
            "When batching chain history requests, this is the number of blocks for each request"
        )
        parser.add_argument(
            "--tokenflow-url",
            type=str,
            help=
            "When specified, urn history will be initialized using the TokenFlow API"
        )
        parser.add_argument("--tokenflow-key",
                            type=str,
                            help="API key for the TokenFlow endpoint")
        parser.add_argument(
            "--vulcanize-endpoint",
            type=str,
            help=
            "When specified, urn history will be initialized from a VulcanizeDB node"
        )
        parser.add_argument("--vulcanize-key",
                            type=str,
                            help="API key for the Vulcanize endpoint")

        parser.add_argument(
            '--vat-dai-target',
            type=str,
            help=
            "Amount of Dai to keep in the Vat contract or ALL to join entire token balance"
        )
        parser.add_argument(
            '--keep-dai-in-vat-on-exit',
            dest='exit_dai_on_shutdown',
            action='store_false',
            help=
            "Retain Dai in the Vat on exit, saving gas when restarting the keeper"
        )
        parser.add_argument('--keep-gem-in-vat-on-exit',
                            dest='exit_gem_on_shutdown',
                            action='store_false',
                            help="Retain collateral in the Vat on exit")
        parser.add_argument(
            '--return-gem-interval',
            type=int,
            default=300,
            help=
            "Period of timer [in seconds] used to check and exit won collateral"
        )

        parser.add_argument(
            "--model",
            type=str,
            nargs='+',
            help="Commandline to use in order to start the bidding model")

        parser.add_argument(
            "--oracle-gas-price",
            action='store_true',
            help="Use a fast gas price aggregated across multiple oracles")
        parser.add_argument("--ethgasstation-api-key",
                            type=str,
                            default=None,
                            help="EthGasStation API key")
        parser.add_argument("--etherscan-api-key",
                            type=str,
                            default=None,
                            help="Etherscan API key")
        parser.add_argument("--blocknative-api-key",
                            type=str,
                            default=None,
                            help="Blocknative API key")
        parser.add_argument(
            '--fixed-gas-price',
            type=float,
            default=None,
            help=
            "Uses a fixed value (in Gwei) instead of an external API to determine initial gas"
        )
        parser.add_argument("--poanetwork-url",
                            type=str,
                            default=None,
                            help="Alternative POANetwork URL")
        parser.add_argument(
            "--gas-initial-multiplier",
            type=float,
            default=1.0,
            help=
            "Adjusts the initial API-provided 'fast' gas price, default 1.0")
        parser.add_argument(
            "--gas-reactive-multiplier",
            type=float,
            default=1.125,
            help=
            "Increases gas price when transactions haven't been mined after some time"
        )
        parser.add_argument(
            "--gas-maximum",
            type=float,
            default=2000,
            help=
            "Places an upper bound (in Gwei) on the amount of gas to use for a single TX"
        )

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

        self.arguments = parser.parse_args(args)

        # Configure connection to the chain
        self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else web3_via_http(
            endpoint_uri=self.arguments.rpc_host,
            timeout=self.arguments.rpc_timeout,
            http_pool_size=100)
        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        # Check configuration for retrieving urns/bites
        if self.arguments.type in ['clip', 'flip'] and self.arguments.create_auctions \
                and self.arguments.from_block is None \
                and self.arguments.tokenflow_url is None \
                and self.arguments.vulcanize_endpoint is None:
            raise RuntimeError(
                "One of --from-block, --tokenflow_url, or --vulcanize-endpoint must be specified "
                "to bite and kick off new collateral auctions")
        if self.arguments.type in ['clip', 'flip'] and not self.arguments.ilk:
            raise RuntimeError(
                "--ilk must be supplied when configuring a collateral auction keeper"
            )
        if self.arguments.type == 'flop' and self.arguments.create_auctions \
                and self.arguments.from_block is None:
            raise RuntimeError(
                "--from-block must be specified to kick off flop auctions")

        # Configure core and token contracts
        self.mcd = DssDeployment.from_node(web3=self.web3)
        self.vat = self.mcd.vat
        self.vow = self.mcd.vow
        self.mkr = self.mcd.mkr
        self.dai_join = self.mcd.dai_adapter
        if self.arguments.type in ['clip', 'flip']:
            self.collateral = self.mcd.collaterals[self.arguments.ilk]
            self.ilk = self.collateral.ilk
            self.gem_join = self.collateral.adapter
        else:
            self.collateral = None
            self.ilk = None
            self.gem_join = None

        # Configure auction contracts
        self.auction_contract = self.get_contract()
        self.auction_type = None
        is_collateral_auction = False
        self.is_dealable = True
        self.urn_history = None

        if isinstance(self.auction_contract, Clipper):
            self.auction_type = 'clip'
            is_collateral_auction = True
            self.min_collateral_lot = Wad.from_number(
                self.arguments.min_collateral_lot)
            self.is_dealable = False
            self.strategy = ClipperStrategy(self.auction_contract,
                                            self.min_collateral_lot)
        elif isinstance(self.auction_contract, Flipper):
            self.auction_type = 'flip'
            is_collateral_auction = True
            self.min_collateral_lot = Wad.from_number(
                self.arguments.min_collateral_lot)
            self.strategy = FlipperStrategy(self.auction_contract,
                                            self.min_collateral_lot)
        elif isinstance(self.auction_contract, Flapper):
            self.auction_type = 'flap'
            self.strategy = FlapperStrategy(self.auction_contract,
                                            self.mkr.address)
        elif isinstance(self.auction_contract, Flopper):
            self.auction_type = 'flop'
            self.strategy = FlopperStrategy(self.auction_contract)
        else:
            raise RuntimeError(
                f"{self.auction_contract} auction contract is not supported")

        if is_collateral_auction and self.arguments.create_auctions:
            if self.arguments.vulcanize_endpoint:
                self.urn_history = VulcanizeUrnHistoryProvider(
                    self.mcd, self.ilk, self.arguments.vulcanize_endpoint,
                    self.arguments.vulcanize_key)
            elif self.arguments.tokenflow_url:
                self.urn_history = TokenFlowUrnHistoryProvider(
                    self.web3, self.mcd, self.ilk,
                    self.arguments.tokenflow_url, self.arguments.tokenflow_key,
                    self.arguments.chunk_size)
            else:
                self.urn_history = ChainUrnHistoryProvider(
                    self.web3, self.mcd, self.ilk, self.arguments.from_block,
                    self.arguments.chunk_size)

        # Create the collection used to manage auctions relevant to this keeper
        if self.arguments.model:
            model_command = ' '.join(self.arguments.model)
        else:
            if self.arguments.bid_on_auctions:
                raise RuntimeError(
                    "--model must be specified to bid on auctions")
            else:
                model_command = ":"
        self.auctions = Auctions(auction_contract=self.auction_contract,
                                 model_factory=ModelFactory(model_command))
        self.auctions_lock = threading.Lock()
        # Since we don't want periodically-pollled bidding threads to back up, use a flag instead of a lock.
        self.is_joining_dai = False
        self.dead_since = {}
        self.lifecycle = None

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

        # Create gas strategy used for non-bids and bids which do not supply gas price
        self.gas_price = DynamicGasPrice(self.arguments, self.web3)

        # Configure account(s) for which we'll deal auctions
        self.deal_all = False
        self.deal_for = set()
        if self.is_dealable:
            if self.arguments.deal_for is None:
                self.deal_for.add(self.our_address)
            elif len(
                    self.arguments.deal_for
            ) == 1 and self.arguments.deal_for[0].upper() in ["ALL", "NONE"]:
                if self.arguments.deal_for[0].upper() == "ALL":
                    self.deal_all = True
                # else no auctions will be dealt
            elif len(self.arguments.deal_for) > 0:
                for account in self.arguments.deal_for:
                    self.deal_for.add(Address(account))

        # reduce logspew
        logging.getLogger('urllib3').setLevel(logging.INFO)
        logging.getLogger("web3").setLevel(logging.INFO)
        logging.getLogger("asyncio").setLevel(logging.INFO)
        logging.getLogger("requests").setLevel(logging.INFO)
Exemple #3
0
class AuctionKeeper:
    logger = logging.getLogger()

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='auction-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('--type',
                            type=str,
                            choices=['flip', 'flap', 'flop'],
                            help="Auction type in which to participate")
        parser.add_argument(
            '--ilk',
            type=str,
            help=
            "Name of the collateral type for a flip keeper (e.g. 'ETH-B', 'ZRX-A'); "
            "available collateral types can be found at the left side of the CDP Portal"
        )

        parser.add_argument(
            '--bid-only',
            dest='create_auctions',
            action='store_false',
            help="Do not take opportunities to create new auctions")
        parser.add_argument(
            '--max-auctions',
            type=int,
            default=100,
            help="Maximum number of auctions to simultaneously interact with, "
            "used to manage OS and hardware limitations")
        parser.add_argument(
            '--min-flip-lot',
            type=float,
            default=0,
            help="Minimum lot size to create or bid upon a flip auction")

        parser.add_argument(
            "--vulcanize-endpoint",
            type=str,
            help=
            "When specified, frob history will be queried from a VulcanizeDB lite node, "
            "reducing load on the Ethereum node for flip auctions")
        parser.add_argument(
            '--from-block',
            type=int,
            help=
            "Starting block from which to look at history (set to block where MCD was deployed)"
        )

        parser.add_argument(
            '--vat-dai-target',
            type=float,
            help="Amount of Dai to keep in the Vat contract (e.g. 2000)")
        parser.add_argument(
            '--keep-dai-in-vat-on-exit',
            dest='exit_dai_on_shutdown',
            action='store_false',
            help=
            "Retain Dai in the Vat on exit, saving gas when restarting the keeper"
        )
        parser.add_argument('--keep-gem-in-vat-on-exit',
                            dest='exit_gem_on_shutdown',
                            action='store_false',
                            help="Retain collateral in the Vat on exit")

        parser.add_argument(
            "--model",
            type=str,
            required=True,
            nargs='+',
            help="Commandline to use in order to start the bidding model")

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

        self.arguments = parser.parse_args(args)

        # Configure connection to the chain
        if self.arguments.rpc_host.startswith("http"):
            endpoint_uri = f"{self.arguments.rpc_host}:{self.arguments.rpc_port}"
        else:
            # Should probably default this to use TLS, but I don't want to break existing configs
            endpoint_uri = f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}"
        self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=endpoint_uri,
                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)

        # Check configuration for retrieving urns/bites
        if self.arguments.type == 'flip' and self.arguments.create_auctions \
                and self.arguments.from_block is None and self.arguments.vulcanize_endpoint is None:
            raise RuntimeError(
                "Either --from-block or --vulcanize-endpoint must be specified to kick off "
                "flip auctions")
        if self.arguments.type == 'flip' and not self.arguments.ilk:
            raise RuntimeError(
                "--ilk must be supplied when configuring a flip keeper")
        if self.arguments.type == 'flop' and self.arguments.create_auctions \
                and self.arguments.from_block is None:
            raise RuntimeError(
                "--from-block must be specified to kick off flop auctions")

        # Configure core and token contracts
        mcd = DssDeployment.from_node(web3=self.web3)
        self.vat = mcd.vat
        self.cat = mcd.cat
        self.vow = mcd.vow
        self.mkr = mcd.mkr
        self.dai_join = mcd.dai_adapter
        if self.arguments.type == 'flip':
            self.collateral = mcd.collaterals[self.arguments.ilk]
            self.ilk = self.collateral.ilk
            self.gem_join = self.collateral.adapter
        else:
            self.collateral = None
            self.ilk = None
            self.gem_join = None

        # Configure auction contracts
        self.flipper = self.collateral.flipper if self.arguments.type == 'flip' else None
        self.flapper = mcd.flapper if self.arguments.type == 'flap' else None
        self.flopper = mcd.flopper if self.arguments.type == 'flop' else None
        self.urn_history = None
        if self.flipper:
            self.min_flip_lot = Wad.from_number(self.arguments.min_flip_lot)
            self.strategy = FlipperStrategy(self.flipper, self.min_flip_lot)
            self.urn_history = UrnHistory(self.web3, mcd, self.ilk,
                                          self.arguments.from_block,
                                          self.arguments.vulcanize_endpoint)
        elif self.flapper:
            self.strategy = FlapperStrategy(self.flapper, self.mkr.address)
        elif self.flopper:
            self.strategy = FlopperStrategy(self.flopper)
        else:
            raise RuntimeError("Please specify auction type")

        # Create the collection used to manage auctions relevant to this keeper
        self.auctions = Auctions(
            flipper=self.flipper.address if self.flipper else None,
            flapper=self.flapper.address if self.flapper else None,
            flopper=self.flopper.address if self.flopper else None,
            model_factory=ModelFactory(' '.join(self.arguments.model)))
        self.auctions_lock = threading.Lock()
        self.dead_auctions = set()

        self.vat_dai_target = Wad.from_number(self.arguments.vat_dai_target) if \
            self.arguments.vat_dai_target is not None else None

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
        # reduce logspew
        logging.getLogger('urllib3').setLevel(logging.INFO)
        logging.getLogger("web3").setLevel(logging.INFO)
        logging.getLogger("asyncio").setLevel(logging.INFO)
        logging.getLogger("requests").setLevel(logging.INFO)

    def main(self):
        def seq_func(check_func: callable):
            assert callable(check_func)
            if self.arguments.create_auctions:
                try:
                    check_func()
                except (RequestException, ConnectionError, ValueError,
                        AttributeError):
                    logging.exception(
                        "Error checking for opportunities to start an auction")

            try:
                self.check_all_auctions()
            except (RequestException, ConnectionError, ValueError,
                    AttributeError):
                logging.exception("Error checking auction states")

        with Lifecycle(self.web3) as lifecycle:
            lifecycle.on_startup(self.startup)
            lifecycle.on_shutdown(self.shutdown)
            if self.flipper and self.cat:
                lifecycle.on_block(
                    functools.partial(seq_func, check_func=self.check_cdps))
            elif self.flapper and self.vow:
                lifecycle.on_block(
                    functools.partial(seq_func, check_func=self.check_flap))
            elif self.flopper and self.vow:
                lifecycle.on_block(
                    functools.partial(seq_func, check_func=self.check_flop))
            else:  # unusual corner case
                lifecycle.on_block(self.check_all_auctions)

            lifecycle.every(2, self.check_for_bids)

    def startup(self):
        self.approve()
        self.rebalance_dai()
        if self.flapper:
            self.logger.info(
                f"MKR balance is {self.mkr.balance_of(self.our_address)}")

        if not self.arguments.create_auctions:
            logging.info("Keeper will not create new auctions")

    def approve(self):
        self.strategy.approve()
        time.sleep(2)
        if self.dai_join:
            self.dai_join.approve(hope_directly(), self.vat.address)
            time.sleep(2)
            self.dai_join.dai().approve(self.dai_join.address).transact()

    def shutdown(self):
        with self.auctions_lock:
            del self.auctions
            self.exit_dai_on_shutdown()
            self.exit_collateral_on_shutdown()

    def exit_dai_on_shutdown(self):
        if not self.arguments.exit_dai_on_shutdown or not self.dai_join:
            return

        vat_balance = Wad(self.vat.dai(self.our_address))
        if vat_balance > Wad(0):
            self.logger.info(
                f"Exiting {str(vat_balance)} Dai from the Vat before shutdown")
            assert self.dai_join.exit(self.our_address, vat_balance).transact()

    def exit_collateral_on_shutdown(self):
        if not self.arguments.exit_gem_on_shutdown or not self.gem_join:
            return

        vat_balance = self.vat.gem(self.ilk, self.our_address)
        if vat_balance > Wad(0):
            self.logger.info(
                f"Exiting {str(vat_balance)} {self.ilk.name} from the Vat before shutdown"
            )
            assert self.gem_join.exit(self.our_address, vat_balance).transact()

    def check_cdps(self):
        started = datetime.now()
        ilk = self.vat.ilk(self.ilk.name)
        rate = ilk.rate
        dai_to_bid = self.vat.dai(self.our_address)

        # Look for unsafe CDPs and bite them
        urns = self.urn_history.get_urns()
        for urn in urns.values():
            safe = urn.ink * ilk.spot >= urn.art * rate
            if not safe:
                if dai_to_bid == Rad(0):
                    self.logger.warning(
                        f"Skipping opportunity to bite urn {urn.address} "
                        "because there is no Dai to bid")
                    break

                if urn.ink < self.min_flip_lot:
                    self.logger.info(
                        f"Ignoring urn {urn.address.address} with ink={urn.ink} < "
                        f"min_lot={self.min_flip_lot}")
                    continue

                self._run_future(self.cat.bite(ilk, urn).transact_async())

        self.logger.debug(
            f"Checked {len(urns)} urns in {(datetime.now()-started).seconds} seconds"
        )
        # Cat.bite implicitly kicks off the flip auction; no further action needed.

    def check_flap(self):
        # Check if Vow has a surplus of Dai compared to bad debt
        joy = self.vat.dai(self.vow.address)
        awe = self.vat.sin(self.vow.address)

        # Check if Vow has Dai in excess
        if joy > awe:
            bump = self.vow.bump()
            hump = self.vow.hump()

            # Check if Vow has enough Dai surplus to start an auction and that we have enough mkr balance
            if (joy - awe) >= (bump + hump):

                if self.mkr.balance_of(self.our_address) == Wad(0):
                    self.logger.warning(
                        "Skipping opportunity to heal/flap because there is no MKR to bid"
                    )
                    return

                woe = self.vow.woe()
                # Heal the system to bring Woe to 0
                if woe > Rad(0):
                    self.vow.heal(woe).transact()
                self.vow.flap().transact()

    def check_flop(self):
        # Check if Vow has a surplus of bad debt compared to Dai
        joy = self.vat.dai(self.vow.address)
        awe = self.vat.sin(self.vow.address)

        # Check if Vow has bad debt in excess
        excess_debt = joy < awe
        if not excess_debt:
            return

        woe = self.vow.woe()
        sin = self.vow.sin()
        sump = self.vow.sump()
        wait = self.vow.wait()

        # Check if Vow has enough bad debt to start an auction and that we have enough dai balance
        if woe + sin >= sump:
            # We need to bring Joy to 0 and Woe to at least sump

            if self.vat.dai(self.our_address) == Rad(0):
                self.logger.warning(
                    "Skipping opportunity to kiss/flog/heal/flop because there is no Dai to bid"
                )
                return

            # first use kiss() as it settled bad debt already in auctions and doesn't decrease woe
            ash = self.vow.ash()
            goodnight = min(ash, joy)
            if goodnight > Rad(0):
                self.vow.kiss(goodnight).transact()

            # Convert enough sin in woe to have woe >= sump + joy
            if woe < (sump + joy) and self.cat is not None:
                past_blocks = self.web3.eth.blockNumber - self.arguments.from_block
                for bite_event in self.cat.past_bites(
                        past_blocks):  # TODO: cache ?
                    era = bite_event.era(self.web3)
                    now = self.web3.eth.getBlock('latest')['timestamp']
                    sin = self.vow.sin_of(era)
                    # If the bite hasn't already been flogged and has aged past the `wait`
                    if sin > Rad(0) and era + wait <= now:
                        self.vow.flog(era).transact()

                        # flog() sin until woe is above sump + joy
                        joy = self.vat.dai(self.vow.address)
                        if self.vow.woe() - joy >= sump:
                            break

            # use heal() for reconciling the remaining joy
            joy = self.vat.dai(self.vow.address)
            if Rad(0) < joy <= self.vow.woe():
                self.vow.heal(joy).transact()
                # heal() changes joy and woe (the balance of surplus and debt)
                joy = self.vat.dai(self.vow.address)

            woe = self.vow.woe()
            if sump <= woe and joy == Rad(0):
                self.vow.flop().transact()

    def check_all_auctions(self):
        started = datetime.now()
        for id in range(1, self.strategy.kicks() + 1):
            with self.auctions_lock:
                if not self.check_auction(id):
                    continue

                # Prevent growing the auctions collection beyond the configured size
                if len(self.auctions.auctions) < self.arguments.max_auctions:
                    self.feed_model(id)
                else:
                    logging.warning(
                        f"Processing {len(self.auctions.auctions)} auctions; "
                        f"ignoring auction {id} and beyond")
                    break

        self.logger.debug(
            f"Checked {self.strategy.kicks()} auctions in {(datetime.now() - started).seconds} seconds"
        )

    def check_for_bids(self):
        with self.auctions_lock:
            for id, auction in self.auctions.auctions.items():
                self.handle_bid(id=id, auction=auction)

    # TODO if we will introduce multithreading here, proper locking should be introduced as well
    #     locking should not happen on `auction.lock`, but on auction.id here. as sometimes we will
    #     intend to lock on auction id but not create `Auction` object for it (as the auction is already finished
    #     for example).
    def check_auction(self, id: int) -> bool:
        assert isinstance(id, int)

        if id in self.dead_auctions:
            return False

        # Read auction information
        input = self.strategy.get_input(id)
        auction_missing = (input.end == 0)
        auction_finished = (input.tic < input.era
                            and input.tic != 0) or (input.end < input.era)

        if auction_missing:
            # Try to remove the auction so the model terminates and we stop tracking it.
            # If auction has already been removed, nothing happens.
            self.auctions.remove_auction(id)
            self.dead_auctions.add(id)
            return False

        # Check if the auction is finished.
        # If it is finished and we are the winner, `deal` the auction.
        # If it is finished and we aren't the winner, there is no point in carrying on with this auction.
        elif auction_finished:
            if input.guy == self.our_address:
                # Always using default gas price for `deal`
                self._run_future(
                    self.strategy.deal(id).transact_async(
                        gas_price=DefaultGasPrice()))

                # Upon winning a flip or flop auction, we may need to replenish Dai to the Vat.
                # Upon winning a flap auction, we may want to withdraw won Dai from the Vat.
                self.rebalance_dai()

            else:
                # Try to remove the auction so the model terminates and we stop tracking it.
                # If auction has already been removed, nothing happens.
                self.auctions.remove_auction(id)
            return False

        else:
            return True

    def feed_model(self, id: int):
        assert isinstance(id, int)

        auction = self.auctions.get_auction(id)
        input = self.strategy.get_input(id)

        # Feed the model with current state
        auction.feed_model(input)

    def handle_bid(self, id: int, auction: Auction):
        assert isinstance(id, int)
        assert isinstance(auction, Auction)

        output = auction.model_output()

        if output is not None:
            bid_price, bid_transact, cost = self.strategy.bid(id, output.price)
            # If we can't afford the bid, log a warning/error and back out.
            # By continuing, we'll burn through gas fees while the keeper pointlessly retries the bid.
            if cost is not None:
                if not self.check_bid_cost(cost):
                    return

            if bid_price is not None and bid_transact is not None:
                # if no transaction in progress, send a new one
                transaction_in_progress = auction.transaction_in_progress()

                if transaction_in_progress is None:
                    self.logger.info(
                        f"Sending new bid @{output.price} (gas_price={output.gas_price})"
                    )

                    auction.price = bid_price
                    auction.gas_price = UpdatableGasPrice(output.gas_price)
                    auction.register_transaction(bid_transact)

                    self._run_future(
                        bid_transact.transact_async(
                            gas_price=auction.gas_price))

                # if transaction in progress and gas price went up...
                elif output.gas_price and output.gas_price > auction.gas_price.gas_price:

                    # ...replace the entire bid if the price has changed...
                    if bid_price != auction.price:
                        self.logger.info(
                            f"Overriding pending bid with new bid @{output.price} (gas_price={output.gas_price})"
                        )

                        auction.price = bid_price
                        auction.gas_price = UpdatableGasPrice(output.gas_price)
                        auction.register_transaction(bid_transact)

                        self._run_future(
                            bid_transact.transact_async(
                                replace=transaction_in_progress,
                                gas_price=auction.gas_price))
                    # ...or just replace gas_price if price stays the same
                    else:
                        self.logger.info(
                            f"Overriding pending bid with new gas_price ({output.gas_price})"
                        )

                        auction.gas_price.update_gas_price(output.gas_price)

    def check_bid_cost(self, cost: Rad) -> bool:
        assert isinstance(cost, Rad)

        # If this is an auction where we bid with Dai...
        if self.flipper or self.flopper:
            vat_dai = self.vat.dai(self.our_address)
            if cost > vat_dai:
                self.logger.debug(
                    f"Bid cost {str(cost)} exceeds vat balance of {vat_dai}; "
                    "bid will not be submitted")
                return False
        # If this is an auction where we bid with MKR...
        elif self.flapper:
            mkr_balance = self.mkr.balance_of(self.our_address)
            if cost > Rad(mkr_balance):
                self.logger.debug(
                    f"Bid cost {str(cost)} exceeds MKR balance of {mkr_balance}; "
                    "bid will not be submitted")
                return False
        return True

    def rebalance_dai(self):
        if self.vat_dai_target is None or not self.dai_join or (
                not self.flipper and not self.flopper):
            return

        dai = self.dai_join.dai()
        token_balance = dai.balance_of(self.our_address)  # Wad
        difference = Wad(self.vat.dai(
            self.our_address)) - self.vat_dai_target  # Wad
        if difference < Wad(0):
            # Join tokens to the vat
            if token_balance >= difference * -1:
                self.logger.info(
                    f"Joining {str(difference * -1)} Dai to the Vat")
                assert self.dai_join.join(self.our_address,
                                          difference * -1).transact()
            elif token_balance > Wad(0):
                self.logger.warning(
                    f"Insufficient balance to maintain Dai target; joining {str(token_balance)} "
                    "Dai to the Vat")
                assert self.dai_join.join(self.our_address,
                                          token_balance).transact()
            else:
                self.logger.warning(
                    "No Dai is available to join to Vat; cannot maintain Dai target"
                )
        elif difference > Wad(0):
            # Exit dai from the vat
            self.logger.info(f"Exiting {str(difference)} Dai from the Vat")
            assert self.dai_join.exit(self.our_address, difference).transact()
        self.logger.info(
            f"Dai token balance: {str(dai.balance_of(self.our_address))}, "
            f"Vat balance: {self.vat.dai(self.our_address)}")

    @staticmethod
    def _run_future(future):
        def worker():
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            try:
                asyncio.get_event_loop().run_until_complete(future)
            finally:
                loop.close()

        thread = threading.Thread(target=worker, daemon=True)
        thread.start()
Exemple #4
0
class AuctionKeeper:
    logger = logging.getLogger()
    dead_after = 10  # Assume block reorgs cannot resurrect an auction id after this many blocks

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='auction-keeper')

        parser.add_argument(
            "--rpc-host",
            type=str,
            default="http://*****:*****@{output.price} for auction {id}")
                auction.price = bid_price
                auction.gas_price = new_gas_strategy if new_gas_strategy else auction.gas_price
                auction.register_transaction(bid_transact)

                # ...submit a new transaction and wait the delay period (if so configured)
                self._run_future(
                    bid_transact.transact_async(gas_price=auction.gas_price))
                if self.arguments.bid_delay:
                    logging.debug(f"Waiting {self.arguments.bid_delay}s")
                    time.sleep(self.arguments.bid_delay)

            # if transaction in progress and the bid price changed...
            elif auction.price and bid_price != auction.price:
                self.logger.info(
                    f"Attempting to override pending bid with new bid @{output.price} for auction {id}"
                )
                auction.price = bid_price
                if new_gas_strategy:  # gas strategy changed
                    auction.gas_price = new_gas_strategy
                elif fixed_gas_price_changed:  # gas price updated
                    assert isinstance(auction.gas_price, UpdatableGasPrice)
                    auction.gas_price.update_gas_price(output.gas_price)
                auction.register_transaction(bid_transact)

                # ...ask pymaker to replace the transaction
                self._run_future(
                    bid_transact.transact_async(
                        replace=transaction_in_progress,
                        gas_price=auction.gas_price))

            # if model has been providing a gas price, and only that changed...
            elif fixed_gas_price_changed:
                assert isinstance(auction.gas_price, UpdatableGasPrice)
                self.logger.info(
                    f"Overriding pending bid with new gas_price ({output.gas_price}) for auction {id}"
                )
                auction.gas_price.update_gas_price(output.gas_price)

            # if transaction in progress, bid price unchanged, but gas strategy changed...
            elif new_gas_strategy:
                self.logger.info(
                    f"Changing gas strategy for pending bid @{output.price} for auction {id}"
                )
                auction.price = bid_price
                auction.gas_price = new_gas_strategy
                auction.register_transaction(bid_transact)

                # ...ask pymaker to replace the transaction
                self._run_future(
                    bid_transact.transact_async(
                        replace=transaction_in_progress,
                        gas_price=auction.gas_price))

    def check_bid_cost(self,
                       id: int,
                       cost: Rad,
                       reservoir: Reservoir,
                       already_rebalanced=False) -> bool:
        assert isinstance(id, int)
        assert isinstance(cost, Rad)

        # If this is an auction where we bid with Dai...
        if self.auction_type in ['clip', 'flip', 'flop']:
            if not reservoir.check_bid_cost(id, cost):
                if not already_rebalanced:
                    # Try to synchronously join Dai the Vat
                    if self.is_joining_dai:
                        self.logger.info(
                            f"Bid cost {str(cost)} exceeds reservoir level of {reservoir.level}; "
                            "waiting for Dai to rebalance")
                        return False
                    else:
                        rebalanced = self.rebalance_dai()
                        if rebalanced and rebalanced > Wad(0):
                            reservoir.refill(Rad(rebalanced))
                            return self.check_bid_cost(id,
                                                       cost,
                                                       reservoir,
                                                       already_rebalanced=True)

                self.logger.info(
                    f"Bid cost {str(cost)} exceeds reservoir level of {reservoir.level}; "
                    "bid will not be submitted")
                return False
        # If this is an auction where we bid with MKR...
        elif self.auction_type == 'flap':
            mkr_balance = self.mkr.balance_of(self.our_address)
            if cost > Rad(mkr_balance):
                self.logger.debug(
                    f"Bid cost {str(cost)} exceeds reservoir level of {reservoir.level}; "
                    "bid will not be submitted")
                return False
        return True

    def rebalance_dai(self) -> Optional[Wad]:
        # Returns amount joined (positive) or exited (negative) as a result of rebalancing towards vat_dai_target

        if self.arguments.vat_dai_target is None:
            return None

        logging.info(
            f"Checking if internal Dai balance needs to be rebalanced")
        dai = self.dai_join.dai()
        token_balance = dai.balance_of(self.our_address)  # Wad
        # Prevent spending gas on small rebalances
        dust = Wad(self.mcd.vat.ilk(
            self.ilk.name).dust) if self.ilk else Wad.from_number(20)

        dai_to_join = Wad(0)
        dai_to_exit = Wad(0)
        try:
            if self.arguments.vat_dai_target.upper() == "ALL":
                dai_to_join = token_balance
            else:
                dai_target = Wad.from_number(
                    float(self.arguments.vat_dai_target))
                if dai_target < dust:
                    self.logger.warning(
                        f"Dust cutoff of {dust} exceeds Dai target {dai_target}; "
                        "please adjust configuration accordingly")
                vat_balance = Wad(self.vat.dai(self.our_address))
                if vat_balance < dai_target:
                    dai_to_join = dai_target - vat_balance
                elif vat_balance > dai_target:
                    dai_to_exit = vat_balance - dai_target
        except ValueError:
            raise ValueError("Unsupported --vat-dai-target")

        if dai_to_join >= dust:
            # Join tokens to the vat
            if token_balance >= dai_to_join:
                self.logger.info(f"Joining {str(dai_to_join)} Dai to the Vat")
                return self.join_dai(dai_to_join)
            elif token_balance > Wad(0):
                self.logger.warning(
                    f"Insufficient balance to maintain Dai target; joining {str(token_balance)} "
                    "Dai to the Vat")
                return self.join_dai(token_balance)
            else:
                self.logger.warning(
                    "Insufficient Dai is available to join to Vat; cannot maintain Dai target"
                )
                return Wad(0)
        elif dai_to_exit > dust:
            # Exit dai from the vat
            self.logger.info(f"Exiting {str(dai_to_exit)} Dai from the Vat")
            assert self.dai_join.exit(
                self.our_address,
                dai_to_exit).transact(gas_price=self.gas_price)
            return dai_to_exit * -1
        self.logger.info(
            f"Dai token balance: {str(dai.balance_of(self.our_address))}, "
            f"Vat balance: {self.vat.dai(self.our_address)}")

    def join_dai(self, amount: Wad):
        assert isinstance(amount, Wad)
        assert not self.is_joining_dai
        try:
            self.is_joining_dai = True
            assert self.dai_join.join(
                self.our_address, amount).transact(gas_price=self.gas_price)
        finally:
            self.is_joining_dai = False
        return amount

    def exit_gem(self):
        if not self.collateral:
            return

        token = Token(
            self.collateral.ilk.name.split('-')[0],
            self.collateral.gem.address, self.collateral.adapter.dec())
        vat_balance = self.vat.gem(self.ilk, self.our_address)
        if vat_balance > token.min_amount:
            self.logger.info(
                f"Exiting {str(vat_balance)} {self.ilk.name} from the Vat")
            assert self.gem_join.exit(
                self.our_address,
                token.unnormalize_amount(vat_balance)).transact(
                    gas_price=self.gas_price)

    @staticmethod
    def _run_future(future):
        def worker():
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            try:
                asyncio.get_event_loop().run_until_complete(future)
            finally:
                loop.close()

        thread = threading.Thread(target=worker, daemon=True)
        thread.start()
Exemple #5
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='auction-keeper')

        parser.add_argument(
            "--rpc-host",
            type=str,
            default="http://localhost:8545",
            help=
            "JSON-RPC endpoint URI with port (default: `http://localhost: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('--type',
                            type=str,
                            choices=['flip', 'flap', 'flop'],
                            help="Auction type in which to participate")
        parser.add_argument(
            '--ilk',
            type=str,
            help=
            "Name of the collateral type for a flip keeper (e.g. 'ETH-B', 'ZRX-A'); "
            "available collateral types can be found at the left side of the CDP Portal"
        )

        parser.add_argument(
            '--bid-only',
            dest='create_auctions',
            action='store_false',
            help="Do not take opportunities to create new auctions")
        parser.add_argument('--kick-only',
                            dest='bid_on_auctions',
                            action='store_false',
                            help="Do not bid on auctions")
        parser.add_argument(
            '--deal-for',
            type=str,
            nargs="+",
            help="List of addresses for which auctions will be dealt")

        parser.add_argument('--min-auction',
                            type=int,
                            default=1,
                            help="Lowest auction id to consider")
        parser.add_argument(
            '--max-auctions',
            type=int,
            default=1000,
            help="Maximum number of auctions to simultaneously interact with, "
            "used to manage OS and hardware limitations")
        parser.add_argument(
            '--min-flip-lot',
            type=float,
            default=0,
            help="Minimum lot size to create or bid upon a flip auction")
        parser.add_argument(
            '--bid-check-interval',
            type=float,
            default=2.0,
            help=
            "Period of timer [in seconds] used to check bidding models for changes"
        )
        parser.add_argument(
            '--bid-delay',
            type=float,
            default=0.0,
            help=
            "Seconds to wait between bids, used to manage OS and hardware limitations"
        )
        parser.add_argument(
            '--shard-id',
            type=int,
            default=0,
            help=
            "When sharding auctions across multiple keepers, this identifies the shard"
        )
        parser.add_argument(
            '--shards',
            type=int,
            default=1,
            help=
            "Number of shards; should be one greater than your highest --shard-id"
        )

        parser.add_argument(
            "--vulcanize-endpoint",
            type=str,
            help=
            "When specified, frob history will be queried from a VulcanizeDB lite node, "
            "reducing load on the Ethereum node for flip auctions")
        parser.add_argument(
            '--from-block',
            type=int,
            help=
            "Starting block from which to find vaults to bite or debt to queue "
            "(set to block where MCD was deployed)")

        parser.add_argument(
            '--vat-dai-target',
            type=float,
            help="Amount of Dai to keep in the Vat contract (e.g. 2000)")
        parser.add_argument(
            '--keep-dai-in-vat-on-exit',
            dest='exit_dai_on_shutdown',
            action='store_false',
            help=
            "Retain Dai in the Vat on exit, saving gas when restarting the keeper"
        )
        parser.add_argument('--keep-gem-in-vat-on-exit',
                            dest='exit_gem_on_shutdown',
                            action='store_false',
                            help="Retain collateral in the Vat on exit")

        parser.add_argument(
            "--model",
            type=str,
            required=True,
            nargs='+',
            help="Commandline to use in order to start the bidding model")
        parser.add_argument(
            "--max-gem-balance",
            type=float,
            required=True,
            help=
            "Max gem (e.g. ETH, BAT) balance to store in keeper account before selling for DAI"
        )
        parser.add_argument(
            "--max-gem-sale",
            type=float,
            required=True,
            help=
            "Max gem (e.g. ETH, BAT) to sell in a single transaction in order to reduce risk of slippage"
        )
        parser.add_argument("--gem-eth-ratio",
                            type=float,
                            default=1,
                            help="gem/eth ratio for estimating gas price")
        parser.add_argument(
            "--profit-margin",
            type=float,
            default=0.01,
            help="Minimum percent discount from feed price for bidding")
        parser.add_argument(
            "--tab-discount",
            type=float,
            nargs=4,
            help=
            "Enables adaptive profit margins based on the amount of total Dai currently being auctioned. Useful for detecting when large CDPs or rapid declines in ETH price have occurred. Profit margins will be added to base profit margin.  Usage = tab level 1 in dai, discount 1, tab level 2 in dai, discount 2",
            default=[100000, .15, 25000, .01])
        parser.add_argument(
            "--bid-start-time",
            type=float,
            default=30,
            help="Number of minutes before auction end time to start bidding")
        parser.add_argument(
            "--force-premium",
            dest='force_premium',
            action='store_true',
            help=
            "Use to enable bidding with negative margins (i.e. above the market feed rate)"
        )
        gas_group = parser.add_mutually_exclusive_group()
        gas_group.add_argument("--ethgasstation-api-key",
                               type=str,
                               default=None,
                               help="ethgasstation API key")
        gas_group.add_argument('--etherchain-gas-price',
                               dest='etherchain_gas',
                               action='store_true',
                               help="Use etherchain.org gas price")
        gas_group.add_argument('--poanetwork-gas-price',
                               dest='poanetwork_gas',
                               action='store_true',
                               help="Use POANetwork gas price")
        gas_group.add_argument(
            '--fixed-gas-price',
            type=float,
            default=None,
            help=
            "Uses a fixed value (in Gwei) instead of an external API to determine initial gas"
        )
        parser.add_argument("--poanetwork-url",
                            type=str,
                            default=None,
                            help="Alternative POANetwork URL")
        parser.add_argument(
            "--gas-initial-multiplier",
            type=float,
            default=1.0,
            help=
            "Adjusts the initial API-provided 'fast' gas price, default 1.0")
        parser.add_argument(
            "--gas-reactive-multiplier",
            type=float,
            default=2.25,
            help=
            "Increases gas price when transactions haven't been mined after some time"
        )
        parser.add_argument(
            "--gas-maximum",
            type=float,
            default=5000,
            help=
            "Places an upper bound (in Gwei) on the amount of gas to use for a single TX"
        )

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

        self.arguments = parser.parse_args(args)

        # Configure connection to the chain
        provider = HTTPProvider(
            endpoint_uri=self.arguments.rpc_host,
            request_kwargs={'timeout': self.arguments.rpc_timeout})
        self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            provider)
        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        # Check configuration for retrieving urns/bites
        if self.arguments.type == 'flip' and self.arguments.create_auctions \
                and self.arguments.from_block is None and self.arguments.vulcanize_endpoint is None:
            raise RuntimeError(
                "Either --from-block or --vulcanize-endpoint must be specified to kick off "
                "flip auctions")
        if self.arguments.type == 'flip' and not self.arguments.ilk:
            raise RuntimeError(
                "--ilk must be supplied when configuring a flip keeper")
        if self.arguments.type == 'flop' and self.arguments.create_auctions \
                and self.arguments.from_block is None:
            raise RuntimeError(
                "--from-block must be specified to kick off flop auctions")

        if self.arguments.type != 'flip':
            raise RuntimeError("Thrifty Keeper only works with Flip auctions")

        if self.arguments.profit_margin < 0 and not self.arguments.force_premium:
            raise RuntimeError(
                "Negative profit margins will place bids above market price.  Run with '--force-premium' to override"
            )

        # Configure core and token contracts
        self.mcd = DssDeployment.from_node(web3=self.web3)
        self.vat = self.mcd.vat
        self.cat = self.mcd.cat
        self.vow = self.mcd.vow
        self.mkr = self.mcd.mkr
        self.dai_join = self.mcd.dai_adapter
        if self.arguments.type == 'flip':
            self.collateral = self.mcd.collaterals[self.arguments.ilk]
            self.ilk = self.collateral.ilk
            self.gem_join = self.collateral.adapter
        else:
            self.collateral = None
            self.ilk = None
            self.gem_join = None

        # Configure auction contracts
        self.flipper = self.collateral.flipper if self.arguments.type == 'flip' else None
        self.flapper = self.mcd.flapper if self.arguments.type == 'flap' else None
        self.flopper = self.mcd.flopper if self.arguments.type == 'flop' else None
        self.urn_history = None
        if self.flipper:
            self.min_flip_lot = Wad.from_number(self.arguments.min_flip_lot)
            self.strategy = FlipperStrategy(self.flipper, self.min_flip_lot)
            self.urn_history = UrnHistory(self.web3, self.mcd, self.ilk,
                                          self.arguments.from_block,
                                          self.arguments.vulcanize_endpoint)
        elif self.flapper:
            self.strategy = FlapperStrategy(self.flapper, self.mkr.address)
        elif self.flopper:
            self.strategy = FlopperStrategy(self.flopper)
        else:
            raise RuntimeError("Please specify auction type")

        # Create the collection used to manage auctions relevant to this keeper
        self.auctions = Auctions(
            flipper=self.flipper.address if self.flipper else None,
            flapper=self.flapper.address if self.flapper else None,
            flopper=self.flopper.address if self.flopper else None,
            model_factory=ModelFactory(' '.join(self.arguments.model)))
        self.auctions_lock = threading.Lock()
        self.dead_since = {}
        self.lifecycle = None

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

        # Create gas strategy used for non-bids and bids which do not supply gas price
        self.gas_price = DynamicGasPrice(self.arguments)

        self.vat_dai_target = Wad.from_number(self.arguments.vat_dai_target) if \
            self.arguments.vat_dai_target is not None else None

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

        ###Thrifty Keeper
        self.balance_manager = Balance_Manager(
            self.our_address, self.web3, self.mcd, self.ilk, self.gem_join,
            self.vat_dai_target, self.arguments.max_gem_balance,
            self.arguments.max_gem_sale, self.arguments.gem_eth_ratio,
            self.arguments.profit_margin, self.arguments.tab_discount,
            self.arguments.bid_start_time)

        # Configure account(s) for which we'll deal auctions
        self.deal_all = False
        self.deal_for = set()
        if self.arguments.deal_for is None:
            self.deal_for.add(self.our_address)
        elif len(
                self.arguments.deal_for
        ) == 1 and self.arguments.deal_for[0].upper() in ["ALL", "NONE"]:
            if self.arguments.deal_for[0].upper() == "ALL":
                self.deal_all = True
            # else no auctions will be dealt
        elif len(self.arguments.deal_for) > 0:
            for account in self.arguments.deal_for:
                self.deal_for.add(Address(account))

        # reduce logspew
        logging.getLogger('urllib3').setLevel(logging.INFO)
        logging.getLogger("web3").setLevel(logging.INFO)
        logging.getLogger("asyncio").setLevel(logging.INFO)
        logging.getLogger("requests").setLevel(logging.INFO)
Exemple #6
0
class AuctionKeeper:
    logger = logging.getLogger()
    dead_after = 10  # Assume block reorgs cannot resurrect an auction id after this many blocks

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='auction-keeper')

        parser.add_argument(
            "--rpc-host",
            type=str,
            default="http://*****:*****@ {bid_price} {cost}")

        if cost is not None:
            if not self.check_bid_cost(cost):
                return

        if bid_price is not None and bid_transact is not None:
            # Ensure this auction has a gas strategy assigned
            (new_gas_strategy,
             fixed_gas_price_changed) = auction.determine_gas_strategy_for_bid(
                 output, self.gas_price)

            # if no transaction in progress, send a new one
            transaction_in_progress = auction.transaction_in_progress()
            logging.info(f"should be bidding")
            # if transaction has not been submitted...
            if transaction_in_progress is None:
                self.logger.info(
                    f"Sending new bid @{managed_price} (gas_price={output.gas_price})"
                )

                auction.price = bid_price
                auction.gas_price = new_gas_strategy if new_gas_strategy else auction.gas_price
                auction.register_transaction(bid_transact)

                # ...submit a new transaction and wait the delay period (if so configured)
                self._run_future(
                    bid_transact.transact_async(gas_price=auction.gas_price))
                if self.arguments.bid_delay:
                    logging.debug(f"Waiting {self.arguments.bid_delay}s")
                    time.sleep(self.arguments.bid_delay)

            # if transaction in progress and the bid price changed...
            elif bid_price != auction.price:
                self.logger.info(
                    f"Attempting to override pending bid with new bid @{managed_price} for auction {id}"
                )
                auction.price = bid_price
                if new_gas_strategy:  # gas strategy changed
                    auction.gas_price = new_gas_strategy
                elif fixed_gas_price_changed:  # gas price updated
                    assert isinstance(auction.gas_price, UpdatableGasPrice)
                    auction.gas_price.update_gas_price(output.gas_price)
                auction.register_transaction(bid_transact)

                # ...ask pymaker to replace the transaction
                self._run_future(
                    bid_transact.transact_async(
                        replace=transaction_in_progress,
                        gas_price=auction.gas_price))

            # if model has been providing a gas price, and only that changed...
            elif fixed_gas_price_changed:
                assert isinstance(auction.gas_price, UpdatableGasPrice)
                self.logger.info(
                    f"Overriding pending bid with new gas_price ({output.gas_price}) for auction {id}"
                )
                auction.gas_price.update_gas_price(output.gas_price)

            # if transaction in progress, bid price unchanged, but gas strategy changed...
            elif new_gas_strategy:
                self.logger.info(
                    f"Changing gas strategy for pending bid @{managed_price} for auction {id}"
                )
                auction.price = bid_price
                auction.gas_price = new_gas_strategy
                auction.register_transaction(bid_transact)

                # ...ask pymaker to replace the transaction
                self._run_future(
                    bid_transact.transact_async(
                        replace=transaction_in_progress,
                        gas_price=auction.gas_price))

    def check_bid_cost(self, cost: Rad) -> bool:
        assert isinstance(cost, Rad)
        # If this is an auction where we bid with Dai...
        if self.flipper or self.flopper:
            vat_dai = self.vat.dai(self.our_address)
            if cost > vat_dai:
                self.logger.info(
                    f"Bid cost {str(cost)} exceeds vat balance of {vat_dai}; "
                    "bid will not be submitted")
                return False
        # If this is an auction where we bid with MKR...
        elif self.flapper:
            mkr_balance = self.mkr.balance_of(self.our_address)
            if cost > Rad(mkr_balance):
                self.logger.debug(
                    f"Bid cost {str(cost)} exceeds MKR balance of {mkr_balance}; "
                    "bid will not be submitted")
                return False
        return True

    def rebalance_dai(self):
        logging.info(
            f"Checking if internal Dai balance needs to be rebalanced")
        if self.vat_dai_target is None or not self.dai_join or (
                not self.flipper and not self.flopper):
            return

        dai = self.dai_join.dai()
        token_balance = dai.balance_of(self.our_address)  # Wad
        difference = Wad(self.vat.dai(
            self.our_address)) - self.vat_dai_target  # Wad
        if difference < Wad(0):
            # Join tokens to the vat
            if token_balance >= difference * -1:
                self.logger.info(
                    f"Joining {str(difference * -1)} Dai to the Vat")
                assert self.dai_join.join(
                    self.our_address,
                    difference * -1).transact(gas_price=self.gas_price)
            elif token_balance > Wad(0):
                self.logger.warning(
                    f"Insufficient balance to maintain Dai target; joining {str(token_balance)} "
                    "Dai to the Vat")
                assert self.dai_join.join(
                    self.our_address,
                    token_balance).transact(gas_price=self.gas_price)
            else:
                self.logger.warning(
                    "No Dai is available to join to Vat; cannot maintain Dai target"
                )
        elif difference > Wad(0):
            # Exit dai from the vat
            self.logger.info(f"Exiting {str(difference)} Dai from the Vat")
            assert self.dai_join.exit(
                self.our_address,
                difference).transact(gas_price=self.gas_price)

        self.logger.info(
            f"Dai token balance: {str(dai.balance_of(self.our_address))}, "
            f"Vat balance: {self.vat.dai(self.our_address)}")

    @staticmethod
    def _run_future(future):
        def worker():
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            try:
                asyncio.get_event_loop().run_until_complete(future)
            finally:
                loop.close()

        thread = threading.Thread(target=worker, daemon=True)
        thread.start()
Exemple #7
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser()
        self.add_arguments(parser=parser)
        self.arguments = parser.parse_args(args)

        # Configure connection to the chain
        provider = HTTPProvider(
            endpoint_uri=self.arguments.rpc_host,
            request_kwargs={'timeout': self.arguments.rpc_timeout})

        self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            provider)
        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        # Check configuration for retrieving urns/bites
        if self.arguments.type == 'flip' and self.arguments.create_auctions \
                and self.arguments.from_block is None and self.arguments.vulcanize_endpoint is None:
            raise RuntimeError(
                "Either --from-block or --vulcanize-endpoint must be specified to kick off "
                "flip auctions")
        if self.arguments.type == 'flip' and not self.arguments.ilk:
            raise RuntimeError(
                "--ilk must be supplied when configuring a flip keeper")
        if self.arguments.type == 'flop' and self.arguments.create_auctions \
                and self.arguments.from_block is None:
            raise RuntimeError(
                "--from-block must be specified to kick off flop auctions")

        self.addresses_path = kwargs[
            "addresses_path"] if "addresses-path" in kwargs else self.arguments.addresses_path

        # Configure core and token contracts
        if self.addresses_path is not None:
            self.mcd = DssDeployment.from_json(web3=self.web3,
                                               conf=open(
                                                   self.addresses_path,
                                                   "r").read())
        else:
            self.mcd = DssDeployment.from_node(web3=self.web3)
        self.vat = self.mcd.vat
        self.cat = self.mcd.cat
        self.vow = self.mcd.vow
        self.mkr = self.mcd.mkr
        self.dai_join = self.mcd.dai_adapter
        if self.arguments.type == 'flip':
            self.collateral = self.mcd.collaterals[self.arguments.ilk]
            self.ilk = self.collateral.ilk
            self.gem_join = self.collateral.adapter
        else:
            self.collateral = None
            self.ilk = None
            self.gem_join = None

        # Configure auction contracts
        self.flipper = self.collateral.flipper if self.arguments.type == 'flip' else None
        self.flapper = self.mcd.flapper if self.arguments.type == 'flap' else None
        self.flopper = self.mcd.flopper if self.arguments.type == 'flop' else None
        self.urn_history = None
        if self.flipper:
            self.min_flip_lot = Wad.from_number(self.arguments.min_flip_lot)
            self.strategy = FlipperStrategy(self.flipper, self.min_flip_lot)
            self.urn_history = UrnHistory(self.web3, self.mcd, self.ilk,
                                          self.arguments.from_block,
                                          self.arguments.vulcanize_endpoint)
        elif self.flapper:
            self.strategy = FlapperStrategy(self.flapper, self.mkr.address)
        elif self.flopper:
            self.strategy = FlopperStrategy(self.flopper)
        else:
            raise RuntimeError("Please specify auction type")

        # Create the collection used to manage auctions relevant to this keeper
        self.auctions = Auctions(
            flipper=self.flipper.address if self.flipper else None,
            flapper=self.flapper.address if self.flapper else None,
            flopper=self.flopper.address if self.flopper else None,
            model_factory=ModelFactory(' '.join(self.arguments.model)))
        self.auctions_lock = threading.Lock()
        self.dead_auctions = set()
        self.lifecycle = None

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

        # Create gas strategy used for non-bids and bids which do not supply gas price
        self.gas_price = DynamicGasPrice(self.arguments)

        self.vat_dai_target = Wad.from_number(self.arguments.vat_dai_target) if \
            self.arguments.vat_dai_target is not None else None

        # Configure account(s) for which we'll deal auctions
        self.deal_all = False
        self.deal_for = set()
        if self.arguments.deal_for is None:
            self.deal_for.add(self.our_address)
        elif len(
                self.arguments.deal_for
        ) == 1 and self.arguments.deal_for[0].upper() in ["ALL", "NONE"]:
            if self.arguments.deal_for[0].upper() == "ALL":
                self.deal_all = True
            # else no auctions will be dealt
        elif len(self.arguments.deal_for) > 0:
            for account in self.arguments.deal_for:
                self.deal_for.add(Address(account))

        # reduce logspew
        logging.getLogger('urllib3').setLevel(logging.INFO)
        logging.getLogger("web3").setLevel(logging.INFO)
        logging.getLogger("asyncio").setLevel(logging.INFO)
        logging.getLogger("requests").setLevel(logging.INFO)
Exemple #8
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='auction-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(
            '--network',
            type=str,
            required=True,
            help="Ethereum network to connect (e.g. 'kovan' or 'testnet')")
        parser.add_argument('--type',
                            type=str,
                            choices=['flip', 'flap', 'flop'],
                            help="Auction type in which to participate")
        parser.add_argument(
            '--ilk',
            type=str,
            help=
            "Name of the collateral type for a flip keeper (e.g. 'ETH-B', 'ZRX-A')"
        )
        parser.add_argument(
            '--bid-only',
            dest='create_auctions',
            action='store_false',
            help="Do not take opportunities to create new auctions")

        parser.add_argument(
            '--vat-dai-target',
            type=float,
            help="Amount of Dai to keep in the Vat contract (e.g. 2000)")
        parser.add_argument(
            '--keep-dai-in-vat-on-exit',
            dest='exit_dai_on_shutdown',
            action='store_false',
            help=
            "Retain Dai in the Vat on exit, saving gas when restarting the keeper"
        )
        parser.add_argument('--keep-gem-in-vat-on-exit',
                            dest='exit_gem_on_shutdown',
                            action='store_false',
                            help="Retain collateral in the Vat on exit")

        parser.add_argument(
            "--model",
            type=str,
            required=True,
            help="Commandline to use in order to start the bidding model")

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

        self.arguments = parser.parse_args(args)

        # Configure connection to the chain
        if self.arguments.rpc_host.startswith("http"):
            endpoint_uri = f"{self.arguments.rpc_host}:{self.arguments.rpc_port}"
        else:
            # Should probably default this to use TLS, but I don't want to break existing configs
            endpoint_uri = f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}"
        self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=endpoint_uri,
                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)

        # Configure core and token contracts
        if self.arguments.type == 'flip' and not self.arguments.ilk:
            raise RuntimeError(
                "--ilk must be supplied when configuring a flip keeper")
        mcd = DssDeployment.from_network(web3=self.web3,
                                         network=self.arguments.network)
        self.vat = mcd.vat
        self.cat = mcd.cat
        self.vow = mcd.vow
        self.mkr = mcd.mkr
        self.dai_join = mcd.dai_adapter
        if self.arguments.type == 'flip':
            self.collateral = mcd.collaterals[self.arguments.ilk]
            self.ilk = self.collateral.ilk
            self.gem_join = self.collateral.adapter
        else:
            self.collateral = None
            self.ilk = None
            self.gem_join = None

        # Configure auction contracts
        self.flipper = self.collateral.flipper if self.arguments.type == 'flip' else None
        self.flapper = mcd.flapper if self.arguments.type == 'flap' else None
        self.flopper = mcd.flopper if self.arguments.type == 'flop' else None
        if self.flipper:
            self.strategy = FlipperStrategy(self.flipper)
        elif self.flapper:
            self.strategy = FlapperStrategy(self.flapper, self.mkr.address)
        elif self.flopper:
            self.strategy = FlopperStrategy(self.flopper)

        # Create the collection used to manage auctions relevant to this keeper
        self.auctions = Auctions(
            flipper=self.flipper.address if self.flipper else None,
            flapper=self.flapper.address if self.flapper else None,
            flopper=self.flopper.address if self.flopper else None,
            model_factory=ModelFactory(self.arguments.model))
        self.auctions_lock = threading.Lock()

        self.vat_dai_target = Wad.from_number(self.arguments.vat_dai_target) if \
            self.arguments.vat_dai_target is not None else None

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