示例#1
0
parser.add_argument("-w", "--warnratio", help="minimum ratio in percent that gives a warning",
                    type=int, default="180")
parser.add_argument("-q", "--quiet", help="only output warnings",
                    action='store_true')
parser.add_argument('cdps', metavar='CDP', type=int, nargs='+',
                    help='CDP id(s) to check')
args = parser.parse_args()

web3 = Web3(HTTPProvider(endpoint_uri="https://mainnet.infura.io/metamask"))

tub = Tub(web3=web3, address=Address('0x448a5065aebb8e423f0896e6c5d525c040f59af3'))
minimum_ratio=Ray.from_number(args.warnratio / 100)
requirements_satisfied=True

for cup_id in args.cdps:
    cup = tub.cups(cup_id)
    pro = tub.ink(cup_id)*tub.tag()
    tab = tub.tab(cup_id)
    if not args.quiet:
        print(f'CDP #{cup_id}')
        print(f'  Owner {cup.lad}')
        print(f'  Deposited {float(cup.ink):.8} PETH')
        print(f'  Debt {float(tab):.8} DAI')
    if tab > Wad(0):
        current_ratio = Ray(pro / tab)
        if not args.quiet:
            print(f'  Current Ratio {float(current_ratio):.2%}')
        is_undercollateralized = (current_ratio < minimum_ratio)
        if is_undercollateralized:
            print(f'CDP #{cup_id} is {float(current_ratio):.2%} which is less than {float(minimum_ratio):.2%}')
            requirements_satisfied=False
示例#2
0
class CdpKeeper:
    """Keeper to actively manage open CDPs."""

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

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='cdp-keeper')
        parser.add_argument("--rpc-host",
                            help="JSON-RPC host (default: `localhost')",
                            default="localhost",
                            type=str)
        parser.add_argument("--rpc-port",
                            help="JSON-RPC port (default: `8545')",
                            default=8545,
                            type=int)
        parser.add_argument("--rpc-timeout",
                            help="JSON-RPC timeout (in seconds, default: 10)",
                            default=10,
                            type=int)
        parser.add_argument(
            "--eth-from",
            help="Ethereum account from which to send transactions",
            required=True,
            type=str)
        parser.add_argument("--tub-address",
                            help="Ethereum address of the Tub contract",
                            required=True,
                            type=str)
        parser.add_argument(
            "--min-margin",
            help=
            "Margin between the liquidation ratio and the top-up threshold",
            type=float,
            required=True)
        parser.add_argument(
            "--top-up-margin",
            help="Margin between the liquidation ratio and the top-up target",
            type=float,
            required=True)
        parser.add_argument("--max-sai", type=float, required=True)
        parser.add_argument("--avg-sai", type=float, required=True)
        parser.add_argument("--gas-price",
                            help="Gas price in Wei (default: node default)",
                            default=0,
                            type=int)
        parser.add_argument("--debug",
                            help="Enable debug output",
                            dest='debug',
                            action='store_true')
        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
        self.our_address = Address(self.arguments.eth_from)
        self.tub = Tub(web3=self.web3,
                       address=Address(self.arguments.tub_address))
        self.sai = ERC20Token(web3=self.web3, address=self.tub.sai())

        self.liquidation_ratio = self.tub.mat()
        self.minimum_ratio = self.liquidation_ratio + Ray.from_number(
            self.arguments.min_margin)
        self.target_ratio = self.liquidation_ratio + Ray.from_number(
            self.arguments.top_up_margin)
        self.max_sai = Wad.from_number(self.arguments.max_sai)
        self.avg_sai = Wad.from_number(self.arguments.avg_sai)

        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:
            lifecycle.on_startup(self.startup)
            lifecycle.on_block(self.check_all_cups)

    def startup(self):
        self.approve()

    def approve(self):
        self.tub.approve(directly(gas_price=self.gas_price()))

    def check_all_cups(self):
        for cup in self.our_cups():
            self.check_cup(cup.cup_id)

    def check_cup(self, cup_id: int):
        assert (isinstance(cup_id, int))

        # If cup is undercollateralized and the amount of SAI we are holding is more than `--max-sai`
        # then we wipe some debt first so our balance reaches `--avg-sai`. Bear in mind that it is
        # possible that we pay all out debt this way and our SAI balance will still be higher
        # than `--max-sai`.
        if self.is_undercollateralized(cup_id) and self.sai.balance_of(
                self.our_address) > self.max_sai:
            amount_of_sai_to_wipe = self.calculate_sai_wipe()
            if amount_of_sai_to_wipe > Wad(0):
                self.tub.wipe(
                    cup_id,
                    amount_of_sai_to_wipe).transact(gas_price=self.gas_price())

        # If cup is still undercollateralized, calculate the amount of SKR needed to top it up so
        # the collateralization level reaches `--top-up-margin`. If we have enough ETH, exchange
        # in to SKR and then top-up the cup.
        if self.is_undercollateralized(cup_id):
            top_up_amount = self.calculate_skr_top_up(cup_id)
            if top_up_amount <= eth_balance(self.web3, self.our_address):
                # TODO we do not always join with the same amount as the one we lock!
                self.tub.join(top_up_amount).transact(
                    gas_price=self.gas_price())
                self.tub.lock(
                    cup_id, top_up_amount).transact(gas_price=self.gas_price())
            else:
                self.logger.info(
                    f"Cannot top-up as our balance is less than {top_up_amount} ETH."
                )

    def our_cups(self):
        for cup_id in range(1, self.tub.cupi() + 1):
            cup = self.tub.cups(cup_id)
            if cup.lad == self.our_address:
                yield cup

    def is_undercollateralized(self, cup_id) -> bool:
        pro = self.tub.ink(cup_id) * self.tub.tag()
        tab = self.tub.tab(cup_id)
        if tab > Wad(0):
            current_ratio = Ray(pro / tab)
            # Prints the Current CDP Ratio and the Minimum Ratio specified under --min-margin
            print(f'Current Ratio {current_ratio}')
            print(f'Minimum Ratio {self.minimum_ratio}')
            return current_ratio < self.minimum_ratio
        else:
            return False

    def calculate_sai_wipe(self) -> Wad:
        """Calculates the amount of SAI that can be wiped.

        Calculates the amount of SAI than can be wiped in order to bring the SAI holdings
        to `--avg-sai`.
        """
        return Wad.max(
            self.sai.balance_of(self.our_address) - self.avg_sai, Wad(0))

    def calculate_skr_top_up(self, cup_id) -> Wad:
        """Calculates the required top-up in SKR.

        Calculates the required top-up in SKR in order to bring the collateralization level
        of the cup to `--target-ratio`.
        """
        pro = self.tub.ink(cup_id) * self.tub.tag()
        tab = self.tub.tab(cup_id)
        if tab > Wad(0):
            current_ratio = Ray(pro / tab)
            return Wad.max(
                tab * (Wad(self.target_ratio - current_ratio) /
                       Wad.from_number(self.tub.tag())), Wad(0))
        else:
            return Wad(0)

    def gas_price(self):
        if self.arguments.gas_price > 0:
            return FixedGasPrice(self.arguments.gas_price)
        else:
            return DefaultGasPrice()
