diff --git a/README.md b/README.md index f839e3c..1d8a197 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ To be consistent with the Protocol's technical terminology for the rest of this The central goal of the `cage-keeper` is to process all under-collateralized `urns`. This accounting step is performed within `End.skim()`, and since it is surrounded by other required/important steps in the Emergency Shutdown, a first iteration of this keeper will help to call most of the other public function calls within the `End` contract. -As can be seen in the above flowchart, the keeper checks if the system has been caged before attempting to `skim` all underwater urns and `skip` all flip auctions. After the processing period has been facilitated and the `End.wait` waittime has been reached, it will transition the system into the Dai redemption phase of Emergency Shutdown by calling `End.thaw()` and `End.flow()`. This first iteration of this keeper is naive, as it assumes it's the only keeper and attempts to account for all urns, ilks, and auctions. Because of this, it's important that the keeper's address has enough ETH to cover the gas costs involved with sending numerous transactions. Any transaction that attempts to call a function that's already been invoked by another Keeper/user would simply fail. +As can be seen in the above flowchart, the keeper checks if the system has been caged before attempting to `skim` all underwater urns, `snip` all clip auctions, and `skip` all flip auctions. After the processing period has been facilitated and the `End.wait` waittime has been reached, it will transition the system into the Dai redemption phase of Emergency Shutdown by calling `End.thaw()` and `End.flow()`. This first iteration of this keeper is naive, as it assumes it's the only keeper and attempts to account for all urns, ilks, and auctions. Because of this, it's important that the keeper's address has enough ETH to cover the gas costs involved with sending numerous transactions. Any transaction that attempts to call a function that's already been invoked by another Keeper/user would simply fail. ## Operation @@ -39,6 +39,7 @@ The keeper's ethereum address should have enough ETH to cover gas costs and is a min_ETH = average_gasPrice * [ ( Flopper.yank()_gas * #_of_Flop_Auctions ) + ( Flapper.yank()_gas * #_of_Flap_Auctions ) + ( End.cage(Ilk)_gas * #_of_Collateral_Types ) + + ( End.snip()_gas * #_of_Clip_Auctions ) + ( End.skip()_gas * #_of_Flip_Auctions ) + ( End.skim()_gas * #_of_Underwater_Vaults ) + ( Vow.heal()_gas ) + @@ -95,6 +96,9 @@ Make a run-cage-keeper.sh to easily spin up the cage-keeper. [--vulcanize-endpoint 'http://vdb.sampleendpoint.com:8545/graphql'] ``` +To `flow` the PSM along with other collaterals, pass the `--psm` address. Current addresses may be obtained from +https://github.com/BellwoodStudios/dss-psm . + ## Testing diff --git a/lib/auction-keeper b/lib/auction-keeper index a524c78..52b3f93 160000 --- a/lib/auction-keeper +++ b/lib/auction-keeper @@ -1 +1 @@ -Subproject commit a524c785137c445f44916972db1c3f1a4e1902b5 +Subproject commit 52b3f93bef862f327b4d7450aeda56cd5c4a23e5 diff --git a/lib/pygasprice-client b/lib/pygasprice-client index a96d759..f75c23d 160000 --- a/lib/pygasprice-client +++ b/lib/pygasprice-client @@ -1 +1 @@ -Subproject commit a96d759cf1c6f51071def2fd262742f2d6cde37a +Subproject commit f75c23dc271b2fb4ada5e9d423c0315339de8658 diff --git a/lib/pymaker b/lib/pymaker index 36cf321..e62645a 160000 --- a/lib/pymaker +++ b/lib/pymaker @@ -1 +1 @@ -Subproject commit 36cf3210c4ff3ce469ab6ed62cead03aa85be5d6 +Subproject commit e62645a4771b7d615c306c1051967b63bf827eba diff --git a/operation.png b/operation.png index c2abcaf..aff3d96 100644 Binary files a/operation.png and b/operation.png differ diff --git a/src/cage_keeper.py b/src/cage_keeper.py index fc36a0f..7130e8e 100644 --- a/src/cage_keeper.py +++ b/src/cage_keeper.py @@ -1,6 +1,6 @@ # This file is part of the Maker Keeper Framework. # -# Copyright (C) 2019 EdNoepel, KentonPrescott +# Copyright (C) 2019-2021 EdNoepel, KentonPrescott # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -18,27 +18,25 @@ import argparse import logging import sys -import time from datetime import datetime, timezone -import types -from os import path from typing import List from web3 import Web3 from pymaker import Address, web3_via_http -from pymaker.gas import DefaultGasPrice, FixedGasPrice -from pymaker.auctions import Flipper, Flapper, Flopper +from pymaker.auctions import Clipper, Flipper +from pymaker.deployment import Collateral, DssDeployment +from pymaker.dss import Ilk, Urn +from pymaker.gas import DefaultGasPrice from pymaker.keys import register_keys from pymaker.lifecycle import Lifecycle from pymaker.numeric import Wad, Rad, Ray -from pymaker.token import ERC20Token -from pymaker.deployment import DssDeployment -from pymaker.dss import Ilk, Urn -from auction_keeper.urn_history import UrnHistory +from auction_keeper.urn_history import ChainUrnHistoryProvider +from auction_keeper.urn_history_vulcanize import VulcanizeUrnHistoryProvider from auction_keeper.gas import DynamicGasPrice + class CageKeeper: """Keeper to facilitate Emergency Shutdown""" @@ -52,14 +50,11 @@ def __init__(self, args: list, **kwargs): parser.add_argument("--rpc-host", type=str, default="https://localhost:8545", help="JSON-RPC host:port (default: 'localhost:8545')") - parser.add_argument("--rpc-timeout", type=int, default=1200, - help="JSON-RPC timeout (in seconds, default: 10)") - - parser.add_argument("--network", type=str, required=True, - help="Network that you're running the Keeper on (options, 'mainnet', 'kovan', 'testnet')") + parser.add_argument("--rpc-timeout", type=int, default=60, + help="JSON-RPC timeout (in seconds, default: 60)") parser.add_argument('--previous-cage', dest='cageFacilitated', action='store_true', - help='Include this argument if this keeper previously helped to facilitate the processing phase of ES') + help='Include this argument if this keeper previously started the processing phase of ES') parser.add_argument("--eth-from", type=str, required=True, help="Ethereum address from which to send transactions; checksummed (e.g. '0x12AebC')") @@ -67,22 +62,20 @@ def __init__(self, args: list, **kwargs): 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("--psm", type=str, default="", + help="When provided, PSM will be flowed along with other collaterals") + parser.add_argument("--dss-deployment-file", type=str, required=False, help="Json description of all the system addresses (e.g. /Full/Path/To/configFile.json)") parser.add_argument("--vat-deployment-block", type=int, required=False, default=0, - help=" Block that the Vat from dss-deployment-file was deployed at (e.g. 8836668") + help="Block that the Vat from dss-deployment-file was deployed at (e.g. 8836668") 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 Vault query") - + help="When specified, urn history will be queried from Vulcanize, conserving resources") parser.add_argument("--vulcanize-key", type=str, help="API key for the Vulcanize endpoint") - parser.add_argument("--max-errors", type=int, default=100, - help="Maximum number of allowed errors before the keeper terminates (default: 100)") - parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") @@ -92,8 +85,6 @@ def __init__(self, args: list, **kwargs): parser.add_argument("--gas-reactive-multiplier", type=str, default=2.25, help="gas strategy tuning") parser.add_argument("--gas-maximum", type=str, default=5000, help="gas strategy tuning") - - parser.set_defaults(cageFacilitated=False) self.arguments = parser.parse_args(args) @@ -107,15 +98,11 @@ def __init__(self, args: list, **kwargs): if self.arguments.dss_deployment_file: self.dss = DssDeployment.from_json(web3=self.web3, conf=open(self.arguments.dss_deployment_file, "r").read()) else: - self.dss = DssDeployment.from_network(web3=self.web3, network=self.arguments.network) + self.dss = DssDeployment.from_node(web3=self.web3) self.deployment_block = self.arguments.vat_deployment_block - - self.max_errors = self.arguments.max_errors - self.errors = 0 - + self.complete = False self.cageFacilitated = self.arguments.cageFacilitated - self.confirmations = 0 # Create gas strategy @@ -124,11 +111,10 @@ def __init__(self, args: list, **kwargs): else: self.gas_price = DefaultGasPrice() - + self.lifecycle = None logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s', level=(logging.DEBUG if self.arguments.debug else logging.INFO)) - def main(self): """ Initialize the lifecycle and enter into the Keeper Lifecycle controller @@ -142,7 +128,6 @@ def main(self): lifecycle.on_startup(self.check_deployment) lifecycle.on_block(self.process_block) - def check_deployment(self): self.logger.info('') self.logger.info('Please confirm the deployment details') @@ -155,15 +140,13 @@ def check_deployment(self): self.logger.info(f'End: {self.dss.end.address}') self.logger.info('') - def process_block(self): - """Callback called on each new block. If too many errors, terminate the keeper to minimize potential damage.""" - if self.errors >= self.max_errors: + """Callback called on each new block.""" + if self.complete: self.lifecycle.terminate() else: self.check_cage() - def check_cage(self): """ After live is 0 for 12 block confirmations, facilitate the processing period, then thaw the cage """ blockNumber = self.web3.eth.blockNumber @@ -186,11 +169,18 @@ def check_cage(self): self.facilitate_processing_period() # wait until processing time concludes - elif (now >= thawedCage): + elif now >= thawedCage: self.thaw_cage() - - if not (self.arguments.network == 'testnet'): - self.lifecycle.terminate() + joined_mkr = self.dss.esm.sum() + if joined_mkr > Wad(0): + self.logger.info('') + self.logger.info(f'======== Burning {float(joined_mkr)} deposited MKR ========') + self.logger.info('') + self.dss.esm.burn().transact(gas_price=self.gas_price) + self.logger.info('') + self.logger.info('======== Completed emergency shutdown ========') + self.logger.info('') + self.complete = True else: whenThawedCage = datetime.utcfromtimestamp(thawedCage) @@ -200,17 +190,16 @@ def check_cage(self): elif not live and self.confirmations < 13: self.confirmations = self.confirmations + 1 - self.logger.info(f'======== System has been caged ( {self.confirmations} confirmations) ========') - + self.logger.info(f'======== System has been caged ({self.confirmations} confirmations) ========') def facilitate_processing_period(self): - """ Yank all active flap/flop auctions, cage all ilks, skip all flip auctions, skim all underwater urns """ + """ Yank active flap/flop auctions, cage ilks, snip clip auctions, skip flip auctions, skim underwater urns """ self.logger.info('') self.logger.info('======== Facilitating Cage ========') self.logger.info('') # check ilks - ilks = self.get_ilks() + ilks = list(map(lambda l: l.ilk, self.get_collaterals())) # Get all auctions that can be yanked after cage auctions = self.all_active_auctions() @@ -220,21 +209,76 @@ def facilitate_processing_period(self): # Cage all ilks for ilk in ilks: - self.dss.end.cage(ilk).transact(gas_price=self.gas_price) + assert self.dss.end.cage(ilk).transact(gas_price=self.gas_price) + + # Snip all clip auctions + for key in auctions["clips"].keys(): + ilk = self.dss.vat.ilk(key) + for bid in auctions["clips"][key]: + self.dss.end.snip(ilk, bid.id).transact(gas_price=self.gas_price) # Skip all flip auctions for key in auctions["flips"].keys(): ilk = self.dss.vat.ilk(key) for bid in auctions["flips"][key]: - self.dss.end.skip(ilk,bid.id).transact(gas_price=self.gas_price) + self.dss.end.skip(ilk, bid.id).transact(gas_price=self.gas_price) + + # Cancel vault debt, confiscating backing collateral + self.skim_urns(ilks, undercollateralized_only=True) - #get all underwater urns - urns = self.get_underwater_urns(ilks) + def reconcile_debt(self): + joy = self.dss.vat.dai(self.dss.vow.address) + ash = self.dss.vow.ash() + woe = self.dss.vow.woe() - #skim all underwater urns - for i in urns: - self.dss.end.skim(i.ilk, i.address).transact(gas_price=self.gas_price) + if ash > Rad(0): + if joy > ash: + self.dss.vow.kiss(ash).transact(gas_price=self.gas_price) + else: + self.dss.vow.kiss(joy).transact(gas_price=self.gas_price) + return + if woe > Rad(0): + joy = self.dss.vat.dai(self.dss.vow.address) + if joy > woe: + self.dss.vow.heal(woe).transact(gas_price=self.gas_price) + else: + self.dss.vow.heal(joy).transact(gas_price=self.gas_price) + def skim_urns(self, ilks: List, undercollateralized_only: bool): + """ Skim each urn to cancel debt """ + + for ilk in ilks: + if self.arguments.vulcanize_endpoint: + urn_history = VulcanizeUrnHistoryProvider(self.web3, self.dss, ilk, + self.arguments.vulcanize_endpoint, + self.arguments.vulcanize_key) + else: + urn_history = ChainUrnHistoryProvider(self.web3, self.dss, ilk, self.deployment_block) + urns = urn_history.get_urns() + self.logger.info(f'Collected {len(urns)} urns from {ilk}') + + for count, urn in enumerate(urns.values()): + # ignore vaults with no debt + if urn.art == Wad(0): + continue + # while facilitating the processing period, only underwater vaults will be confiscated + if undercollateralized_only: + mat = self.dss.spotter.mat(urn.ilk) + usd_debt = Ray(urn.art) * urn.ilk.rate + usd_collateral = Ray(urn.ink) * urn.ilk.spot * mat + # Check if underwater -> urn.art * ilk.rate > urn.ink * ilk.spot * spotter.mat[ilk] + if usd_debt < usd_collateral: + continue + + self.dss.end.skim(urn.ilk, urn.address).transact(gas_price=self.gas_price) + if count % 50 == 0: + self.logger.info(f'Submitted skim transactions for {count} of {len(urns)} {ilk.name} urns') + # after the processing period, check whether we've skimmed enough to cover the surplus + if not undercollateralized_only: + self.reconcile_debt() + if self.dss.vat.dai(self.dss.vow.address) == Rad(0): + self.logger.info(f'Enough urns were skimmed to eliminate surplus') + break def thaw_cage(self): """ Once End.wait is reached, annihilate any lingering Dai in the vow, thaw the cage, and set the fix for all ilks """ @@ -242,91 +286,68 @@ def thaw_cage(self): self.logger.info('======== Thawing Cage ========') self.logger.info('') - ilks = self.get_ilks() + collaterals = self.get_collaterals() - # check if Dai is in Vow and annialate it with Heal() - dai = self.dss.vat.dai(self.dss.vow.address) - if dai > Rad(0): - self.dss.vow.heal(dai).transact(gas_price=self.gas_price) + # Reconcile surplus and debt to reduce joy, skim as necessary until surplus is eliminated + self.reconcile_debt() + if self.dss.vat.dai(self.dss.vow.address) > Rad(0): + ilks = list(map(lambda l: l.ilk, self.get_collaterals())) + self.skim_urns(ilks, undercollateralized_only=False) # Call thaw and Fix outstanding supply of Dai - self.dss.end.thaw().transact(gas_price=self.gas_price) + assert self.dss.end.thaw().transact(gas_price=self.gas_price) # Set fix (collateral/Dai ratio) for all Ilks - for ilk in ilks: - self.dss.end.flow(ilk).transact(gas_price=self.gas_price) - - - def get_ilks(self) -> List[Ilk]: + for collateral in collaterals: + self.dss.end.flow(collateral.ilk).transact(gas_price=self.gas_price) + if collateral.clipper: + self.dss.esm.deny(collateral.clipper.address).transact(gas_price=self.gas_price) + if collateral.flipper: + self.dss.esm.deny(collateral.flipper.address).transact(gas_price=self.gas_price) + + # Flow the PSM if configured to do so + if self.arguments.psm: + self.dss.end.flow(Ilk("PSM-USDC-A")).transact(gas_price=self.gas_price) + self.dss.esm.deny(Address(self.arguments.psm)).transact(gas_price=self.gas_price) + + def get_collaterals(self) -> List[Collateral]: """ Use Ilks as saved in https://github.com/makerdao/pymaker/tree/master/config """ - ilks = [self.dss.collaterals[key].ilk for key in self.dss.collaterals.keys()] - ilksFiltered = list(filter(lambda l: l.name != 'SAI', ilks)) - ilks_with_debt = list(filter(lambda l: self.dss.vat.ilk(l.name).art > Wad(0), ilksFiltered)) - - ilkNames = [i.name for i in ilks_with_debt] - - self.logger.info(f'Ilks to check: {ilkNames}') - - return ilks_with_debt - - - def get_underwater_urns(self, ilks: List) -> List[Urn]: - """ With all urns every frobbed, compile and return a list urns that are under-collateralized up to 100% """ - - underwater_urns = [] - - for ilk in ilks: - - urn_history = UrnHistory(self.web3, - self.dss, - ilk, - self.deployment_block, - self.arguments.vulcanize_endpoint, - self.arguments.vulcanize_key) - - urns = urn_history.get_urns() - - self.logger.info(f'Collected {len(urns)} from {ilk}') - - i = 0 - for urn in urns.values(): - urn.ilk = self.dss.vat.ilk(urn.ilk.name) - mat = self.dss.spotter.mat(urn.ilk) - usdDebt = Ray(urn.art) * urn.ilk.rate - usdCollateral = Ray(urn.ink) * urn.ilk.spot * mat - # Check if underwater -> urn.art * ilk.rate > urn.ink * ilk.spot * spotter.mat[ilk] - if usdDebt > usdCollateral: - underwater_urns.append(urn) - i += 1; - - if i % 100 == 0: - self.logger.info(f'Processed {i} urns of {ilk.name}') - - return underwater_urns + collaterals_filtered = filter(lambda l: l.ilk.name != 'SAI', self.dss.collaterals.values()) + collaterals_with_debt = list(filter(lambda l: self.dss.vat.ilk(l.ilk.name).art > Wad(0), collaterals_filtered)) + self.logger.info(f'Collaterals to check: {[c.ilk.name for c in collaterals_with_debt]}') + return collaterals_with_debt def all_active_auctions(self) -> dict: """ Aggregates active auctions that meet criteria to be called after Cage """ + clips = {} flips = {} for collateral in self.dss.collaterals.values(): - # Each collateral has it's own flip contract; add auctions from each. - flips[collateral.ilk.name] = self.cage_active_auctions(collateral.flipper) + # Each collateral has it's own contract; add auctions from each. + if collateral.clipper: + clips[collateral.ilk.name] = self.cage_active_auctions(collateral.clipper) + elif collateral.flipper: + flips[collateral.ilk.name] = self.cage_active_auctions(collateral.flipper) return { + "clips": clips, "flips": flips, "flaps": self.cage_active_auctions(self.dss.flapper), "flops": self.cage_active_auctions(self.dss.flopper) } - def cage_active_auctions(self, parentObj) -> List: """ Returns auctions that meet the requiremenets to be called by End.skip, Flap.yank, and Flop.yank """ active_auctions = [] auction_count = parentObj.kicks()+1 + # clip auctions + if isinstance(parentObj, Clipper): + active_auctions = parentObj.active_auctions() + # flip auctions - if isinstance(parentObj, Flipper): + elif isinstance(parentObj, Flipper): for index in range(1, auction_count): bid = parentObj._bids(index) if bid.guy != Address("0x0000000000000000000000000000000000000000"): @@ -341,11 +362,8 @@ def cage_active_auctions(self, parentObj) -> List: if bid.guy != Address("0x0000000000000000000000000000000000000000"): active_auctions.append(bid) index += 1 - - return active_auctions - def yank_auctions(self, flapBids: List, flopBids: List): """ Calls Flap.yank and Flop.yank on all auctions ids that meet the cage criteria """ for bid in flapBids: diff --git a/test.sh b/test.sh index 777d8d5..c43d970 100755 --- a/test.sh +++ b/test.sh @@ -1,7 +1,7 @@ #!/bin/bash # Pull the docker image -docker pull makerdao/testchain-pymaker:unit-testing +docker pull makerdao/testchain-pymaker:unit-testing-2.0.0 # Start the docker image and wait for parity to initialize pushd ./lib/pymaker diff --git a/tests/conftest.py b/tests/conftest.py index 83950a7..71bac44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -107,7 +107,7 @@ def mcd(web3) -> DssDeployment: @pytest.fixture(scope="session") def keeper(mcd: DssDeployment, keeper_address: Address) -> CageKeeper: - keeper = CageKeeper(args=args(f"--eth-from {keeper_address} --network testnet --vat-deployment-block {1}"), web3=mcd.web3) + keeper = CageKeeper(args=args(f"--eth-from {keeper_address} --vat-deployment-block {1}"), web3=mcd.web3) assert isinstance(keeper, CageKeeper) return keeper diff --git a/tests/test_cageKeeper.py b/tests/test_cageKeeper.py index fec5b9f..1e70b66 100644 --- a/tests/test_cageKeeper.py +++ b/tests/test_cageKeeper.py @@ -30,8 +30,8 @@ from pymaker import Address from pymaker.approval import directly, hope_directly from pymaker.auctions import Flapper, Flopper, Flipper -from pymaker.deployment import DssDeployment -from pymaker.dss import Collateral, Ilk, Urn +from pymaker.deployment import Collateral, DssDeployment +from pymaker.dss import Ilk, Urn from pymaker.numeric import Wad, Ray, Rad from pymaker.shutdown import ShutdownModule, End @@ -86,7 +86,7 @@ def create_surplus(mcd: DssDeployment, flapper: Flapper, deployment_address: Add if joy < mcd.vow.hump() + mcd.vow.bump(): # Create a CDP with surplus print('Creating a CDP with surplus') - collateral = mcd.collaterals['ETH-B'] + collateral = mcd.collaterals['ETH-C'] assert flapper.kicks() == 0 wrap_eth(mcd, deployment_address, Wad.from_number(10)) collateral.approve(deployment_address) @@ -119,7 +119,6 @@ def create_flap_auction(mcd: DssDeployment, deployment_address: Address, our_add assert kick == 1 assert len(flapper.active_auctions()) == 1 - mint_mkr(mcd.mkr, our_address, Wad.from_number(10)) flapper.approve(mcd.mkr.address, directly(from_address=our_address)) bid = Wad.from_number(0.001) @@ -150,7 +149,6 @@ def create_flop_auction(mcd: DssDeployment, deployment_address: Address, our_add check_active_auctions(flopper) current_bid = flopper.bids(kicks) - bid = Wad.from_number(0.000005) flopper.approve(mcd.vat.address, approval_function=hope_directly(from_address=our_address)) assert mcd.vat.can(our_address, flopper.address) @@ -179,14 +177,16 @@ def dent(flopper: Flopper, id: int, address: Address, lot: Wad, bid: Rad): assert flopper.dent(id, lot, bid).transact(from_address=address) -def create_flip_auction(mcd: DssDeployment, deployment_address: Address, our_address: Address): +def create_clip_auction(mcd: DssDeployment, deployment_address: Address, our_address: Address): assert isinstance(mcd, DssDeployment) assert isinstance(our_address, Address) assert isinstance(deployment_address, Address) - # Create a CDP - collateral = mcd.collaterals['ETH-A'] + collateral = mcd.collaterals['ETH-B'] ilk = collateral.ilk + original_price = Wad(mcd.web3.toInt(collateral.pip.read())) + + # Create a vault wrap_eth(mcd, deployment_address, Wad.from_number(1)) collateral.approve(deployment_address) assert collateral.adapter.join(deployment_address, Wad.from_number(1)).transact( @@ -195,15 +195,55 @@ def create_flip_auction(mcd: DssDeployment, deployment_address: Address, our_add dart = max_dart(mcd, collateral, deployment_address) - Wad(1) frob(mcd, collateral, deployment_address, dink=Wad(0), dart=dart) + # Undercollateralize and bark the vault + set_collateral_price(mcd, collateral, original_price / Wad.from_number(2)) + urn = mcd.vat.urn(collateral.ilk, deployment_address) + ilk = mcd.vat.ilk(ilk.name) + safe = Ray(urn.art) * mcd.vat.ilk(ilk.name).rate <= Ray(urn.ink) * ilk.spot + assert not safe + assert mcd.dog.bark(ilk, urn).transact() + clip_kick = collateral.clipper.kicks() + + # Generate some Dai, bid on part of the clip auction + wrap_eth(mcd, our_address, Wad.from_number(10)) + collateral.approve(our_address) + assert collateral.adapter.join(our_address, Wad.from_number(10)).transact(from_address=our_address) + mcd.web3.eth.defaultAccount = our_address.address + frob(mcd, collateral, our_address, dink=Wad.from_number(10), dart=Wad.from_number(200)) + collateral.clipper.approve(mcd.vat.address, approval_function=hope_directly()) + (needs_redo, price, lot, tab) = collateral.clipper.status(clip_kick) + assert collateral.clipper.take(clip_kick, lot/Wad.from_number(2), price, our_address).transact( + from_address=our_address) + + # Reset price + set_collateral_price(mcd, collateral, original_price) + + +def create_flip_auction(mcd: DssDeployment, other_address: Address, our_address: Address): + assert isinstance(mcd, DssDeployment) + assert isinstance(our_address, Address) + assert isinstance(other_address, Address) + + # Create a CDP + collateral = mcd.collaterals['ETH-A'] + ilk = collateral.ilk + wrap_eth(mcd, other_address, Wad.from_number(1)) + collateral.approve(other_address) + assert collateral.adapter.join(other_address, Wad.from_number(1)).transact( + from_address=other_address) + frob(mcd, collateral, other_address, dink=Wad.from_number(1), dart=Wad(0)) + # dart = max_dart(mcd, collateral, other_address) - Wad(1) + # frob(mcd, collateral, other_address, dink=Wad(0), dart=dart) + # Undercollateralize and bite the CDP to_price = Wad(mcd.web3.toInt(collateral.pip.read())) / Wad.from_number(2) set_collateral_price(mcd, collateral, to_price) - urn = mcd.vat.urn(collateral.ilk, deployment_address) + urn = mcd.vat.urn(collateral.ilk, other_address) ilk = mcd.vat.ilk(ilk.name) safe = Ray(urn.art) * mcd.vat.ilk(ilk.name).rate <= Ray(urn.ink) * ilk.spot assert not safe - assert mcd.cat.can_bite(collateral.ilk, Urn(deployment_address)) - assert mcd.cat.bite(collateral.ilk, Urn(deployment_address)).transact() + assert mcd.cat.can_bite(collateral.ilk, Urn(other_address)) + assert mcd.cat.bite(collateral.ilk, Urn(other_address)).transact() flip_kick = collateral.flipper.kicks() # Generate some Dai, bid on the flip auction without covering all the debt @@ -211,11 +251,10 @@ def create_flip_auction(mcd: DssDeployment, deployment_address: Address, our_add collateral.approve(our_address) assert collateral.adapter.join(our_address, Wad.from_number(10)).transact(from_address=our_address) mcd.web3.eth.defaultAccount = our_address.address - frob(mcd, collateral, our_address, dink=Wad.from_number(10), dart=Wad.from_number(200)) + frob(mcd, collateral, our_address, dink=Wad.from_number(10), dart=Wad.from_number(0)) collateral.flipper.approve(mcd.vat.address, approval_function=hope_directly()) current_bid = collateral.flipper.bids(flip_kick) urn = mcd.vat.urn(collateral.ilk, our_address) - assert Rad(urn.art) > current_bid.tab bid = Rad.from_number(6) tend(collateral.flipper, flip_kick, our_address, current_bid.lot, bid) @@ -245,7 +284,7 @@ def prepare_esm(mcd: DssDeployment, our_address: Address): assert isinstance(mcd.esm.address, Address) assert mcd.esm.sum() == Wad(0) assert mcd.esm.min() > Wad(0) - assert not mcd.esm.fired() + assert mcd.end.live() assert mcd.mkr.approve(mcd.esm.address).transact() @@ -264,7 +303,6 @@ def prepare_esm(mcd: DssDeployment, our_address: Address): def fire_esm(mcd: DssDeployment): assert mcd.end.live() assert mcd.esm.fire().transact() - assert mcd.esm.fired() assert not mcd.end.live() @@ -283,38 +321,17 @@ def test_check_deployment(self, mcd: DssDeployment, keeper: CageKeeper): print_out("test_check_deployment") keeper.check_deployment() - def test_get_underwater_urns(self, mcd: DssDeployment, keeper: CageKeeper, guy_address: Address, our_address: Address): - print_out("test_get_underwater_urns") + def test_get_collaterals(self, mcd: DssDeployment, keeper: CageKeeper): + print_out("test_get_collaterals") - previous_eth_price = open_underwater_urn(mcd, mcd.collaterals['ETH-A'], guy_address) - open_vault(mcd, mcd.collaterals['ETH-C'], our_address) - - ilks = keeper.get_ilks() - - urns = keeper.get_underwater_urns(ilks) - assert type(urns) is list - assert all(isinstance(x, Urn) for x in urns) - assert len(urns) == 1 - assert urns[0].address.address == guy_address.address - - ## We've multiplied by a small Ray amount to counteract - ## the residual dust (or lack thereof) in this step that causes - ## create_flop_auction fail - set_collateral_price(mcd, mcd.collaterals['ETH-A'], Wad(previous_eth_price * Ray.from_number(1.0001))) - - pytest.global_urns = urns - - def test_get_ilks(self, mcd: DssDeployment, keeper: CageKeeper): - print_out("test_get_ilks") - - ilks = keeper.get_ilks() - assert type(ilks) is list - assert all(isinstance(x, Ilk) for x in ilks) + collaterals = keeper.get_collaterals() + assert type(collaterals) is list + assert all(isinstance(x, Collateral) for x in collaterals) deploymentIlks = [mcd.vat.ilk(key) for key in mcd.collaterals.keys()] empty_deploymentIlks = list(filter(lambda l: mcd.vat.ilk(l.name).art == Wad(0), deploymentIlks)) - assert all(elem not in empty_deploymentIlks for elem in ilks) + assert all(elem.ilk not in empty_deploymentIlks for elem in collaterals) def test_active_auctions(self, mcd: DssDeployment, keeper: CageKeeper, our_address: Address, other_address: Address, deployment_address: Address): print_out("test_active_auctions") @@ -323,11 +340,13 @@ def test_active_auctions(self, mcd: DssDeployment, keeper: CageKeeper, our_addre create_flap_auction(mcd, deployment_address, our_address) create_flop_auction(mcd, deployment_address, other_address) + create_clip_auction(mcd, deployment_address, our_address) # this flip auction sets the collateral back to a price that makes the guy's vault underwater again. # 49 to make it underwater, and create_flip_auction sets it to 33 create_flip_auction(mcd, deployment_address, our_address) auctions = keeper.all_active_auctions() + assert "clips" in auctions assert "flips" in auctions assert "flops" in auctions assert "flaps" in auctions @@ -335,6 +354,13 @@ def test_active_auctions(self, mcd: DssDeployment, keeper: CageKeeper, our_addre nobody = Address("0x0000000000000000000000000000000000000000") # All auctions active before cage have been yanked + for ilk in auctions["clips"].keys(): + for auction in auctions["clips"][ilk]: + assert len(auctions["clips"][ilk]) == 1 + assert auction.id > 0 + assert auction.lot > Wad(0) + assert auction.usr != nobody + for ilk in auctions["flips"].keys(): for auction in auctions["flips"][ilk]: assert len(auctions["flips"][ilk]) == 1 @@ -387,11 +413,12 @@ def test_check_cage(self, mcd: DssDeployment, keeper: CageKeeper, our_address: A def test_cage_keeper(self, mcd: DssDeployment, keeper: CageKeeper, our_address: Address, other_address: Address): print_out("test_cage_keeper") - ilks = keeper.get_ilks() + ilks = list(map(lambda l: l.ilk, keeper.get_collaterals())) urns = pytest.global_urns auctions = pytest.global_auctions for ilk in ilks: + print(f"Checking end.tag for {ilk.name}") # Check if cage(ilk) called on all ilks assert mcd.end.tag(ilk) > Ray(0) @@ -404,6 +431,10 @@ def test_cage_keeper(self, mcd: DssDeployment, keeper: CageKeeper, our_address: assert urn.art == Wad(0) # All auctions active before cage have been yanked + for ilk in auctions["clips"].keys(): + for auction in auctions["clips"][ilk]: + assert mcd.collaterals[ilk].clipper.sales(auction.id).lot == Wad(0) + for ilk in auctions["flips"].keys(): for auction in auctions["flips"][ilk]: assert mcd.collaterals[ilk].flipper.bids(auction.id).lot == Wad(0)