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
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()
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)
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
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)