def __init__(self, web3: Web3, address: Address): assert isinstance(web3, Web3) assert isinstance(address, Address) self.web3 = web3 self.address = address self._contract = self._get_contract(web3, self.abi, address) self.vat = Vat(self.web3, Address(self._contract.functions.vat().call()))
def from_json(web3: Web3, conf: str): conf = json.loads(conf) mom = DSGuard(web3, Address(conf['MCD_MOM'])) vat = Vat(web3, Address(conf['MCD_VAT'])) vow = Vow(web3, Address(conf['MCD_VOW'])) drip = Drip(web3, Address(conf['MCD_DRIP'])) pit = Pit(web3, Address(conf['MCD_PIT'])) cat = Cat(web3, Address(conf['MCD_CAT'])) flap = Flapper(web3, Address(conf['MCD_FLAP'])) flop = Flopper(web3, Address(conf['MCD_FLOP'])) dai = DSToken(web3, Address(conf['MCD_DAI'])) dai_adapter = DaiAdapter(web3, Address(conf['MCD_JOIN_DAI'])) dai_move = DaiVat(web3, Address(conf['MCD_MOVE_DAI'])) mkr = DSToken(web3, Address(conf['MCD_GOV'])) collaterals = [] for name in conf['COLLATERALS']: collateral = Collateral(Ilk(name)) collateral.gem = DSToken(web3, Address(conf[name])) collateral.adapter = GemAdapter(web3, Address(conf[f'MCD_JOIN_{name}'])) collateral.mover = GemVat(web3, Address(conf[f'MCD_MOVE_{name}'])) collateral.flipper = Flipper(web3, Address(conf[f'MCD_FLIP_{name}'])) collateral.pip = DSValue(web3, Address(conf[f'PIP_{name}'])) collateral.spotter = Spotter(web3, Address(conf[f'MCD_SPOT_{name}'])) collaterals.append(collateral) return DssDeployment.Config(mom, vat, vow, drip, pit, cat, flap, flop, dai, dai_adapter, dai_move, mkr, collaterals)
def from_json(web3: Web3, conf: str): conf = json.loads(conf) pause = DSPause(web3, Address(conf['MCD_PAUSE'])) vat = Vat(web3, Address(conf['MCD_VAT'])) vow = Vow(web3, Address(conf['MCD_VOW'])) jug = Jug(web3, Address(conf['MCD_JUG'])) cat = Cat(web3, Address(conf['MCD_CAT'])) dai = DSToken(web3, Address(conf['MCD_DAI'])) dai_adapter = DaiJoin(web3, Address(conf['MCD_JOIN_DAI'])) flapper = Flapper(web3, Address(conf['MCD_FLAP'])) flopper = Flopper(web3, Address(conf['MCD_FLOP'])) mkr = DSToken(web3, Address(conf['MCD_GOV'])) spotter = Spotter(web3, Address(conf['MCD_SPOT'])) collaterals = {} for name in DssDeployment.Config._infer_collaterals_from_addresses(conf.keys()): ilk = Ilk(name[0].replace('_', '-')) if name[1] == "ETH": gem = DSEthToken(web3, Address(conf[name[1]])) else: gem = DSToken(web3, Address(conf[name[1]])) # TODO: If problematic, skip pip for deployments which use a medianizer. collateral = Collateral(ilk=ilk, gem=gem, adapter=GemJoin(web3, Address(conf[f'MCD_JOIN_{name[0]}'])), flipper=Flipper(web3, Address(conf[f'MCD_FLIP_{name[0]}'])), pip=DSValue(web3, Address(conf[f'PIP_{name[1]}']))) collaterals[ilk.name] = collateral return DssDeployment.Config(pause, vat, vow, jug, cat, flapper, flopper, dai, dai_adapter, mkr, spotter, collaterals)
def from_json(web3: Web3, conf: str): conf = json.loads(conf) pause = DSPause(web3, Address(conf['MCD_PAUSE'])) vat = Vat(web3, Address(conf['MCD_VAT'])) vow = Vow(web3, Address(conf['MCD_VOW'])) jug = Jug(web3, Address(conf['MCD_JUG'])) cat = Cat(web3, Address(conf['MCD_CAT'])) dai = DSToken(web3, Address(conf['MCD_DAI'])) dai_adapter = DaiJoin(web3, Address(conf['MCD_JOIN_DAI'])) flapper = Flapper(web3, Address(conf['MCD_FLAP'])) flopper = Flopper(web3, Address(conf['MCD_FLOP'])) pot = Pot(web3, Address(conf['MCD_POT'])) mkr = DSToken(web3, Address(conf['MCD_GOV'])) spotter = Spotter(web3, Address(conf['MCD_SPOT'])) ds_chief = DSChief(web3, Address(conf['MCD_ADM'])) esm = ShutdownModule(web3, Address(conf['MCD_ESM'])) end = End(web3, Address(conf['MCD_END'])) proxy_registry = ProxyRegistry(web3, Address(conf['PROXY_REGISTRY'])) dss_proxy_actions = DssProxyActionsDsr( web3, Address(conf['PROXY_ACTIONS_DSR'])) collaterals = {} for name in DssDeployment.Config._infer_collaterals_from_addresses( conf.keys()): ilk = Ilk(name[0].replace('_', '-')) if name[1] == "ETH": gem = DSEthToken(web3, Address(conf[name[1]])) else: gem = DSToken(web3, Address(conf[name[1]])) # PIP contract may be a DSValue, OSM, or bogus address. pip_address = Address(conf[f'PIP_{name[1]}']) network = DssDeployment.NETWORKS.get(web3.net.version, "testnet") if network == "testnet": pip = DSValue(web3, pip_address) else: pip = OSM(web3, pip_address) collateral = Collateral( ilk=ilk, gem=gem, adapter=GemJoin(web3, Address(conf[f'MCD_JOIN_{name[0]}'])), flipper=Flipper(web3, Address(conf[f'MCD_FLIP_{name[0]}'])), pip=pip) collaterals[ilk.name] = collateral return DssDeployment.Config(pause, vat, vow, jug, cat, flapper, flopper, pot, dai, dai_adapter, mkr, spotter, ds_chief, esm, end, proxy_registry, dss_proxy_actions, collaterals)
def __init__(self, web3: Web3, address: Address): super(Clipper, self).__init__(web3, address, Clipper.abi) assert isinstance(web3, Web3) assert isinstance(address, Address) self.web3 = web3 self.address = address self._contract = self._get_contract(web3, self.abi, address) # Albeit more elegant, this is inconsistent with AuctionContract.vat(), a method call self.calc = Address(self._contract.functions.calc().call()) self.dog = Dog(web3, Address(self._contract.functions.dog().call())) self.vat = Vat(web3, Address(self._contract.functions.vat().call())) self.take_abi = None self.redo_abi = None for member in self.abi: if not self.take_abi and member.get('name') == 'Take': self.take_abi = member if not self.redo_abi and member.get('name') == 'Redo': self.redo_abi = member
def from_json(web3: Web3, conf: str): def address_in_configs(key: str, conf: str) -> bool: if key not in conf: return False elif not conf[key]: return False elif conf[key] == "0x0000000000000000000000000000000000000000": return False else: return True conf = json.loads(conf) pause = DSPause(web3, Address(conf['MCD_PAUSE'])) vat = Vat(web3, Address(conf['MCD_VAT'])) vow = Vow(web3, Address(conf['MCD_VOW'])) jug = Jug(web3, Address(conf['MCD_JUG'])) cat = Cat(web3, Address(conf['MCD_CAT'])) if address_in_configs( 'MCD_CAT', conf) else None dog = Dog(web3, Address(conf['MCD_DOG'])) if address_in_configs( 'MCD_DOG', conf) else None dai = DSToken(web3, Address(conf['MCD_DAI'])) dai_adapter = DaiJoin(web3, Address(conf['MCD_JOIN_DAI'])) flapper = Flapper(web3, Address(conf['MCD_FLAP'])) flopper = Flopper(web3, Address(conf['MCD_FLOP'])) pot = Pot(web3, Address(conf['MCD_POT'])) mkr = DSToken(web3, Address(conf['MCD_GOV'])) spotter = Spotter(web3, Address(conf['MCD_SPOT'])) ds_chief = DSChief(web3, Address(conf['MCD_ADM'])) esm = ShutdownModule(web3, Address(conf['MCD_ESM'])) end = End(web3, Address(conf['MCD_END'])) proxy_registry = ProxyRegistry(web3, Address(conf['PROXY_REGISTRY'])) dss_proxy_actions = DssProxyActionsDsr( web3, Address(conf['PROXY_ACTIONS_DSR'])) cdp_manager = CdpManager(web3, Address(conf['CDP_MANAGER'])) dsr_manager = DsrManager(web3, Address(conf['DSR_MANAGER'])) faucet = TokenFaucet( web3, Address(conf['FAUCET'])) if address_in_configs( 'FAUCET', conf) else None collaterals = {} for name in DssDeployment.Config._infer_collaterals_from_addresses( conf.keys()): ilk = vat.ilk(name[0].replace('_', '-')) if name[1] == "ETH": gem = DSEthToken(web3, Address(conf[name[1]])) else: gem = DSToken(web3, Address(conf[name[1]])) if name[1] in [ 'USDC', 'WBTC', 'TUSD', 'USDT', 'GUSD', 'RENBTC' ]: adapter = GemJoin5(web3, Address(conf[f'MCD_JOIN_{name[0]}'])) else: adapter = GemJoin(web3, Address(conf[f'MCD_JOIN_{name[0]}'])) # PIP contract may be a DSValue, OSM, or bogus address. pip_name = f'PIP_{name[1]}' pip_address = Address( conf[pip_name] ) if pip_name in conf and conf[pip_name] else None val_name = f'VAL_{name[1]}' val_address = Address( conf[val_name] ) if val_name in conf and conf[val_name] else None if pip_address: # Configure OSM as price source if name[1].startswith('UNIV2'): pip = Univ2LpOSM(web3, pip_address) else: pip = OSM(web3, pip_address) elif val_address: # Configure price using DSValue pip = DSValue(web3, val_address) else: pip = None auction = None if f'MCD_FLIP_{name[0]}' in conf: auction = Flipper(web3, Address(conf[f'MCD_FLIP_{name[0]}'])) elif f'MCD_CLIP_{name[0]}' in conf: auction = Clipper(web3, Address(conf[f'MCD_CLIP_{name[0]}'])) collateral = Collateral(ilk=ilk, gem=gem, adapter=adapter, auction=auction, pip=pip, vat=vat) collaterals[ilk.name] = collateral return DssDeployment.Config(pause, vat, vow, jug, cat, dog, flapper, flopper, pot, dai, dai_adapter, mkr, spotter, ds_chief, esm, end, proxy_registry, dss_proxy_actions, cdp_manager, dsr_manager, faucet, collaterals)
class Clipper(AuctionContract): """A client for the `Clipper` contract, used to interact with collateral auctions. You can find the source code of the `Clipper` contract here: <https://github.com/makerdao/dss/blob/master/src/clip.sol>. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the `Clipper` contract. """ abi = Contract._load_abi(__name__, 'abi/Clipper.abi') bin = Contract._load_bin(__name__, 'abi/Clipper.bin') class KickLog: def __init__(self, log): args = log['args'] self.id = args['id'] self.top = Ray(args['top']) # starting price self.tab = Rad(args['tab']) # debt self.lot = Wad(args['lot']) # collateral self.usr = Address(args['usr']) # liquidated vault self.kpr = Address(args['kpr']) # keeper who barked self.coin = Rad(args['coin']) # total kick incentive (tip + tab*chip) self.block = log['blockNumber'] self.tx_hash = log['transactionHash'].hex() def __repr__(self): return f"Clipper.KickLog({pformat(vars(self))})" class TakeLog: def __init__(self, log, sender): args = log['args'] self.id = args['id'] self.max = Ray(args['max']) # Max bid price specified self.price = Ray(args['price']) # Calculated bid price self.owe = Rad(args['owe']) # Dai needed to satisfy the calculated bid price self.tab = Rad(args['tab']) # Remaining debt self.lot = Wad(args['lot']) # Remaining lot self.usr = Address(args['usr']) # Liquidated vault self.block = log['blockNumber'] self.tx_hash = log['transactionHash'].hex() self.sender = sender def __repr__(self): return f"Clipper.TakeLog({pformat(vars(self))})" class RedoLog(KickLog): # Same fields as KickLog def __repr__(self): return f"Clipper.RedoLog({pformat(vars(self))})" class Sale: def __init__(self, id: int, pos: int, tab: Rad, lot: Wad, usr: Address, tic: int, top: Ray): assert(isinstance(id, int)) assert(isinstance(pos, int)) assert(isinstance(tab, Rad)) assert(isinstance(lot, Wad)) assert(isinstance(usr, Address)) assert(isinstance(tic, int)) assert(isinstance(top, Ray)) self.id = id # auction identifier self.pos = pos # active index self.tab = tab # dai to raise self.lot = lot # collateral to sell self.usr = usr # liquidated urn address self.tic = tic # auction start time self.top = top # starting price def __repr__(self): return f"Clipper.Sale({pformat(vars(self))})" def __init__(self, web3: Web3, address: Address): super(Clipper, self).__init__(web3, address, Clipper.abi) assert isinstance(web3, Web3) assert isinstance(address, Address) self.web3 = web3 self.address = address self._contract = self._get_contract(web3, self.abi, address) # Albeit more elegant, this is inconsistent with AuctionContract.vat(), a method call self.calc = Address(self._contract.functions.calc().call()) self.dog = Dog(web3, Address(self._contract.functions.dog().call())) self.vat = Vat(web3, Address(self._contract.functions.vat().call())) self.take_abi = None self.redo_abi = None for member in self.abi: if not self.take_abi and member.get('name') == 'Take': self.take_abi = member if not self.redo_abi and member.get('name') == 'Redo': self.redo_abi = member def active_auctions(self) -> list: active_auctions = [] for index in range(1, self.kicks()+1): sale = self.sales(index) if sale.usr != Address.zero(): active_auctions.append(sale) index += 1 return active_auctions def ilk_name(self) -> str: ilk = self._contract.functions.ilk().call() return Web3.toText(ilk.strip(bytes(1))) def buf(self) -> Ray: """Multiplicative factor to increase starting price""" return Ray(self._contract.functions.buf().call()) def tail(self) -> int: """Time elapsed before auction reset""" return int(self._contract.functions.tail().call()) def cusp(self) -> Ray: """Percentage drop before auction reset""" return Ray(self._contract.functions.cusp().call()) def chip(self) -> Wad: """Percentage of tab to suck from vow to incentivize keepers""" return Wad(self._contract.functions.chip().call()) def tip(self) -> Rad: """Flat fee to suck from vow to incentivize keepers""" return Rad(self._contract.functions.tip().call()) def chost(self) -> Rad: """Ilk dust times the ilk chop""" return Rad(self._contract.functions.chost().call()) def kicks(self) -> int: """Number of auctions started so far.""" return int(self._contract.functions.kicks().call()) def active_count(self) -> int: """Number of active and redoable auctions.""" return int(self._contract.functions.count().call()) def status(self, id: int) -> (bool, Ray, Wad, Rad): """Indicates current state of the auction Args: id: Auction identifier. """ assert isinstance(id, int) (needs_redo, price, lot, tab) = self._contract.functions.getStatus(id).call() logging.debug(f"Auction {id} {'needs redo ' if needs_redo else ''}with price={float(Ray(price))} " f"lot={float(Wad(lot))} tab={float(Rad(tab))}") return needs_redo, Ray(price), Wad(lot), Rad(tab) def sales(self, id: int) -> Sale: """Returns the auction details. Args: id: Auction identifier. Returns: The auction details. """ assert(isinstance(id, int)) array = self._contract.functions.sales(id).call() return Clipper.Sale(id=id, pos=int(array[0]), tab=Rad(array[1]), lot=Wad(array[2]), usr=Address(array[3]), tic=int(array[4]), top=Ray(array[5])) def validate_take(self, id: int, amt: Wad, max: Ray, our_address: Address = None): """Raise assertion if collateral cannot be purchased from an auction as desired""" assert isinstance(id, int) assert isinstance(amt, Wad) assert isinstance(max, Ray) if our_address: assert isinstance(our_address, Address) else: our_address = Address(self.web3.eth.defaultAccount) (done, price, lot, tab) = self.status(id) assert not done assert max >= price slice: Wad = min(lot, amt) # Purchase as much as possible, up to amt owe: Rad = Rad(slice) * Rad(price) # DAI needed to buy a slice of this sale chost = self.chost() if Rad(owe) > tab: owe = Rad(tab) slice = Wad(owe / Rad(price)) elif owe < tab and slice < lot: if (tab - owe) < chost: assert tab > chost owe = tab - chost slice = Wad(owe / Rad(price)) tab: Rad = tab - owe lot: Wad = lot - slice assert self.vat.dai(our_address) >= owe logger.debug(f"Validated clip.take which will leave tab={float(tab)} and lot={float(lot)}") def take(self, id: int, amt: Wad, max: Ray, who: Address = None, data=b'') -> Transact: """Buy amount of collateral from auction indexed by id. Args: id: Auction id amt: Upper limit on amount of collateral to buy max: Maximum acceptable price (DAI / collateral) who: Receiver of collateral and external call address data: Data to pass in external call; if length 0, no call is done """ assert isinstance(id, int) assert isinstance(amt, Wad) assert isinstance(max, Ray) if who: assert isinstance(who, Address) else: who = Address(self.web3.eth.defaultAccount) return Transact(self, self.web3, self.abi, self.address, self._contract, 'take', [id, amt.value, max.value, who.address, data]) def redo(self, id: int, kpr: Address = None) -> Transact: """Restart an auction which ended without liquidating all collateral. id: Auction id kpr: Keeper that called dog.bark() """ assert isinstance(id, int) assert isinstance(kpr, Address) or kpr is None if kpr: assert isinstance(kpr, Address) else: kpr = Address(self.web3.eth.defaultAccount) return Transact(self, self.web3, self.abi, self.address, self._contract, 'redo', [id, kpr.address]) def upchost(self): """Update the the cached dust*chop value following a governance change""" return Transact(self, self.web3, self.abi, self.address, self._contract, 'upchost', []) def past_logs(self, from_block: int, to_block: int = None, chunk_size=20000): logs = super().get_past_lognotes(Clipper.abi, from_block, to_block, chunk_size) history = [] for log in logs: if log is None: continue elif isinstance(log, Clipper.KickLog) \ or isinstance(log, Clipper.TakeLog) \ or isinstance(log, Clipper.RedoLog): history.append(log) else: logger.debug(f"Found log with signature {log.sig}") return history def parse_event(self, event): signature = Web3.toHex(event['topics'][0]) codec = ABICodec(default_registry) if signature == "0x7c5bfdc0a5e8192f6cd4972f382cec69116862fb62e6abff8003874c58e064b8": event_data = get_event_data(codec, self.kick_abi, event) return Clipper.KickLog(event_data) elif signature == "0x05e309fd6ce72f2ab888a20056bb4210df08daed86f21f95053deb19964d86b1": event_data = get_event_data(codec, self.take_abi, event) self._get_sender_for_eventlog(event_data) return Clipper.TakeLog(event_data, self._get_sender_for_eventlog(event_data)) elif signature == "0x275de7ecdd375b5e8049319f8b350686131c219dd4dc450a08e9cf83b03c865f": event_data = get_event_data(codec, self.redo_abi, event) return Clipper.RedoLog(event_data) else: logger.debug(f"Found event signature {signature}") def _get_sender_for_eventlog(self, event_data) -> Address: tx_hash = event_data['transactionHash'].hex() receipt = self.web3.eth.getTransactionReceipt(tx_hash) return Address(receipt['from']) def __repr__(self): return f"Clipper('{self.address}')"
class CdpManager(Contract): """A client for the `DSCdpManger` contract, which is a wrapper around the cdp system, for easier use. Ref. <https://github.com/makerdao/dss-cdp-manager/blob/master/src/DssCdpManager.sol> """ abi = Contract._load_abi(__name__, 'abi/DssCdpManager.abi') bin = Contract._load_bin(__name__, 'abi/DssCdpManager.bin') def __init__(self, web3: Web3, address: Address): assert isinstance(web3, Web3) assert isinstance(address, Address) self.web3 = web3 self.address = address self._contract = self._get_contract(web3, self.abi, address) self.vat = Vat(self.web3, Address(self._contract.functions.vat().call())) def open(self, ilk: Ilk, address: Address) -> Transact: assert isinstance(ilk, Ilk) assert isinstance(address, Address) return Transact(self, self.web3, self.abi, self.address, self._contract, 'open', [ilk.toBytes(), address.address]) def urn(self, cdpid: int) -> Urn: '''Returns Urn for respective CDP ID''' assert isinstance(cdpid, int) urn_address = Address(self._contract.functions.urns(cdpid).call()) ilk = self.ilk(cdpid) urn = self.vat.urn(ilk, Address(urn_address)) return urn def owns(self, cdpid: int) -> Address: '''Returns owner Address of respective CDP ID''' assert isinstance(cdpid, int) owner = Address(self._contract.functions.owns(cdpid).call()) return owner def ilk(self, cdpid: int) -> Ilk: '''Returns Ilk for respective CDP ID''' assert isinstance(cdpid, int) ilk = Ilk.fromBytes(self._contract.functions.ilks(cdpid).call()) return ilk def first(self, address: Address) -> int: '''Returns first CDP Id created by owner address''' assert isinstance(address, Address) cdpid = int(self._contract.functions.first(address.address).call()) return cdpid def last(self, address: Address) -> int: '''Returns last CDP Id created by owner address''' assert isinstance(address, Address) cdpid = self._contract.functions.last(address.address).call() return int(cdpid) def count(self, address: Address) -> int: '''Returns number of CDP's created using the DS-Cdp-Manager contract specifically''' assert isinstance(address, Address) count = int(self._contract.functions.count(address.address).call()) return count def __repr__(self): return f"CdpManager('{self.address}')"
def deploy(web3: Web3, debt_ceiling: Wad): assert isinstance(web3, Web3) vat = Vat.deploy(web3=web3) pit = Pit.deploy(web3=web3, vat=vat.address) assert pit.file_global_line(debt_ceiling).transact() # Global debt Ceiling assert vat.rely(pit.address).transact() dai = DSToken.deploy(web3=web3, symbol='DAI') dai_adapter = DaiAdapter.deploy(web3=web3, vat=vat.address, dai=dai.address) dai_move = DaiVat.deploy(web3=web3, vat=vat.address) assert vat.rely(dai_adapter.address).transact() assert vat.rely(dai_move.address).transact() mkr = DSToken.deploy(web3=web3, symbol='MKR') # TODO: use a DSProxy mom = DSGuard.deploy(web3) assert mom.permit(DSGuard.ANY, DSGuard.ANY, DSGuard.ANY).transact() assert dai.set_authority(mom.address).transact() assert mkr.set_authority(mom.address).transact() vow = Vow.deploy(web3=web3) drip = Drip.deploy(web3=web3, vat=vat.address) flap = Flapper.deploy(web3=web3, dai=dai_move.address, gem=mkr.address) assert vow.file_vat(vat).transact() assert vow.file_flap(flap).transact() assert vow.file_bump(Wad.from_number(1000)).transact() assert vow.file_sump(Wad.from_number(10)).transact() assert drip.file_vow(vow).transact() assert vat.rely(vow.address).transact() assert vat.rely(drip.address).transact() assert vat.rely(flap.address).transact() cat = Cat.deploy(web3=web3, vat=vat.address) assert cat.file_vow(vow).transact() assert cat.file_pit(pit).transact() flop = Flopper.deploy(web3=web3, dai=dai_move.address, gem=mkr.address) assert vow.file_flop(flop).transact() assert vat.rely(cat.address).transact() assert vat.rely(flop.address).transact() assert vow.rely(cat.address).transact() assert flop.rely(vow.address).transact() config = DssDeployment.Config(mom, vat, vow, drip, pit, cat, flap, flop, dai, dai_adapter, dai_move, mkr) deployment = DssDeployment(web3, config) collateral = Collateral.deploy(web3=web3, name='WETH', vat=vat) deployment.deploy_collateral(collateral, debt_ceiling=Wad.from_number(100000), penalty=Ray.from_number(1), flop_lot=Wad.from_number(10000), ratio=Ray.from_number(1.5), initial_price=Wad.from_number(219)) return deployment