示例#3
0
class BiteKeeper:
    """Keeper to bite undercollateralized cups."""

    logger = logging.getLogger('bite-all-keeper')

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='bite-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",
            help="JSON-RPC timeout (in seconds, default: 10)",
            default=10,
            type=int
        )
        parser.add_argument(
            "--eth-from",
            help="Ethereum account from which to send transactions",
            required=True,
            type=str
        )
        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help="Ethereum private key(s) to use " +
                "(e.g. 'key_file=/path/to/keystore.json," +
                "pass_file=/path/to/passphrase.txt')"
        )
        parser.add_argument(
            "--tub-address",
            help="Ethereum address of the Tub contract",
            required=True,
            type=str
        )
        parser.add_argument(
            "--graphql-url",
            type=str,
            default="https://sai-mainnet.makerfoundation.com/v1",
            help="GraphQL URL " +
                "(default: `https://sai-mainnet.makerfoundation.com/v1')"
        )
        parser.add_argument(
            "--bitecdps-address",
            help="Ethereum address of the BiteCdps contract",
            type=str
        )
        parser.add_argument(
            "--top",
            help="Quickly process N top bites, (default: 500)",
            default=500,
            type=int
        )
        parser.add_argument(
            "--chunks",
            help="Process top bites in chunks of N, (default: 100)",
            default=100,
            type=int
        )
        parser.add_argument(
            "--debug",
            help="Enable debug output",
            dest='debug',
            action='store_true'
        )
        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
        self.our_address = Address(self.arguments.eth_from)
        register_keys(self.web3, self.arguments.eth_key)
        self.tub = Tub(
            web3=self.web3, address=Address(self.arguments.tub_address)
        )
        self.vox = Vox(web3=self.web3, address=self.tub.vox())
        self.top = self.arguments.top
        self.chunks = self.arguments.chunks

        if self.arguments.bitecdps_address and self.arguments.graphql_url:
            self.use_bitecdps = True
            self.bitecdps = BiteCdps(
                web3=self.web3, address=Address(self.arguments.bitecdps_address)
            )
            self.graphql_url = self.arguments.graphql_url
        else:
            self.use_bitecdps = False

        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_block(self.check_all_cups)

    def check_all_cups(self):
        if self.tub._contract.functions.off().call():
            self.logger.info('Single Collateral Dai has been Caged')
            self.logger.info('Starting to bite all cups in the tub contract')

            # Read some things that wont change across cups
            axe = self.tub.axe() # Liquidation penalty [RAY] Fixed at 1 RAY at cage
            par = self.vox.par() # Dai Targe Price     [RAY] Typically 1 RAY
            tag = self.tub.tag() # Ref/Oracle price    [RAY] Fixed at shutdown

            if self.use_bitecdps:
                self.call_bitecdps()
            else:
                for cup_id in range(self.tub.cupi()):
                    self.check_cup(cup_id+1, axe, par, tag)

            self.lifecycle.terminate()
        else:
            self.logger.info('Single Collateral Dai live')

    def check_cup(self, cup_id, axe: Ray, par: Ray, tag: Ray):
        cup = self.tub.cups(cup_id)
        rue = Ray(self.tub.tab(cup_id)) # Amount of Debt[RAY]

        # Amount owed in SKR, including liquidation penalty
        # var owe = rdiv(rmul(rmul(rue, axe), vox.par()), tag());
        owe = ((rue * axe) * par) / tag

        # Bite cups with owe over a threshold that haven't been bitten before
        if owe > Ray.from_number(0) and cup.art != Wad.from_number(0):
            self.logger.info(
                f'Bite cup {cup_id} with owe of {owe} and ink of {cup.ink}'
            )
            self.tub.bite(cup_id).transact(gas_price=self.gas_price())

    def call_bitecdps(self):
        self.logger.info(f'Will bite top {self.top} CDPs')
        # cdps = [i+1 for i in range(self.tub.cupi())]
        cdps = self.get_cdps()
        self.logger.info(f'found {len(cdps)} CDPs')
        for i in range(0, len(cdps), self.chunks):
            chunk = cdps[i:i+self.chunks]
            self.logger.info(f'BiteCdps.bite({chunk})')
            self.bitecdps.bite(chunk).transact(gas_price=self.gas_price())

    def get_cdps(self):
        client = GraphQLClient(self.graphql_url)
        result = client.execute(QUERY.replace('XXX', f'{self.top}'))
        data = json.loads(result)
        cdps = []
        for cdp in data["data"]["allCups"]["nodes"]:
            cdps.append(cdp["id"])
        return cdps

    def gas_price(self):
        """ IncreasingGasPrice """
        GWEI = 1000000000

        return IncreasingGasPrice(initial_price=5*GWEI,
                                  increase_by=10*GWEI,
                                  every_secs=60,
                                  max_price=300*GWEI)
示例#4
0
class DAIv1(Market):

    def __init__(self, web3, dai_tub = '0x448a5065aeBB8E423F0896E6c5D525C040f59af3'):
        self.web3 = web3
        self.tub = Tub(web3=web3, address=Address(dai_tub))
        self.tap = Tap(web3=web3, address=self.tub.tap())
        self.tokens = {
                'MKR': ERC20Token(web3, self.tub.gov()),
                'PETH': ERC20Token(web3, self.tub.skr()),
                'WETH': ERC20Token(web3, self.tub.gem()),
                'DAI': ERC20Token(web3, self.tub.sai()),
        }

    def get_cup(self, cup_id):
        cup = self.tub.cups(cup_id)
        return { 'id': cup.cup_id,
                'lad': cup.lad.address,
                'art': float(cup.art),
                'ink': float(cup.ink),
                'safe': self.tub.safe(cup_id)
               }

    def get_cups(self):
        last_cup_id = self.tub.cupi()
        cups = map(self.get_cup, range(1, last_cup_id+1))
        not_empty_cups = filter(lambda cup: cup['lad'] != "0x0000000000000000000000000000000000000000", cups)
        return list(not_empty_cups)

    def get_pairs(self):
        pairs = ['PETH/DAI', 'PETH/WETH']
        return pairs

    def get_orders(self, base, quote):
        depth = {'bids': [], 'asks': []}

        # PETH/DAI order book
        if base == 'PETH' and quote == 'DAI':
            # boom is a taker using a bid side from maker tap
            # a taker convert PETH to DAI using tap.bid(1) as price
            # maker side offer DAI in exchange for PETH (flap)
            # DAI qty offered by is min(joy - woe, 0)
            order = { 'price': float(self.tap.bid(Wad.from_number(1))),
                      'amount': float(min(self.tap.joy() - self.tap.woe(), Wad.from_number(0))),
                      'id': 'take:tap.boom()',
                    }
            if order['amount'] > 0:
                depth['bids'].append(order)

            # bust is a taker using ask side from maker tap
            # a taker convert DAI to PETH using tap.ask(1) as price
            # maker side offer PETH from fog (flip) and PETH minted to cover woe (flop)
            # PETH qty offered by maker is fog+min(woe-joy, 0)/ask
            order = { 'price': float(self.tap.ask(Wad.from_number(1))),
                      'amount': float(self.tap.fog() + min(self.tap.woe() - self.tap.joy(), Wad.from_number(0)) / self.tap.ask(Wad.from_number(1))),
                      'id': 'take:tap.bust()',
                    }
            if order['amount'] > 0:
                depth['asks'].append(order)

        # PETH/WETH order book
        if base == 'PETH' and quote == 'WETH':
            # exit is a taker using a bid side from maker tub
            # a taker PETH to WETH using tub.bid(1) as price
            # maker side offer WETH in exchange for PETH
            # WETH qty offered by maker is infinity (2**32 as a large number for infinity ...)
            order = { 'price': float(self.tub.bid(Wad.from_number(1))),
                      'amount': float(2**32),
                      'id': 'take:tub.exit()',
                    }
            depth['bids'].append(order)

            # join is a taker using ask side from maker tub
            # a taker convert WETH to PETH usgin tub.ask(1) as price
            # maker side offer PETH in exchange for WETH
            # PETH qty offered by maker is infinity (2**32 as a large number for infinity ...)
            order = { 'price': float(self.tub.ask(Wad.from_number(1))),
                      'amount': float(2**32),
                      'id': 'take:tub.join()',
                    }
            depth['asks'].append(order)

        return depth

    def get_accounts(self, manager_url):
        accounts = {}
        for addr in requests.get(manager_url).json():
            accounts[addr] = {
                'balance' : self.web3.eth.getBalance(addr),
            }
            accounts[addr]['tokens'] = {}
            for name, token in self.tokens.items():
                balance = token.balance_of(Address(addr))
                allowance = token.allowance_of(Address(addr), self.tub.address)
                #TODO check tap allowance ...
                if float(allowance) or float(balance):
                    accounts[addr]['tokens'][name] = {
                            'allowance': float(allowance),
                            'balance': float(balance),
                    }

        return accounts
示例#5
0
class BiteKeeper:
    """Keeper to bite undercollateralized cups."""

    logger = logging.getLogger('bite-all-keeper')

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='bite-keeper')
        parser.add_argument("--rpc-host",
                            help="JSON-RPC host (default: `localhost')",
                            default="localhost",
                            type=str)
        parser.add_argument("--rpc-port",
                            help="JSON-RPC port (default: `8545')",
                            default=8545,
                            type=int)
        parser.add_argument("--rpc-timeout",
                            help="JSON-RPC timeout (in seconds, default: 10)",
                            default=10,
                            type=int)
        parser.add_argument(
            "--eth-from",
            help="Ethereum account from which to send transactions",
            required=True,
            type=str)
        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=/path/to/keystore.json,pass_file=/path/to/passphrase.txt')"
        )
        parser.add_argument("--tub-address",
                            help="Ethereum address of the Tub contract",
                            required=True,
                            type=str)
        parser.add_argument("--debug",
                            help="Enable debug output",
                            dest='debug',
                            action='store_true')
        self.arguments = parser.parse_args(args)

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

        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:
            lifecycle.on_block(self.check_all_cups)

    def check_all_cups(self):
        if self.tub._contract.functions.off().call():
            self.logger.info('Single Collateral Dai has been Caged')
            self.logger.info('Starting to bite all cups in the tub contract')

            # Read some things that wont change across cups
            axe = self.tub.axe(
            )  # Liquidation penalty [RAY] Fixed at 1 RAY at cage
            par = self.vox.par()  # Dai Targe Price     [RAY] Typically 1 RAY
            tag = self.tub.tag()  # Ref/Oracle price    [RAY] Fixed at shutdown

            for cup_id in range(self.tub.cupi()):
                self.check_cup(cup_id + 1, axe, par, tag)
        else:
            self.logger.info('Single Collateral Dai live')

    def check_cup(self, cup_id, axe: Ray, par: Ray, tag: Ray):
        cup = self.tub.cups(cup_id)
        rue = Ray(self.tub.tab(cup_id))  # Amount of Debt[RAY]

        # Amount owed in SKR, including liquidation penalty
        # var owe = rdiv(rmul(rmul(rue, axe), vox.par()), tag());
        owe = ((rue * axe) * par) / tag

        # Bite cups with owe over a threshold that haven't been bitten before
        if owe > Ray.from_number(0) and cup.art != Wad.from_number(0):
            self.logger.info(
                f'Bite cup {cup_id} with owe of {owe} and ink of {cup.ink}')
            self.tub.bite(cup_id).transact(gas_price=self.gas_price())

    def gas_price(self):
        """ IncreasingGasPrice """
        GWEI = 1000000000

        return IncreasingGasPrice(initial_price=5 * GWEI,
                                  increase_by=10 * GWEI,
                                  every_secs=60,
                                  max_price=300 * GWEI)